Гайд · TNWS AI

Как скомпилировать шаг обучения через jax.jit

4 мин—

JIT-компиляция функционального train step: value_and_grad внутри jit, неизменяемый PyTree, совпадение результатов и блокировка для измерения.

Задача и применимость

Актуальный запрос: «как использовать jax jit для train step нейросети». Скомпилировать чистый шаг обучения XLA и проверить, что он возвращает те же значения, что обычная JAX-функция. jax.jit кэширует скомпилированный вариант для совместимых форм, dtype и статических аргументов. Первый вызов включает компиляцию; сравнивать его напрямую с прогретым временем неверно.

Материал применим к JAX 0.11.2 и jaxlib 0.11.2. JAX предоставляет массивы, автоматическое дифференцирование и программные преобразования; он не является готовой библиотекой слоёв, датасетов или полного тренировочного цикла. Пример намеренно мал и выполняется локально на доступном backend без загрузки весов и внешних данных. Численный критерий доказывает поведение конкретного кода, но не качество рабочей нейросети.

Пошаговое решение

  1. Опишите loss и функциональный step без побочных эффектов.
  2. Оберните step через jax.jit.
  3. Выполните обычный и compiled шаг на одинаковых параметрах.
  4. Сравните loss и каждый leaf результата, затем прогрейте перед benchmark.

Готовый проверяемый шаблон

import jax
import jax.numpy as jnp

X=jnp.array([[1.,0.],[0.,1.],[1.,1.]]); y=jnp.array([2.,-1.,1.])
params={"w":jnp.zeros((2,)),"b":jnp.array(0.)}
def loss_fn(p, xb, yb): return jnp.mean((xb@p["w"]+p["b"]-yb)**2)
def step(p, xb, yb, lr=0.1):
    value, grads=jax.value_and_grad(loss_fn)(p,xb,yb)
    return jax.tree.map(lambda a,g:a-lr*g,p,grads), value

compiled=jax.jit(step)
plain_p, plain_loss=step(params,X,y)
jit_p, jit_loss=compiled(params,X,y)
jax.block_until_ready(jit_loss)
assert jnp.allclose(plain_loss,jit_loss)
for a,b in zip(jax.tree.leaves(plain_p),jax.tree.leaves(jit_p)):
    assert jnp.allclose(a,b)
print(float(jit_loss), jit_p)

Вход и ожидаемый результат

Обычный и jitted step получают одни параметры и batch. Их loss и обновлённые листья совпадают численно. block_until_ready гарантирует завершение вычисления перед измерением или выводом.

Как проверить результат

Разделяйте compile time и steady-state latency: сделайте прогрев, затем много повторов с одинаковыми shape. Изменение batch shape может вызвать новую компиляцию. Сначала доказать эквивалентность, затем измерять.

Перенос на настоящую нейросеть

Для рабочей модели сначала зафиксируйте контракт параметров и batch: pytree structure, формы, dtype, порядок признаков и семантику выходов. Генерацию случайностей оформите как явный поток keys. Разделите чистые функции forward, loss и update, чтобы их можно было независимо тестировать, дифференцировать и компилировать. Данные train, validation и test не должны смешиваться. Любое изменение формы, precision, backend или версии JAX проверяйте на сохранённом обезличенном regression-наборе до развёртывания.

Критерии приёмки

  1. Все формы и dtype совпадают с контрактом.
  2. Каждый PRNG subkey используется в одной логической случайной операции.
  3. Loss и gradients конечны на положительном тесте.
  4. Негативный вход вызывает ожидаемую ошибку, а не случайный output.
  5. Обычный и преобразованный через jit/vmap путь численно согласованы, когда это применимо.
  6. Устройство, версии JAX/jaxlib и численный допуск записаны в отчёте.

Регрессионный протокол

Храните несколько малых входов: стандартный batch, один объект, нули, граничные значения и заведомо невалидную форму. Сравнивайте outputs и loss с эталоном через allclose с явно выбранным допуском. После смены backend проверяйте допустимую численную погрешность, а не требуйте безусловной битовой идентичности. Производительность измеряйте после компиляционного прогрева и завершения асинхронной работы. Не выбирайте лучший единичный замер: сообщайте распределение задержки и условия машины.

Диагностика расхождений

Если результат изменился, локализуйте слой: данные и preprocessing, key-поток, shapes/dtype, forward, loss, gradients, update, затем jit и backend. Сначала выполните функцию без jit на минимальном входе, затем верните transformations по одной. Сравните tree structure и каждый leaf. Не увеличивайте допуск и не применяйте nan_to_num, пока не выяснено, не скрывает ли это неверную форму, переполнение или повтор ключа.

Контроль после внедрения

В эксплуатации ограничьте допустимые batch shapes и объём входа, чтобы запрос не исчерпал память и не создал бесконечный поток перекомпиляций. Собирайте агрегированные метрики latency, compile count, долю невалидных входов и численные ошибки без записи исходных чувствительных данных. Сохраняйте предыдущий проверенный артефакт и код параметров для отката. Автоматическая замена весов требует той же независимой проверки, что и обычный выпуск.

Границы применимости

Пример на CPU не доказывает свойства GPU или TPU, а маленький массив не предсказывает производительность большой модели. JIT может ускорить повторяющиеся вычисления, но стоимость компиляции, передача данных и новые формы способны ухудшить end-to-end время. Autodiff возвращает производную написанной функции, а не подтверждает правильность математической постановки. Для критичных решений нужны предметные метрики, мониторинг и человеческий контроль.

Типичные ошибки и чего не делать

  • Не помещайте print Python внутрь jit для обычной диагностики значений.
  • Не измеряйте только первый вызов.
  • Не меняйте формы batch в каждом запросе без оценки числа компиляций.

Не публикуйте только удачный вывод без негативного теста и версий среды. Не используйте тестовую выборку для настройки модели. Не загружайте непроверенные параметры и не передавайте в диагностику персональные данные. Для важных решений предусмотрите безопасный отказ.

FAQ

Что компилирует jit?

JAX-функцию для конкретной комбинации абстрактных входов и статических параметров.

Почему нужен block_until_ready?

Вычисления могут быть асинхронными.

Первый вызов быстрее?

Обычно нет: он включает компиляцию.

Официальные источники

Частые вопросы

Что компилирует jit?

JAX-функцию для конкретной комбинации абстрактных входов и статических параметров.

Почему нужен block_until_ready?

Вычисления могут быть асинхронными.

Первый вызов быстрее?

Обычно нет: он включает компиляцию.

Читайте также

Комментарии

Пока тихо. Скажите первое слово