Projekt tłumaczenia rozmowy

Model danych

Programy stabilneHLO to obliczenia oparte na tensorach. (tablice n-wymiarowe), które w obecnym modelu są zaimplementowane za pomocą zajęcia Tensor. bazową klasę pamięci masowej obiektu Tensor, detail::Buffer przechowuje mlir::ShapedType tensora wraz z parametrem Obiekt mlir::HeapAsmResourceBlob reprezentujący zmienny obiekt blob tensora danych w postaci ciągłej tablicy bajtów Zamówienie od dużych do mniejszych. detail::Buffer obiektów jest zliczanych, aby uprościć zarządzanie pamięcią.

Poszczególne elementy tensora są reprezentowane za pomocą klasy Element, która korzysta z dyskryminowanego związku zawierającego jeden z tych elementów: APInt, APFloat lub pair<APFloat,APFloat> na miejsce. Ostatni służy do przechowywania elementów ze złożonymi typami.

Tensor udostępnia te interfejsy API do interakcji z poszczególnymi elementami:

  • Element Tensor::get(llvm::ArrayRef<int64_t> index): aby wyodrębnić pojedynczy element tensora o wielowymiarowym indeksie index jako Element obiektu.
  • void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);: Aby zaktualizować obiekt Element element w tensor w wielowymiarowym indeks index.

Jak działa tłumacz

Funkcja wprowadzania danych do interpretera to

SmallVector<Tensor> eval(func::FuncOp func, ArrayRef<Tensor> args);

który:

  1. Śledzi argumenty SSA usługi func i powiązane z nią środowisko wykonawcze Tensor podane w komórce args za pomocą mapy tabeli symboli, M.
  2. Dla każdej operacji w ramach func, w kolejności SSACFG:
    • Wywołuje op. eval Dla każdego operandu SSA operacji wyodrębnij jego argument wartość czasu działania z M jako argument w wywołaniu eval.
    • Śledzi wyniki SSA operacji i ocenioną wartość w M.

eval na poziomie działania, o którym mowa w punkcie (2), jest odpowiedzialny za implementację semantyka wykonania op. Oto przykład dla stablehlo::AddOp. W tym przykładzie poszczególne elementy tensorów lhs i rhs są nazywane parami. jest wyodrębniane jako obiekty Element, które są następnie dodawane. Wynik dodania: obiekt Element jest przechowywany w końcowym tensorze result.

Tensor eval(AddOp op, const Tensor &lhs, const Tensor &rhs) {
  Tensor result(op.getType());

  for (auto it = result.index_begin(); it != result.index_end(); ++it)
    result.set(*it, lhs.get(*it) + rhs.get(*it));

  return result;
}

Konstrukcja tłumaczenia rozmowy jest zoptymalizowana pod kątem czytelności implementacji funkcji eval w poszczególnych operacjach, ponieważ ma ono za zadanie służą jako referencyjna implementacja StableHLO. Na przykład zamiast Zdefiniowanie atrybutu eval jako funkcji szablonu i dodanie do niego parametrów za pomocą typów elementów, zawarte są w nim szczegółowe informacje o obsłudze różnych typów elementów Element::operator+ itp., aby uprościć implementację eval.

Korzystanie z tłumacza w celu ciągłego składania aplikacji

Możemy użyć mechanizmu tłumaczenia rozmowy, by zawijać operacje ze stałym operandem . Poniższy fragment kodu ilustruje koncepcję implementacji do składania stablehlo::AddOp za pomocą operandów typu zmiennoprzecinkowego:

OpFoldResult AddOp::fold(FoldAdaptor adaptor) {
  auto attrs = adaptor.getOperands();
  DenseElementsAttr lhsData = dyn_cast<DenseElementsAttr>(attrs[0]);
  DenseElementsAttr rhsData = dyn_cast<DenseElementsAttr>(attrs[1]);
  if (!lhsData || !rhsData) return {};

  auto lhs = Tensor(lhsData);
  auto rhs = Tensor(rhsData);
  auto result = eval(*this, lhs, rhs);

  SmallVector<APFloat> values;
  for (auto i = 0; i < result.getNumElements(); ++i) {
    Element element = result.get(i);
    values.push_back(cast<FloatAttr>(element.getValue()).getValue());
  }

  return DenseElementsAttr::get(result.getType(), values);
}

