Что такое детерминизм и как с ним бороться?. cuDNN детерминизм.. cuDNN детерминизм. ml.. cuDNN детерминизм. ml. python.. cuDNN детерминизм. ml. python. pythonhashseed.. cuDNN детерминизм. ml. python. pythonhashseed. PyTorch deterministic mode.. cuDNN детерминизм. ml. python. pythonhashseed. PyTorch deterministic mode. random seed.. cuDNN детерминизм. ml. python. pythonhashseed. PyTorch deterministic mode. random seed. reproducibility.. cuDNN детерминизм. ml. python. pythonhashseed. PyTorch deterministic mode. random seed. reproducibility. детерминизм в ML.. cuDNN детерминизм. ml. python. pythonhashseed. PyTorch deterministic mode. random seed. reproducibility. детерминизм в ML. повторяемость экспериментов.. cuDNN детерминизм. ml. python. pythonhashseed. PyTorch deterministic mode. random seed. reproducibility. детерминизм в ML. повторяемость экспериментов. фиксированный seed.

Много лет можно наблюдать один и тот же ритуал: человек берёт фиксированный 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) и читайте исключения от конкретной операции.


Итог

Даже идеально настроенный проект не обещает битовую идентичность на другом драйвере или свежей сборке фреймворка. Поэтому к детерминизму относитесь как к режиму: включили, локализовали, задокументировали, а потом осознанно вернули быстрые пути.

Как видно, абсолютного детерминизма в машинном обучении добиться непросто: слишком много факторов вносят элемент случайности, начиная от библиотек и заканчивая железом. Но именно поэтому важно уметь разбираться в деталях работы алгоритмов и понимать, какие методы реально позволяют контролировать процесс. Записывайтесь на бесплатные уроки:

А если вы хотите системно и с нуля освоить ML, обратите внимание на Специализацию Machine Learning — она поможет перейти от стандартных аналитических подходов к сложным ML-алгоритмам для бизнес-прогнозирования.

Автор: badcasedaily1

Источник

Rambler's Top100