Defeating Nondeterminism in LLM Inference
Почему LLM неповторяемы и как это исправить
Проблема
Даже при temperature=0 и одном железе выводы моделей различаются от запуска к запуску. Популярное объяснение: «параллельные GPU-ядра + погрешности float = недетерминизм». Это не вся правда.
Что на самом деле происходит
- Все «математические» ядра (matmul, softmax и т.д.) внутри одного forward-прохода детерминированы — бит-в-бит.
- Недетерминизм появляется между forward-проходами:
- динамическое разбиение работы на потоки (different thread blocks);
- неупорядоченные редукции при вычислении
softmax/layernorm; - разные стратегии
cudnn/cublasв зависимости от загрузки GPU; - кэш-промахи и
atomicAddв attention.
Как убедиться
A = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16)
B = torch.randn(2048, 2048, device='cuda', dtype=torch.bfloat16)
ref = A @ B
for _ in range(1000):
assert (A @ B == ref).all() # всегда True
Матричное умножение повторяется, а вот softmax(A @ B) — уже нет.
Побеждаем за 3 шага
-
Фиксируем редукции
torch.use_deterministic_algorithms(True)CUBLAS_WORKSPACE_CONFIG=:4096:8(для CUDA ≥10.2)export CUDA_LAUNCH_BLOCKING=1(медленно, но зато стабильно).
-
Отключаем динамические алгоритмы
torch.backends.cudnn.deterministic = Truetorch.backends.cudnn.benchmark = False- в vLLM:
--disable-custom-all-reduce,--enforce-eager.
-
Контролируем параллелизм
- фиксированный батч и длина последовательности;
- один GPU-поток (
tensor_parallel_size=1); - один и тот же порядок запросов (queuing seed).
Результат
На Llama-3-8B с vLLM + указанными флагами 1000 прогонов дают идентичные токены вплоть до последнего бита. Стоимость: ≈8 % к throughput.
TL;DR
Недетерминизм — не «float плавает», а race-conditions вне математического ядра. Убери их, и LLM станет строго воспроизводимым.