Привет, Хабр!
Много лет можно наблюдать один и тот же ритуал: человек берёт фиксированный seed, торжественно записывает его в три места, запускает обучение и искренне ожидает, что всё будет повторяться до бита. А потом accuracy скачет на третьем знаке, лосс уплывает и приходит вопрос: «Почему не детерминируется?» А потому что детерминизм в ML это не один флажок. Это сумма десятка мелких факторов, от выбора алгоритма в cuDNN до порядка файлов в каталоге.
Что считаем детерминизмом и почему он ускользает
Есть повторяемость метрик в среднем при разных перезапусках, этого часто достаточно для продукта. А есть строгий детерминизм: бит‑в-бит одинаковые веса и предсказания на одинаковом железе и софте. Второе сложнее и дороже.
Сильные источники недетерминизма, которые чаще всего пропускают:
Выбор алгоритма в cuDNN и cuBLAS. Даже при одинаковых размерах тензоров бенчмаркер может выбрать разный путь. Нужны явные флаги.
Операции без детерминированной реализации. Пример: часть редукций, некоторые backward для пулов/индексаций. Нужен общий режим только детерминированные алгоритмы.
Параллельная загрузка данных. Рабочие потоки и внешние библиотеки имеют свои генераторы случайных чисел. Их надо засеять синхронно и стабильно.
Хеш‑рандомизация Python. Порядок в dict/set, а заодно и псевдослучайные разбиения, зависят от переменной окружения.
Порядок файлов в каталоге. os.listdir не гарантирует сортировку. Сортируйте явно.
Разное железо и версии библиотек. Даже при всем по фэншую PyTorch предупреждает: абсолютная повторяемость не обещается между платформами и релизами.
PyTorch: чеклист, который действительно закрывает дыры
Начнём с окружения. Эти переменные нужно выставлять до старта процесса.
# CUDA/cuBLAS: фиксируем workspace, иначе часть CUDA-операций будет недетерминирована
export CUBLAS_WORKSPACE_CONFIG=":4096:8" # или ":16:8" при малой памяти
# Хеши Python: фиксируем порядок в dict/set и всё, что на нём косвенно завязано
export PYTHONHASHSEED="0"
# Потоки BLAS/OpenMP: меньше гонок, проще повторяемость
export OMP_NUM_THREADS="1"
export MKL_NUM_THREADS="1"
При CUDA 10.2+ часть операций остаётся недетерминированной, если не выставлен CUBLAS_WORKSPACE_CONFIG.
Теперь код инициализации. Это минимальный, но практичный бутстрап для проекта.
# file: determinism.py
from __future__ import annotations
import os, random, warnings
import numpy as np
def set_seed(seed: int) -> None:
# Базовые генераторы
random.seed(seed)
np.random.seed(seed)
try:
import torch
torch.manual_seed(seed)
# Один флажок включает глобальный режим детерминизма в PyTorch
torch.use_deterministic_algorithms(True)
# cuDNN: без бенчмаркинга и только детерминированные алгоритмы
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
# Для безопасности: заполнять неинициализированную память известными значениями
from torch.utils import deterministic as tdet
tdet.fill_uninitialized_memory = True
except Exception as e:
warnings.warn(f"PyTorch determinism is partially configured: {e}")
def dataloader_seed_worker(worker_id: int) -> None:
# Важно: синхронизируем Python и NumPy в воркере от базового состояния PyTorch
import torch
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
def make_generator(seed: int):
import torch
g = torch.Generator()
g.manual_seed(seed)
return g
torch.use_deterministic_algorithms(True)
не только включает детерминированные альтернативы, но и уронит рантайм, если операция физически не имеет такой реализации.
torch.utils.deterministic.fill_uninitialized_memory
по умолчанию True при включенном deterministic‑режиме и закрывает класс багов, когда в граф случайно попадает мусор из неинициализированных тензоров.
Для cuBLAS нужна переменная окружения CUBLAS_WORKSPACE_CONFIG. Иначе даже при deterministic‑режиме споткнётесь на некоторых GEMM/редукциях.
Применение в тренировочном скрипте:
# train.py
import os
os.environ.setdefault("CUBLAS_WORKSPACE_CONFIG", ":4096:8")
os.environ.setdefault("PYTHONHASHSEED", "0")
from determinism import set_seed, dataloader_seed_worker, make_generator
set_seed(2025)
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
tfm = transforms.Compose([
transforms.ToTensor(), # детерминированно
# любые рандомные аугментации либо отключаем, либо снабжаем генератором
])
train = datasets.MNIST("./data", train=True, download=True, transform=tfm)
g = make_generator(2025)
loader = DataLoader(
train,
batch_size=256,
shuffle=True,
num_workers=4, # параллелим, но с правильной инициализацией
worker_init_fn=dataloader_seed_worker,
generator=g,
persistent_workers=False, # для простоты детерминизма
prefetch_factor=2, # оставляем по умолчанию
)
model = nn.Sequential(
nn.Flatten(),
nn.Linear(28*28, 256),
nn.ReLU(),
nn.Linear(256, 10),
).cuda()
opt = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
loss_fn = nn.CrossEntropyLoss()
model.train()
for xb, yb in loader:
xb, yb = xb.cuda(non_blocking=True), yb.cuda(non_blocking=True)
opt.zero_grad(set_to_none=True)
logits = model(xb)
loss = loss_fn(logits, yb)
loss.backward()
opt.step()
Отключили бенчмаркинг cuDNN и включили детерминированные алгоритмы, задали CUBLAS_WORKSPACE_CONFIG до старта процесса.
Три нюанса, из‑за которых часто дрейфует:
Полудетализация через float16 может давать микросдвиги из‑за неодинакового порядка округлений. Для строгого детерминизма используем float32 и одинаковые компиляционные пути.
Порядок чтения файлов. os.listdir и os.walk не гарантируют порядок.
Разные версии cudnn/cublas. Даже inference может немного расходиться на разных билдах библиотеки.
Сохранение и восстановление RNG-состояний из чекпоинта
Если нужно ровно продолжить тренировку с того же шага с теми же dropout‑масками и той же очередью данных, сохраняйте состояния генераторов:
# checkpointing_rng.py
import torch, random, numpy as np
def pack_rng_state():
state = {
"python": random.getstate(),
"numpy": np.random.get_state(),
"torch_cpu": torch.get_rng_state(),
"torch_cuda_all": torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None,
}
return state
def unpack_rng_state(state):
random.setstate(state["python"])
np.random.set_state(state["numpy"])
torch.set_rng_state(state["torch_cpu"])
if state["torch_cuda_all"] is not None:
torch.cuda.set_rng_state_all(state["torch_cuda_all"])
TensorFlow: флаги и реальные границы
У TensorFlow давно появился прямой флажок на детерминизм операций. Его надо включать в коде в самом начале:
import os
os.environ.setdefault("TF_DETERMINISTIC_OPS", "1") # старый путь через env
import tensorflow as tf
tf.config.experimental.enable_op_determinism() # современный API
import numpy as np, random
seed = 2025
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)
Включение determinism у TF приводит к выбору детерминированных реализаций, но с ожидаемым падением производительности. Если вы держите legacy‑окружение, его иногда приходится выставлять.
JAX: ключи, split и XLA
В JAX генерация случайных чисел изначально сделана прозрачной: вы всегда передаёте PRNG‑key и сплитите его в местах, где нужна случайность. Пример, который не ломается при переносе на многоядерные машины:
import jax
import jax.numpy as jnp
from jax import random, jit
key = random.key(2025)
@jit
def step(key, x):
key, sub = random.split(key)
w = random.normal(sub, shape=(x.shape[-1], 128))
y = x @ w
return key, jnp.tanh(y).sum()
x = jnp.ones((256, 1024), dtype=jnp.float32)
for _ in range(100):
key, s = step(key, x)
Детерминированность генерации у JAX хорошая, но сам рантайм XLA может выбирать недетерминированные реализации для некоторых операций. На GPU у XLA есть флаг, вырезающий недетерминированные опции компилятора, и тогда часть графа компилироваться не будет, если детерминированной альтернативы нет.
Распределённое обучение: где именно гуляет результат
Даже при идеальном посеве и отключённом AMP результат может немного плыть в DDP/TPU‑конфигурациях. Основные причины:
Разный порядок суммирования при AllReduce. Сложение чисел с плавающей точкой не ассоциативно, и разные деревья редукции дают микроразброс.
Алгоритмы и транспорт NCCL. Между версиями и топологиями возможны разные пути редукции. Нельзя требовать от этого битовой идентичности на любых кластерах.
Что помогает:
Не смешивать DDP‑хуки для градиентов, пока вы добиваетесь строго детерминированной базы. Они меняют порядок и семантику редукций.
Фиксировать версии PyTorch, CUDA, cuDNN, NCCL, драйверов. И фиксировать топологию кластера, если вам нужна строгая проверка регрессий.
Тест, который ловит расхождения рано
Дешёвый тест на детерминизм: вычисляете хеш на фиксированном батче до и после backward для одинакового seed. Если хеш меняется между перезапусками — ищем источник.
# file: smoke_determinism.py
import hashlib, torch
from determinism import set_seed
def tensor_sha256(t: torch.Tensor) -> str:
x = t.detach().cpu().contiguous().numpy().tobytes()
return hashlib.sha256(x).hexdigest()
def run_once(seed=2025) -> tuple[str, str]:
set_seed(seed)
x = torch.randn(64, 128, device="cuda")
w = torch.randn(128, 128, requires_grad=True, device="cuda")
y = x @ w
before = tensor_sha256(y)
y.sum().backward()
after = tensor_sha256(w.grad)
return before, after
if __name__ == "__main__":
b1, a1 = run_once()
print("forward:", b1)
print("backward:", a1)
Если эти строки совпадают между перезапусками на одной и той же версии софта и железа — базовые настройки у вас корректны. Если нет — включайте torch.use_deterministic_algorithms(True) и читайте исключения от конкретной операции.
Итог
Даже идеально настроенный проект не обещает битовую идентичность на другом драйвере или свежей сборке фреймворка. Поэтому к детерминизму относитесь как к режиму: включили, локализовали, задокументировали, а потом осознанно вернули быстрые пути.
Как видно, абсолютного детерминизма в машинном обучении добиться непросто: слишком много факторов вносят элемент случайности, начиная от библиотек и заканчивая железом. Но именно поэтому важно уметь разбираться в деталях работы алгоритмов и понимать, какие методы реально позволяют контролировать процесс. Записывайтесь на бесплатные уроки:
8 сентября в 18:00 — «Методы ансамблирования в ML, которые должен знать любой Data Scientist»
18 сентября в 18:00 — «Data Science — это проще, чем кажется!»
23 сентября в 20:00 — «Алгоритм классификации KNN — определим класс объекта по ближайшим соседям»
А если вы хотите системно и с нуля освоить ML, обратите внимание на Специализацию Machine Learning — она поможет перейти от стандартных аналитических подходов к сложным ML-алгоритмам для бизнес-прогнозирования.
Комментарии (3)
aeder
06.09.2025 02:50Извините за вопрос - а ЗАЧЕМ нужен абсолютный детерменизм в машинном обучении?
Возможно, имеет некоторый смысл при тонкой оптимизации/отладки собственно алгоритмов разработчиками библиотек МЛ - но для обычных целей - зачем?
imageman
06.09.2025 02:50думаю в основном в целях отладки, изредка - убедиться, что твоя сборка не содержит ошибок (т.е. даёт результат "как у того чела из Бразилии").
Для тонкой оптимизации я бы тоже не брал - жадный поиск не всегда даст оптимальный вариант (а жадный он потому что мы отключили разные оптимизации, которые потом будут включены).
BenderA
Ничего не понятно, но очень интересно.