Opisanie obliczeń Tensor
jest dość proste, ale kiedy i jak te obliczenia zostaną przeprowadzone, będzie zależeć od tego, który backend jest używany dla Tensor
i kiedy wyniki będą potrzebne na procesorze hosta.
Za kulisami operacje na Tensor
są wysyłane do akceleratorów, takich jak procesory graficzne lub TPU , lub uruchamiane na procesorze, gdy nie jest dostępny akcelerator. Dzieje się to automatycznie i ułatwia wykonywanie złożonych obliczeń równoległych przy użyciu interfejsu wysokiego poziomu. Jednak przydatne może być zrozumienie, w jaki sposób następuje ta wysyłka i możliwość dostosowania jej w celu uzyskania optymalnej wydajności.
Swift dla TensorFlow ma dwa backendy do wykonywania przyspieszonych obliczeń: tryb chętny TensorFlow i X10. Domyślnym backendem jest tryb chętny TensorFlow, ale można go zastąpić. Dostępny jest interaktywny samouczek , który przeprowadzi Cię przez korzystanie z tych różnych backendów.
Tryb chętny TensorFlow
Zaplecze trybu chętnego TensorFlow wykorzystuje interfejs API TensorFlow C do wysyłania każdej operacji Tensor
do procesora graficznego lub procesora, gdy tylko zostanie ona napotkana. Wynik tej operacji jest następnie pobierany i przekazywany do następnej operacji.
To wysyłanie operacji po operacji jest łatwe do zrozumienia i nie wymaga jawnej konfiguracji w kodzie. Jednak w wielu przypadkach nie skutkuje to optymalną wydajnością ze względu na obciążenie związane z wysyłaniem wielu małych operacji w połączeniu z brakiem fuzji i optymalizacji operacji, które mogą wystąpić, gdy obecne są wykresy operacji. Wreszcie tryb chętny TensorFlow jest niekompatybilny z TPU i można go używać tylko z procesorami CPU i GPU.
X10 (śledzenie oparte na XLA)
X10 to nazwa backendu Swift dla TensorFlow, który wykorzystuje leniwe śledzenie tensora i kompilator optymalizujący XLA , aby w wielu przypadkach znacznie poprawić wydajność w porównaniu z wysyłaniem operacji po operacji. Dodatkowo dodaje kompatybilność z TPU , akceleratorami specjalnie zoptymalizowanymi pod kątem rodzajów obliczeń występujących w modelach uczenia maszynowego.
Użycie X10 do obliczeń Tensor
nie jest ustawieniem domyślnym, dlatego należy włączyć tę opcję. Odbywa się to poprzez określenie, że Tensor
jest umieszczony na urządzeniu XLA:
let tensor1 = Tensor<Float>([0.0, 1.0, 2.0], on: Device.defaultXLA)
let tensor2 = Tensor<Float>([1.5, 2.5, 3.5], on: Device.defaultXLA)
Po tym punkcie opisywanie obliczeń przebiega dokładnie tak samo, jak w przypadku trybu chętnego TensorFlow:
let tensor3 = tensor1 + tensor2
Dalsze szczegóły można podać podczas tworzenia Tensor
, na przykład jakiego rodzaju akceleratora użyć, a nawet jakiego, jeśli jest ich kilka. Na przykład Tensor
można utworzyć na drugim urządzeniu TPU (zakładając, że jest on widoczny dla hosta, na którym działa program), korzystając z następujących poleceń:
let tpuTensor = Tensor<Float>([0.0, 1.0, 2.0], on: Device(kind: .TPU, ordinal: 1, backend: .XLA))
Nie jest wykonywany żaden ukryty ruch Tensor
między urządzeniami, więc jeśli w operacji zostaną użyte razem dwa Tensor
na różnych urządzeniach, wystąpi błąd w czasie wykonywania. Aby ręcznie skopiować zawartość Tensor
na nowe urządzenie, możesz użyć inicjatora Tensor(copying:to:)
. Niektóre struktury o większej skali, zawierające w sobie elementy Tensor
, takie jak modele i optymalizatory, posiadają funkcje pomocnicze umożliwiające przeniesienie wszystkich wewnętrznych elementów Tensor
na nowe urządzenie w jednym kroku.
W przeciwieństwie do trybu chętnego TensorFlow, operacje korzystające z zaplecza X10 nie są wysyłane indywidualnie po ich napotkaniu. Zamiast tego wysłanie do akceleratora jest wyzwalane jedynie przez odczytanie obliczonych wartości z powrotem do hosta lub przez umieszczenie wyraźnej bariery. Działa to w ten sposób, że środowisko wykonawcze rozpoczyna się od wartości odczytanej hostowi (lub ostatniego obliczenia przed barierą ręczną) i śledzi wykres obliczeń, w wyniku których uzyskano tę wartość.
Ten prześledzony wykres jest następnie konwertowany do pośredniej reprezentacji XLA HLO i przekazywany do kompilatora XLA w celu optymalizacji i skompilowania do wykonania w akceleratorze. Stamtąd całe obliczenie jest przesyłane do akceleratora i uzyskiwany jest wynik końcowy.
Obliczenia są procesem czasochłonnym, dlatego X10 najlepiej stosować w przypadku masowo równoległych obliczeń, które są wyrażane za pomocą wykresu i które są wykonywane wielokrotnie. Stosowane są wartości skrótu i buforowanie, dzięki czemu identyczne wykresy są kompilowane tylko raz dla każdej unikalnej konfiguracji.
W przypadku modeli uczenia maszynowego proces uczenia często obejmuje pętlę, w której model jest poddawany w kółko tej samej serii obliczeń. Będziesz chciał, aby każde z tych przebiegów było postrzegane jako powtórzenie tego samego śladu, a nie jako jeden długi wykres z powtarzającymi się jednostkami w środku. Jest to możliwe poprzez ręczne wstawienie wywołania funkcji LazyTensorBarrier()
w miejscach w kodzie, w których ma się zakończyć śledzenie.
Obsługa mieszanej precyzji w X10
Obsługiwane jest szkolenie z mieszaną precyzją za pośrednictwem X10, a do jego kontrolowania dostępne są interfejsy API zarówno niskiego, jak i wysokiego poziomu. Interfejs API niskiego poziomu oferuje dwie obliczone właściwości: toReducedPrecision
i toFullPrecision
, które konwertują między pełną i zmniejszoną precyzją, wraz z isReducedPrecision
do sprawdzania precyzji. Oprócz Tensor
za pomocą tego interfejsu API można konwertować modele i optymalizatory między pełną i zmniejszoną precyzją.
Należy zauważyć, że konwersja na zmniejszoną precyzję nie zmienia typu logicznego Tensor
. Jeśli t
jest Tensor<Float>
, t.toReducedPrecision
jest również Tensor<Float>
z podstawową reprezentacją o zmniejszonej precyzji.
Podobnie jak w przypadku urządzeń, operacje pomiędzy tensorami o różnej precyzji nie są dozwolone. Pozwala to uniknąć cichej i niechcianej promocji do wersji 32-bitowej, która byłaby trudna do wykrycia przez użytkownika.