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 indeksieindex
jakoElement
obiektu.void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);
: Aby zaktualizować obiektElement
element
w tensor w wielowymiarowym indeksindex
.
Jak działa tłumacz
Funkcja wprowadzania danych do interpretera to
SmallVector<Tensor> eval(func::FuncOp func, ArrayRef<Tensor> args);
który:
- Śledzi argumenty SSA usługi
func
i powiązane z nią środowisko wykonawczeTensor
podane w komórceargs
za pomocą mapy tabeli symboli, M. - 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łaniueval
. - Śledzi wyniki SSA operacji i ocenioną wartość w M.
- Wywołuje op.
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ę:
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 kampaniiadd
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.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 operacjiadd
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 APIllvm::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?
- Pamiętaj, by używać rzeczywistej nazwy wejścia/wyjścia, zamiast domyślnego ustawienia na wartości SSA (np. %0, %1 itp.)
- 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).