Описать расчет Tensor
довольно просто, но когда и как выполняется этот расчет, будет зависеть от того, какой бэкэнд используется для Tensor
и когда результаты необходимы на главном процессоре.
За кулисами операции над Tensor
отправляются на ускорители, такие как графические процессоры или TPU , или выполняются на ЦП, когда ускоритель недоступен. Это происходит автоматически и позволяет легко выполнять сложные параллельные вычисления с использованием высокоуровневого интерфейса. Однако может быть полезно понять, как происходит эта диспетчеризация, и иметь возможность настроить ее для оптимальной производительности.
Swift для TensorFlow имеет два бэкэнда для выполнения ускоренных вычислений: режим TensorFlow и X10. Бэкэнд по умолчанию — это режим готовности TensorFlow, но его можно переопределить. Доступно интерактивное руководство , которое поможет вам использовать эти различные серверные части.
Режим нетерпеливости TensorFlow
Серверная часть режима готовности TensorFlow использует API TensorFlow C для отправки каждой операции Tensor
на графический процессор или процессор по мере ее возникновения. Результат этой операции затем извлекается и передается следующей операции.
Эта диспетчеризация операций за операцией проста для понимания и не требует явной настройки в вашем коде. Однако во многих случаях это не приводит к оптимальной производительности из-за накладных расходов на отправку множества мелких операций в сочетании с отсутствием объединения операций и оптимизации, которые могут возникнуть при наличии графов операций. Наконец, режим готовности TensorFlow несовместим с TPU и может использоваться только с центральными и графическими процессорами.
X10 (трассировка на основе XLA)
X10 — это название бэкэнда Swift для TensorFlow, который использует отложенную тензорную трассировку и оптимизирующий компилятор XLA , чтобы во многих случаях значительно повысить производительность по сравнению с диспетчеризацией операций за операцией. Кроме того, добавлена совместимость с TPU — ускорителями, специально оптимизированными для тех видов вычислений, которые используются в моделях машинного обучения.
Использование X10 для вычислений Tensor
не используется по умолчанию, поэтому вам необходимо выбрать этот бэкэнд. Это делается путем указания того, что Tensor
размещен на устройстве 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)
После этого описание вычислений точно такое же, как и для режима нетерпеливости TensorFlow:
let tensor3 = tensor1 + tensor2
Более подробную информацию можно предоставить при создании Tensor
, например, какой тип ускорителя использовать и даже какой, если доступно несколько. Например, Tensor
можно создать на втором устройстве TPU (при условии, что он виден хосту, на котором запущена программа), используя следующее:
let tpuTensor = Tensor<Float>([0.0, 1.0, 2.0], on: Device(kind: .TPU, ordinal: 1, backend: .XLA))
Неявное перемещение Tensor
между устройствами не выполняется, поэтому, если два Tensor
на разных устройствах используются в совместной операции, произойдет ошибка во время выполнения. Чтобы вручную скопировать содержимое Tensor
на новое устройство, вы можете использовать инициализатор Tensor(copying:to:)
. Некоторые крупномасштабные структуры, содержащие внутри себя Tensor
, такие как модели и оптимизаторы, имеют вспомогательные функции для перемещения всех своих внутренних Tensor
на новое устройство за один шаг.
В отличие от режима готовности TensorFlow, операции с использованием бэкэнда X10 не отправляются индивидуально по мере их возникновения. Вместо этого отправка в ускоритель запускается только при чтении вычисленных значений обратно на хост или при установке явного барьера. Это работает следующим образом: среда выполнения начинается со значения, считываемого хосту (или последнего вычисления перед ручным барьером), и отслеживает график вычислений, в результате которых получается это значение.
Этот отслеживаемый граф затем преобразуется в промежуточное представление XLA HLO и передается компилятору XLA для оптимизации и компиляции для выполнения в ускорителе. Оттуда весь расчет отправляется в ускоритель и получается конечный результат.
Вычисления — трудоемкий процесс, поэтому X10 лучше всего использовать для массовых параллельных вычислений, которые выражаются в виде графика и выполняются много раз. Хэш-значения и кэширование используются для того, чтобы идентичные графики компилировались только один раз для каждой уникальной конфигурации.
Для моделей машинного обучения процесс обучения часто включает в себя цикл, в котором модель снова и снова подвергается одной и той же серии вычислений. Вам нужно, чтобы каждый из этих проходов рассматривался как повторение одной и той же трассы, а не как один длинный график с повторяющимися единицами внутри него. Это становится возможным благодаря ручной вставке вызова функции LazyTensorBarrier()
в тех местах вашего кода, где вы хотите, чтобы трассировка заканчивалась.
Поддержка смешанной точности в X10
Поддерживается обучение со смешанной точностью через X10, и для его управления предоставляются как низкоуровневые, так и высокоуровневые API. Низкоуровневый API предлагает два вычисляемых свойства: toReducedPrecision
и toFullPrecision
, которые преобразуют полную и уменьшенную точность, а также isReducedPrecision
для запроса точности. Помимо Tensor
, с помощью этого API можно конвертировать модели и оптимизаторы между полной и пониженной точностью.
Обратите внимание, что преобразование к пониженной точности не меняет логический тип Tensor
. Если t
является Tensor<Float>
, t.toReducedPrecision
также является Tensor<Float>
с базовым представлением пониженной точности.
Как и в случае с устройствами, операции между тензорами разной точности не допускаются. Это позволяет избежать молчаливого и нежелательного перехода к 32-битным числам с плавающей запятой, которые пользователю было бы трудно обнаружить.