Почему LLM выдаёт разные результаты при каждом запуске
Пять одинаковых запросов — пять разных ответов: один запуск ответил «не Python», четыре сказали «Python». Закрепили seed — и батч перевернул токен при temp=0.
Рис. 1 — пять запусков, пять ответов, одно самоопровержение. Запуск 2 (тусклый) — та квитанция: тот же файл, та же машина, тот же промпт — противоположное начало.
Почему LLM отвечает по-разному при каждом запуске?
У модели никогда нет «готового» ответа. Для каждой токен-позиции она держит список вероятностей, temperature меняет его форму, и один токен выигрывает розыгрыш. Победитель дописывается, и следующий аукцион открывается с другим списком — ведь текст, который она продолжает, уже изменился. Одно раннее расхождение растёт снежным комом: запуск 2 сказал «not» там, где запуски 1, 3, 4 и 5 сказали «Python», — и от этого единственного символа пять ответов живут в разных вселенных. Пять запусков — пять независимых розыгрышей, а не один ответ, напечатанный пять раз.
Как сделать так, чтобы ответы LLM повторялись в точности?
Дайте броску имя. Мы повторили идентичный запрос трижды с seed: 42 при temperature 0.7 — заведомо недетерминированной настройке — и получили 3/3 идентичных до байта ответа (скорость генерации гуляла 6.18 → 6.74 → 7.63 tok/s; слова не гуляли вовсе). Seed фиксирует кости, даже пока их бросают.
curl http://localhost:8080/v1/chat/completions -d '{
"model": "qwen3:4b",
"messages": [{"role": "user", "content": "Explain GGUF quantization"}],
"temperature": 0.7,
"seed": 42
}'
проверка: тот же промпт + тот же seed → ответ байт в байт, 3/3 на стенде (temp 0.7, не только жадный режим) · det-results-a.json
Та же ручка есть везде, где вы уже работаете: options.seed в /api/generate у ollama, seed в OpenAI-совместимом чатовом API, --seed в командной строке llama.cpp. Раннер ollama — это llama-server в пиджаке, так что квитанция под капотом та же — seed доходит до того же сэмплера.
Почему temperature 0 всё равно не детерминированность?
Потому что жадное декодирование убирает кости. Оно не убирает округление. Внутри сети счёт каждого токена — тысячи сложений, а сложение чисел с плавающей точкой не ассоциативно: сложите длинный столбец слева направо, потом кусками — те же числа, возможно другая последняя цифра. Когда два верхних кандидата сидят достаточно близко, округление и есть решение. А порядок суммирования зависит от того, сколько работы батч делает за раз.
Поэтому мы зафиксировали всё — temp=0, seed=42, тот же сервер, тот же промпт — и изменили ровно один флаг: ширину микробатча.
| запрос | -ub | temp | seed | начало ответа | против -ub 512 |
|---|---|---|---|---|---|
| 1–2 | 512 | 0 | 42 | “ Python. It is used in many…” | базовая линия, 2/2 идентичны |
| 1 | 8192 | 0 | 42 | “ Python. It’s used in many…” | ПЕРЕВЕРНУЛОСЬ — один токен |
| 2–3 | 8192 | 0 | 42 | “ Python. It is used in many…” | вернулось к базе, 2/2 базовой линии |
Кость с грузиком вживую: каждая ручка говорила «детерминированно», а форма батча всё равно сдвинула один токен — и только на первом запросе после старта.
Прочитайте третью строку дважды. Переворот случился однажды, на первом запросе холодного сервера, а два следующих запроса с теми же настройками вернули базовый текст. Состояние тёплого сервера — кэш промптов, раскладка слотов — встаёт в один ряд с порядком суммирования: тоже то, от чего зависит «детерминированный» ответ. Переворот мы поймали; притворяться, что поймали виновника, не будем. Кости загружены. Рука остаётся скрытой.
Во что обходится разброс от запуска к запуску?
Счёт, оплаченный пять раз. Мы просили «hello» ровно в трёх словах, пять раз, свободный seed, бюджет 256 токенов. Модель ответила 0 из 5 — каждый запуск потратил весь бюджет на размышления, прежде чем лимит его оборвал, и сами размышления гуляли: 884 · 954 · 986 · 915 · 885 символов на один и тот же запрос.
Рис. 2 — разброс и есть суть: разница в 102 символа на фиксированный запрос. Счёт за размышления из октябрьского поста про LFM3, пять раз подряд.
Мы просили «hello» в трёх словах — получили от 884 до 986 символов душевных исканий и ни одного «hello». Модель нерешительна не в ответе — она нерешительна во всём, что до ответа, и вы платите за это независимо от того, пришёл ответ или нет. Любой запрос «точного ответа» — это бюджет: ограничивайте размышления (think: false или собственный выключатель модели), иначе лимит съест вас первым.
Список исправлений
| симптом | что происходит | исправление |
|---|---|---|
| разный ответ на каждом запуске | нет seed; temp > 0 меняет список при каждом розыгрыше | фиксируйте seed; ответ повторится байт в байт |
| разные ответы при temp=0 | форма батча / состояние тёплого сервера изменили порядок суммирования | тот же -ub/-b, та же сборка движка, прогрейте перед измерением |
| ответ так и не приходит | размышления съели токен-бюджет (здесь 0/5) | ограничьте или отключите размышления; смотрите completion_tokens, а не вайбы |
run 1: “ Python. Python is widely used in data science, web…”
run 2: “not Python, Java, or C++. It’s actually a language that’s
not even a progr…”
run 3: “ Python. But there are other languages that are…”
run 4: “ Python. But why? Python is a high-level, interpreted…”
run 5: “ Python. Python has a lot of applications in web…”
Четыре запуска сказали Python. Один начал с оспаривания самой постановки вопроса. Те же веса, та же машина, та же строка на входе. Один из пяти ошибается, считая себя детерминированной машиной; остальным четырём просто повезло.
последняя проверка 2026-11-11
Qwen3-4B Q4_K_M · 2,497,280,256 B · sha256 7485fe6f…df5 · llama-server 0.9.0-dev (b12110) · одна свежая VM Arch, 4 потока — CPU-бенч, никаких заявлений про VRAM