Obecnie nie pracujemy nad integracją tłumacza ciągłego zwijania, ponieważ nie planujemy wdrażać folderu dla StableHLO. Jednak w przyszłości planujemy używać tłumacza do stałego do formatu MHLO, co pozwoli poprawić ergonomię fragmentu kodu. powyżej (np. możemy mieć funkcję pomocniczą, która umieszcza stałe operandy w Tensor obiektów i rozpakowuje wyniki (Tensor) do zakresu OpFoldResult.

Testowanie interpretera StableHLO

Tłumacz pobiera jako dane wejściowe (A), program StableHLO, a (B) wartości danych w i generuje wartości danych wyjściowych, które są dopasowane z oczekiwanymi wartościami danych przekazywanych przez użytkowników. Wartości danych (B) to zakodowanych na stałe w programie przy użyciu operacji stablehlo.constant. tłumacz ocenia program do wprowadzania danych. Wyniki testu objętego testem jest sprawdzana za pomocą weryfikacji (np. check.expect_eq, check.expect_almost_eq), jak poniżej. Sprawdź check.expect_eq i check.expect_eq_const pod kątem przetwarzania bitowego równość dla każdego obsługiwanego typu oraz check.expect_almost_eq i check.expect_almost_eq_const sprawdź pod kątem niemal równości w ramach tolerancji, wyjaśniono w wytycznych dotyczących testowania (G6) dla typów zmiennoprzecinkowych i złożonych.

// CHECK-LABEL: Evaluated results of function: add_op_test_ui4
func.func @add_op_test_ui4() {
  %0 = stablehlo.constant dense<[0, 2]> : tensor<2xui4>
  %1 = stablehlo.constant dense<[15, 3]> : tensor<2xui4>
  %2 = stablehlo.add %0, %1 : tensor<2xui4>
  check.expect_eq_const %2, [15, 5] : tensor<2xui4>
  func.return
}

Narzędzie testowe stablehlo-translate --interpret (kod). odpowiada za analizę programu, interpretację każdej funkcji, w tym operacji, z których składa się funkcja. Mamy specjalny pakiet testowy, kilku testów sprawdzających różne zachowania w czasie działania dla każdej wersji StableHLO Testy znajdziesz tutaj.

Wskazówki dotyczące przeprowadzania testów

(G1) Czy musimy przetestować wszystkie obsługiwane typy w przypadku każdej operacji?

Możemy zastosować kombinację następujących reguł, aby podjąć decyzję:

  1. Jeśli podczas wdrażania operacji istnieje kod w odpowiednim argumencie eval do obsługi określonego typu funkcji, koniecznie musisz mieć testy aby go uwzględnić. Na przykład w kampanii add dostępny jest kod na wyłączność obsługuje liczby całkowite, logiczne, zmiennoprzecinkowe i złożone, dlatego wymagają jednego testu dla każdej kategorii.

  2. Jeśli zbiór typów jest obsługiwany jednakowo w odpowiedniej funkcji eval, wystarczy jeden test dla wszystkich tych typów. Na przykład: w przypadku operacji add wszystkie warianty typów liczb całkowitych (si4, u4, si8, u8 i tak dalej) są obsługiwane w taki sam sposób przy użyciu interfejsów API llvm::APInt, dlatego możemy pominąć dodaj testy każdego z tych wariantów, a zamiast tego dodaj jeden reprezentatywny test. Aby uniknąć niejasności przy wyborze przedstawiciela, zapoznaj się z tymi wytycznymi:

    • Jeśli wszystkie typy, obsługiwane jednolicie, mają ten sam typ podstawowy (jeśli wszystkie są liczbami całkowitymi, zmiennoprzecinkową lub złożonymi), to wybierz ten o maksymalnej szerokości bitów.
    • Jeśli wszystkie typy, obsługiwane jednolicie, zawierają kombinację typów podstawowych, to wybierz ten z typem podstawowym w kolejności malejącej preferencja: liczba całkowita, zmiennoprzecinkowa, logiczna, zespolona.

(G2) Jak ustalamy liczbę testów niezbędnych do obsłużenia kampanii zachowanie użytkownika?

Celem jest kompleksowe omówienie logiki tłumacza danego tekstu (tj. we wszystkich rogach implementacji) przy minimalnej liczbie testów. Minimalizowanie liczby testów jest ważne ze względu na łatwość konserwacji. Im mniej testów tym łatwiej będzie je sprawdzić i upewnić się, kompleksowo omówię op. W związku z tym oczekujemy, że większość prostszych będziemy przeprowadzać tylko jeden test. Jeśli z jakiegoś powodu kompleksowe jest niepraktyczne, można go zatrzymać na poziomie >= 90%. Zostanie to podjęta decyzja indywidualnie w trakcie sprawdzania żądania pull.

(G3) Co z dodaniem testów na potrzeby infrastruktury tłumaczenia rozmowy?

Infrastruktura tłumacza jest przeważnie prosta i można ją dodać w naszej bazie zaufania. Jedyną niezrozumiałą częścią jest sposób przedstawienia różnych rodzajów i rozpakowane z bazowej pamięci interpretera. Jak już wspomnieliśmy (G1), będzie testować tylko te typy operacji, które są obsługiwane w różny sposób. Na że jest możliwe, że kod do pakowania lub rozpakowania, który odpowiada różnym wersji liczb całkowitych/zmiennych, mogą nie zostać w pełni uwzględnione i testowania. Aby zapewnić wszystkie materiały, możemy wybrać kampanię taką jak constant, która obsługuje wszystkie typy elementów StableHLO i umożliwia pisanie szczegółowych testów.

(G4) Jeśli implementacja operacji zależy od innych działań, czy powinniśmy napisać w przypadku tego drugiego rodzaju badań?

Nie. Na przykład implementacja batch_norm_grad może opierać się na divide, subtract, multiply i inne. Powinniśmy unikać testowania tego drugiego sposobu podczas testowania pierwszej wersji.

(G5) Czy powinniśmy napisać testy służące do wykonywania zachowania użytkowników?

Nie należy pisać testów, które wykonują niezdefiniowane zachowanie op. Testy zachowań zdefiniowane w implementacji demonstrować lokalne zachowanie tłumacza, którego nie należy posługiwać się uogólnionych. Testy wykonujące nieokreślone zachowanie nie mają wpływu zrozumienia działania operatora.

(G6) Pisząc testy typów zmiennoprzecinkowych, zwiększasz dokładność należy określić oczekiwany wynik?

Działania podstawowe (dodawanie, odejmowanie, mnożenie, dzielenie i kwadrat), wdrożenie zgodne ze specyfikacją IEEE powinno zapewnić zaokrąglony wynik z zakresu 0,5 ULP od dokładnego wyniku matematycznego. Jednakże może bezpiecznie sobie wyobrazić, że oczekiwany spodziewany efekt większość zasobów ULP znajduje się w odległości 1 od siebie. Może to jednak nie działać w przypadku funkcji transcendentalnych (sine, cosine itp.), w przypadku których gwarancje precyzji są zdefiniowaną (uzasadnienie).

Obecna implementacja wykorzystuje „uniwersalne rozwiązanie” tolerancji wynoszącej 0,0001. Poniższy przykład pokazuje zastosowanie powyższej tolerancji.

func.func @check_tolerance() {
  %0 = stablehlo.constant dense<0.2> : tensor<f32>

  // The following check succeeds as %0 is almost equal to the provided
  // constant modulo the tolerance, mentioned above.
  check.expect_almost_eq_const %0, dense<0.19999> : tensor<f32>

  // The following check fails as %0 is not bitwise equal to the provided
  // constant.
  check.expect_eq_const %0, dense<0.19999> : tensor<f32>

  func.return
}

To tylko pierwszy krok na drodze do testowania dokładności liczbowej operacji StableHLO. W tej chwili jest to niedostatecznie określony obszar specyfikacji StableHLO. trwają prace nad rozwiązaniem tego problemu #1156 na podstawie naszego doświadczenia w praktyce przy korzystaniu ze StableHLO oraz opinii zainteresowanym. W miarę postępów będziemy aktualizować infrastrukturę odpowiednio się zmienia.

(G7) Czy masz jakieś informacje na temat kodowania testów?

  1. Pamiętaj, by używać rzeczywistej nazwy wejścia/wyjścia, zamiast domyślnego ustawienia na wartości SSA (np. %0, %1 itp.)
  2. Sprawdź, czy testy mają format „ładny druk” (jeśli taki istnieje).

(G8) Czy mamy uwzględnić przykład podany już w specyfikacji? Tak (aby sprawdzić kompletność testów).