Temperature, top-p и детерминизм, которого у вас нет
Temperature делит logit перед softmax — и этим ломает идею «регулятора креативности». Даже greedy-вызов может дать два ответа.
На этой странице
Вот один и тот же запрос, отправленный одной и той же модели пять раз. Те же веса, тот же prompt, та же машина, тот же random seed. Меняется только одно число.
prompt: "Q: What is the capital of France?\nA:"
T = 0.0 " Paris\nWhat is the question and does the answer answer it? The
question is: What is the capital of France?..."
T = 0.7 " Paris\nWhat is the question: Which city is the capital of
France?..."
T = 1.0 " Paris\nWhat is a good geographical qualifier for describing
Paris concerning its location?\nA: Near the Mediterranean Sea..."
T = 1.5 " Paris\nWhat clue from premise allows we to conclude that Godwin
was &, He chose Healing Crimson Colour No:white flour Pure..."
T = 2.0 "安全感金华.ITEMT]]];\naims assume parental.st-importe.valtermination
Screens قطر_Zeroหมายเลข-zA ('$ספטמבר..."Ничего не сломалось. Каждый token в последней строке был честно выбран из собственной вероятностной распределённости модели по её словарю из 151 936 элементов. Число, которое менялось, называется temperature; в большинстве документации его описывают как регулятор креативности, и это описание ошибочно так, что эту ошибку можно не утверждать, а показать.
Это ещё и глава, где исполняются три ранних обещания. Глава 4 определила logit и почти не воспользовалась им. Блок о floating-point из главы 2 закончился инструкцией — вспомните об этом, когда глава 17 спросит, почему один и тот же prompt, модель и seed могут давать разные token. А блок о mixture-of-experts из главы 9 обещал каталог из четырёх причин недетерминизма. Все три появляются ниже.
Одна строка, на которой держится вся глава
Ссылка на раздел: Одна строка, на которой держится вся главаГлава 4 ввела logit как ненормированную вещественную оценку, по одной на класс. Глава 8 заставила языковую модель выдавать такую оценку для каждой позиции словаря. softmax превращает этот вектор в вероятности:
Temperature входит именно здесь — название заимствовано из статистической физики, где тот же параметр управляет тем, насколько резко распределение Больцмана концентрируется на состояниях с низкой энергией1, — и делит logits перед экспонентой:
Это расположение и есть весь механизм, и двух строк алгебры достаточно, чтобы увидеть, почему иначе быть не могло. Допустим, вы попытались бы применить temperature к вероятностям: умножить их на и заново нормировать. Получилось бы
Константа сокращается. Масштабирование вероятностей не делает вообще ничего; распределение возвращается неизменным. Temperature имеет эффект только потому, что действует на экспоненту: деление на перед возведением в экспоненту эквивалентно возведению каждой вероятности в степень — нелинейному преобразованию, которое меняет соотношения между элементами, а не их общий масштаб.
Из этого расположения сразу следуют оба предела. Когда , максимальный logit убегает от остальных, и схлопывается в один token с наивысшей оценкой: greedy decoding. Когда растёт, каждое стремится к нулю, каждая экспонента — к 1, и распределение выравнивается к равномерному по всему словарю. Ровно при формула делит на ноль, поэтому каждая реализация обрабатывает этот случай отдельно как арифметический максимум — включая виджет ниже, который при переключается на argmax.
Одно предупреждение: совпадение названий вызывает настоящую путаницу. В machine learning есть другая, несвязанная вещь, которая тоже называется temperature: temperature scaling, метод калибровки, подбирающий одно значение на валидационном наборе так, чтобы уверенность классификатора соответствовала его точности.2 Формула та же, к генерации отношения нет. Когда в статьях говорится «temperature», часто имеется в виду именно это; в этой главе — никогда.
Вот это распределение, с арифметикой перед глазами. logits фиксированы и правдоподобны, так что числа в тексте ниже можно сверить с тем, что вы видите:
Temperature — не регулятор креативности
Ссылка на раздел: Temperature — не регулятор креативностиЧисло ␣banana — весь аргумент в миниатюре: повышение temperature не может дать модели идею, которой у неё не было. logits уже посчитаны, ранжирование уже зафиксировано, и temperature сохраняет его в точности: никакой нагрев не поднимет ниже оценённый token выше выше оценённого. Всё, что она делает, — перераспределяет массу вниз по рангу, который сама модель и создала. Высокая temperature не делает модель изобретательнее; она повышает вероятность вывода token, которые модель оценила как плохие.
На реальном словаре это перестаёт быть любопытным фактом и становится причиной, по которой вывод с высокой temperature непригоден. Измерено на Qwen/Qwen2.5-0.5B-Instruct, один forward pass, prompt выше; считается, сколько token нужно, чтобы накопить заданную долю вероятностной массы:
| temperature | вероятность top-1 | entropy | token для 80 % | 90 % | 95 % | 99 % |
|---|---|---|---|---|---|---|
| 0.5 | 99,98 % | 0,00 nats | 1 | 1 | 1 | 1 |
| 0.7 | 99,65 % | 0,03 nats | 1 | 1 | 1 | 1 |
| 1.0 | 96,01 % | 0,30 nats | 1 | 1 | 1 | 14 |
| 1.2 | 88,20 % | 0,88 nats | 1 | 2 | 13 | 252 |
| 1.5 | 62,83 % | 3,07 nats | 29 | 353 | 2 672 | 26 787 |
| 2.0 | 16,62 % | 8,19 nats | 13 516 | 32 966 | 55 231 | 101 205 |
Прочитайте нижнюю строку медленно. При , на вопросе ровно с одним правильным ответом, 32 966 разных token делят верхние 90 % вероятностной массы. Это не более широкое творческое пространство. Это модель, которой арифметика велела считать корейскую частицу и идентификатор C++ живыми вариантами для слова после A:. Мусор в начальном блоке — прямое следствие, и это не баг модели или библиотеки: именно это запрос и попросил.
Полезный диапазон узок и зависит от задачи, а не от вкуса. В фактическом вопросе ответ — один token, и любой нагрев выше примерно 1.2 просто добавляет ошибку. В открытой задаче хороших продолжений действительно больше одного, и немного нагрева даёт разнообразие, остающееся гладким:
"Write a two-sentence story about a lighthouse."
T = 0.0 "The lighthouse stood tall and proud, its beacon illuminating the
night sky above. A lone sailor, his eyes fixed on the distant
horizon..."
T = 0.7 "In the quiet, stormy waters of the sea, a lighthouse stood
sentinel over the horizon, its golden dome casting a warm glow
on the fog-shrouded streets below..."
T = 1.0 "In the quiet night, a lone lighthouse stood sentinel over the
sea, its shining beacon a beacon of hope and solace for sailors
and fishermen across the vast and endless ocean..."
T = 1.3 "In the gentle sunlight, now reflecting upon the opening of Jack's
lighthouse, Jim Trahan, a small-time individual difficult to
define in paperwork, wondered about a career where simplicity
reigns..."При 1.3 модель придумала имя собственное и предложение, которое не разбирается грамматически. Полоса между «каждый раз одинаково» и «несвязно» для этой модели на этой задаче примерно от 0.6 до 1.1, и честный совет — найти её измерениями на вашей задаче, а не копировать число из blog post.
Почему самый вероятный текст — плохой текст
Ссылка на раздел: Почему самый вероятный текст — плохой текстПод всем этим прячется очевидный вопрос: если у модели есть вероятностное распределение и один token наиболее вероятен, почему не брать его всегда? Greedy decoding бесплатен, воспроизводим и не требует параметров.
Потому что результат получается таким:
prompt: "In a shocking finding, scientists discovered a herd of unicorns
living in a remote valley."
greedy: " The unicorns were so rare that they were not even recognized by
the local people. The unicorns were so rare that they were not
even recognized by the local people. The unicorns were so rare
that they were not even recognized by the local people. ..."
repeated 4-grams: 87.6 %Восемь предложений, одно предложение. Почти девять из десяти четырёх-token окон уже встречались раньше в том же выводе. Это neural text degeneration, названная и объяснённая Holtzman et al. в статье, где был представлен top-p.3 Модель не сломана; максимизация вероятности последовательности просто неверная цель для открытого текста. Человеческое письмо — не самая вероятная последовательность слов: в нём есть неожиданность, его вероятность на token блуждает, падает и восстанавливается, тогда как путь максимальной вероятности — фиксированная точка, в которую, попав, нет причин выходить.
Именно поэтому sampling вообще существует. Но есть часть, которую обычно опускают: это не универсальный закон. Глава 12 измерила 24 из 24 правильных ответов на двухшаговых текстовых задачах с обычным greedy decoding, а sampling при temperature 0.8 снизил результат до 81 %; self-consistency затем потратил в шесть раз больше token, чтобы вернуться туда, где greedy уже был. Оба факта верны одновременно:
Открытая генерация. Единственного правильного продолжения нет, поэтому самое вероятное — ловушка: оно зацикливается, и 87,6 % вывода копирует сам себя. Делайте sampling.
Задачи с одним правильным ответом. Единственное правильное продолжение есть, поэтому выбрать что-то ещё — значит выбрать ошибку. 100 % из главы 12 стали 81 % ровно по этой причине. Не делайте sampling.
Большинство production prompts относятся ко второму типу, но настраиваются как первый, потому что temperature оставили такой, какой она была в примере кода.
Два способа отсечь, и только один адаптируется
Ссылка на раздел: Два способа отсечь, и только один адаптируетсяSampling из полного распределения почти никто не делает, потому что хвост огромен и полон бессмыслицы. Что-то нужно отсекать. Есть два классических ответа, и они отличаются одним свойством, которое решает всё.
Top-k сохраняет фиксированное число кандидатов. Отсортировать по вероятности, оставить первые , отбросить остальное, нормировать заново.4 Top-p, также называемый nucleus sampling, сохраняет фиксированное количество массы: брать token по убыванию вероятности, пока их суммарная вероятность не достигнет , и остановиться.3 Формально nucleus — это наименьшее множество , для которого
Разница звучит косметически, но такой не является, потому что два prompt, отправленные в одну минуту, имеют совершенно разные формы распределения. Оба ниже — одна и та же модель при temperature 1:
Q: What is the capital of France?\nA: | Once upon a time, | |
|---|---|---|
| вероятность top-1 | 96,01 % | 25,39 % |
| token для 90 % массы | 1 | 467 |
| top-k = 40 сохраняет | 99,61 % массы | 78,87 % массы |
| масса в рангах 2–40 | 3,61 % | 53,48 % |
| token на ранге 40 | ␣Av, 0,0093 % | ␣Dr, 0,128 % |
Одно фиксированное — две ошибки в противоположные стороны. На фактическом prompt допускает 39 token, которые вместе стоят 3,6 %: правило пропускает мусор, включая кандидата с девятью тысячными процента, потому что считает слоты, а не доказательства. На сюжетном prompt то же отбрасывает 21 % массы, которую модель действительно назначила, потому что настоящий nucleus там имеет ширину 467 token.
Top-p заставляет ровно одно число делать обе работы. Установите — и на первом prompt он сохранит 1 token, а на втором 467, потому что задаёт вопрос о распределении, а не навязывает ему счётчик. Посмотрите на эту адаптацию прямо: один и тот же cut, четыре temperature:
Этот виджет также закрывает заблуждение, которое стоит людям реальных денег. На уверенном распределении top_p = 0.9 — это не «немного разнообразия». Это greedy. При temperature 1 ведущий token здесь удерживает 96,90 %, то есть уже больше 0.9, поэтому nucleus имеет ширину один token, и ничего другого выбрать невозможно. Команды ставят top_p в 0.9, думая, что что-то ослабили, а потом удивляются, почему каждый ответ одинаков.
Поставьте вместо этого top-k — и противоположная ошибка так же видна:
Штрафы, с формулами, потому что их повсеместно путают
Ссылка на раздел: Штрафы, с формулами, потому что их повсеместно путаютТри разных механизма ходят под похожими названиями, делают разные вещи, и разница измерима. Пусть — число раз, когда token уже появлялся.
Presence penalty
Ссылка на раздел: Presence penaltyВычесть константу из любого token, который вообще уже появлялся. Одно появление и сорок появлений штрафуются одинаково. Это переключатель, а не регулятор.
Frequency penalty
Ссылка на раздел: Frequency penaltyВычитать пропорционально счётчику. token, использованный четыре раза, штрафуется в четыре раза сильнее, чем token, использованный один раз, и давление накапливается по мере роста текста.
Repetition penalty (CTRL)
Ссылка на раздел: Repetition penalty (CTRL)Оригинал из статьи CTRL.7 Он делит, а не вычитает, с отдельным случаем для знака, потому что деление отрицательного logit сделало бы его больше. Поэтому его сила зависит от величины logit, а значит одно и то же бьёт по-разному в разных местах одного предложения.
То же degenerative continuation, что раньше, с применением каждого механизма. «Изменённые шаги» считают, сколько из 120 шагов генерации выбрали другой token, чем выбрала бы модель без штрафа. Здесь прогон на 120 шагов против 140 в блоке выше, поэтому baseline без штрафа показывает 85,5 %, а не 87,6 %:
| настройка | повторённые 4-grams | изменённые шаги |
|---|---|---|
| ничего | 85,5 % | 0 / 120 |
| presence 0.5 | 65,0 % | 3 / 120 |
| presence 1.0 | 3,4 % | 11 / 120 |
| frequency 0.5 | 6,0 % | 12 / 120 |
| frequency 1.0 | 0,0 % | 20 / 120 |
| repetition 1.2 (CTRL) | 0,0 % | 35 / 120 |
Отсюда следуют три вещи. Presence при 0.5 изменил три решения из 120 и сократил повторение на четверть: цикл держался на горстке token. Frequency при 0.5 изменил решений в четыре раза больше и дал гораздо больший эффект, потому что множитель счётчика продолжает расти, а константа presence — нет. А CTRL penalty на широко копируемом значении 1.2 переписал 35 из 120 решений, что не является лёгким толчком; это другая модель.
Последнее число подводит к сбою, о котором почти никто не предупреждает.
Что штрафы делают с текстом, который должен повторяться
Ссылка на раздел: Что штрафы делают с текстом, который должен повторятьсяКод повторяется. Таблицы повторяются. Списки повторяются. Структурированный вывод повторяется по определению — именно это и есть структура. Штраф не умеет отличать модель, застрявшую в цикле, от модели, правильно выводящей четвёртую строку таблицы, потому что в обоих случаях он видит повторное появление token.
Три те же задачи, сгенерированные тремя способами:
| задача | ничего | frequency 0.5 | repetition 1.2 |
|---|---|---|---|
| markdown-таблица, 6 строк | 0 / 56 изменённых шагов | 0 / 56 | 2 / 62 |
| функция Python | 0 / 93 | 0 / 93 | 10 / 110 |
| маркированный список, 1–12 | 0 / 50 | 0 / 50 | 0 / 50 |
Frequency penalty при 0.5 оказался безвреден во всех трёх случаях, что полезно и немного неожиданно, и это говорит о точной вещи: раз ни одно решение не изменилось, структурные token выигрывали свои позиции с запасом больше, чем вычитал штраф, даже после пяти и шести появлений. CTRL penalty, который вместо этого делит, их всё же выбивает, и вот что получилось:
repetition 1.2, markdown table:
| n | 2^n |
| --- | --- |
| 0 | 1 |
| 1 | 2 |
| 2 | 4 |Выравнивание разваливается: количество padding внутри каждой ячейки меняется от строки к строке, потому что серия пробелов перед закрывающей pipe — именно тот тип повторения, который штраф должен ломать. Косметика, и она стоила шесть лишних token. С Python случай не косметический:
nothing / frequency 0.5:
total = 0
for i in range(1, n + 1):
total += i ** 2
return total
repetition 1.2:
# Initialize total_sum with 0
total_sum = 0
# Loop through numbers from 1 to n, incrementing by 2 each time
for i in range(1, n + 1,Штраф столкнул модель с total — уже использованного в docstring — на total_sum, набил вывод выдуманными комментариями, чтобы тратить бюджет на неиспользованные token, а затем пришёл к трёхаргументному range с шагом. Комментарий говорит incrementing by 2 each time, что неверно для суммы квадратов от 1 до . Repetition penalty произвёл неправильный код из prompt, на который без него был дан правильный ответ.
Правило отсюда короткое: штрафы — для открытой прозы, и их следует отключать для кода, структурированного вывода, табличных данных и всего, что имеет schema. Глава 18 как раз о второй категории.
Порядок применения и почему он меняет ответ
Ссылка на раздел: Порядок применения и почему он меняет ответКаждая реальная реализация применяет это в конкретной последовательности:
penalties → temperature → top-k → top-p → sample
Это не произвольная бухгалтерия, и перестановка двух стадий даёт действительно разные распределения. Два измерения, оба на фактическом prompt.
Отсечение до или после temperature. Nucleus считается на том распределении, которое ему передали, а temperature меняет это распределение радикально:
| top-p 0.9 после temperature | top-p 0.9 до temperature | |
|---|---|---|
| 1 token | 1 token | |
| 353 token | 1 token | |
| 32 966 token | 1 token |
При одна и та же номинальная настройка даёт набор кандидатов размером 32 966 или 1 — только в зависимости от того, какая стадия идёт первой. Если вы когда-нибудь удивлялись, почему повышение temperature «ничего не делает» у одного провайдера и разрушает вывод у другого при тех же двух числах, эта таблица — правдоподобный ответ.
Штраф до или после temperature. Вычесть штраф и затем разделить на — значит получить эффективный штраф ; сначала разделить, а потом вычесть — значит получить . С presence penalty 1.0, применённым к ведущему token:
| temperature | штраф, затем temperature | temperature, затем штраф |
|---|---|---|
| 0.5 | 99,858 % | 99,948 % |
| 1.0 | 89,839 % | 89,839 % |
| 2.0 | 10,783 % | 6,830 % |
Идентично при , как и должно быть. Отличие в 1,58 раза при . «Presence penalty 1.0» не является хорошо определённой величиной штрафа, если вы не знаете, где применяется temperature, а ни один API это не документирует.
Показать детали
Опционально: весь pipeline в указанном выше порядке.
Шестнадцать строк, и в них есть всё из этой главы. Это то же вычисление, которое выполняет виджет, только на настоящем векторе logit, а не на десяти фиксированных числах.
def sample(logits, counts, presence=0.0, frequency=0.0,
temperature=1.0, top_k=0, top_p=1.0, generator=None):
z = logits.clone()
idx = torch.tensor(list(counts)) # 1. penalties
if len(idx):
z[idx] -= presence
z[idx] -= frequency * torch.tensor([float(c) for c in counts.values()])
if temperature <= 0: # 2. temperature
return int(z.argmax()) # T=0 is argmax
p = torch.softmax(z / temperature, -1)
p, order = p.sort(descending=True)
if top_k: # 3. top-k
p[top_k:] = 0
p = p * ((p.cumsum(0) - p) < top_p) # 4. top-p
p = p / p.sum() # 5. renormalise
return int(order[torch.multinomial(p, 1, generator=generator)])cumsum(0) - p в строке top-p — это накопленная масса без текущего token; именно поэтому nucleus включает token, который пересекает порог, а не останавливается прямо перед ним. Ошибитесь на единицу — и top_p = 0.9 тихо станет чуть более жёстким cut, чем в любой другой реализации.
Это одно из немногих мест во второй половине курса, где Python — правильный язык, и причина структурная, а не стилистическая: каждая строка выше требует, чтобы весь вектор logits был у вас в руках, а через HTTP API такого вектора нет. Вы можете отправить temperature и top_p провайдеру; вы не можете реализовать их сами и не можете увидеть, что они сделали.
Универсального sampling API не существует
Ссылка на раздел: Универсального sampling API не существуетКаждый провайдер принимает разное подмножество этих настроек, с разными диапазонами, и молча игнорирует остальное. Это не абстрактная жалоба. Любое приложение, предлагающее выбор модели, должно где-то записать различия, и файл, где оно это делает, является картой несовместимости. Вот что один такой каталог объявляет для одного параметра по девяти текстовым источникам, которые он поддерживает:
| объявленный диапазон temperature | источники |
|---|---|
| 0 to 1 | Anthropic, Google, Meta, Cerebras, PaLM |
| 0 to 1.5 | Mistral |
| 0 to 2 | OpenAI, DeepSeek, xAI |
Слово одно и то же; шкала — нет. «temperature of 1» у одного означает неизменённое распределение, у другого — максимально разрешённый нагрев, а половина каталога не может выразить значение, которое другая половина считает нейтральным плюс немного. Остальные ручки столь же неровные: записи OpenAI, DeepSeek и xAI принимают presence и frequency penalties и не принимают topK; записи Google, Meta, Cerebras и PaLM принимают topK и не принимают penalties; Anthropic принимает topK, topP и stop sequences, но не penalties; и ровно один из девяти — Mistral — принимает seed. Отправка параметра, который провайдер не реализует, обычно вообще не даёт ошибки: запрос успешен, ручка ничего не делает, а вы заключаете, что настройка не имеет эффекта.
И обратите внимание, что представляет собой такой файл: утверждение о чужом API, написанное в конкретный день, которое потом ничто не проверяет. Каталог, где для провайдера указано 0 to 1, хотя он теперь принимает 0 to 2, будет тихо обрезать каждый запрос.
Ещё две настройки относятся к той же семье. logprobs, если доступен, возвращает log-probabilities выбранного token и часто несколько верхних альтернатив — единственное окно в распределение, о котором эта глава, и основу каждой эвристики уверенности поверх закрытой модели. А maximum tokens плюс stop sequences завершают генерацию вообще без обращения к вероятности: жёсткий лимит и совпадение строки. Оба всплывают как finish_reason из главы 14, где length означает, что ваш ответ был обрезан посреди предложения бюджетом, а не завершён моделью.
Seed и детерминизм, которого у вас нет
Ссылка на раздел: Seed и детерминизм, которого у вас нетУстановите seed — и sampling становится воспроизводимым. Эта часть настоящая, и её легко проверить:
seed = 1234 " Paris\nWhat is a good geographical qualifier for describing
Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 1234 " Paris\nWhat is a good geographical qualifier for describing
Paris concerning its location?\nA: Near the Mediterranean Sea"
seed = 7 " Paris is the capital of France. The appellation of Paris is
\"Île de Paris\"."
seed = 7 " Paris is the capital of France. The appellation of Paris is
\"Île de Paris\"."Байт-в-байт одинаково внутри одного seed, по-разному между seed, ровно как обещано. Значит, seed фиксирует случайный выбор в последней строке функции sample — какой token будет выбран при заданном распределении.
Чего он не фиксирует, так это самого распределения. И именно тут проблема, потому что вектор logits, который выдаёт ваша модель, не математический объект; это результат миллиардов floating-point сложений, а у них есть порядок.
Глава 2 оставила этот эксперимент готовым. Один и тот же миллион чисел float32, сложенный разными группировками:
sequential 998.564270020 error vs float64: 6.393e-03
pairwise (numpy) 998.570556641 error vs float64: 1.061e-04
in 4 chunks 998.570495605 error vs float64: 1.672e-04
in 8 chunks 998.570556641 error vs float64: 1.061e-04
in 16 chunks 998.570678711 error vs float64: 1.594e-05
sequential == pairwise? False
4 chunks == 8 chunks? FalseПосмотрите на последнюю строку. Число chunks меняет ответ. Это не любопытство про numpy; это механизм, потому что когда inference-сервер делит редукцию между большим или меньшим числом параллельных блоков, он делает ровно это. А сервер делит её в зависимости от того, сколько запросов он обслуживает.
Вот этот эффект на самой модели. Тот же prompt, тот же forward pass, единственная разница — сколько других запросов случайно оказалось в batch:
20 identical forward passes, batch of 1: 20 / 20 bit-for-bit identical
the same prompt inside a batch of 2: 147,321 of 151,936 logits differ
the same prompt inside a batch of 4: 146,515 of 151,936 logits differ
the same prompt inside a batch of 8: 146,515 of 151,936 logits differ
the same prompt inside a batch of 16: 147,321 of 151,936 logits differ
largest change to any logit: 2.5e-05В одиночку модель идеально детерминирована: двадцать прогонов, идентично до бита. Положите тот же prompt в batch с неродственными запросами — и 97 % её logits меняются. В вашем запросе не изменилось ничего. Просто пришёл чей-то ещё запрос.
Теперь честная часть, потому что это обычно рассказывают так, будто история на этом закончилась. Изменение меняет вывод только если два candidate token находились друг от друга в таких пределах. За 717 шагов генерации по двенадцати prompt минимальный разрыв между двумя верхними logits был — в сто раз больше возмущения, — и ни один шаг не был достаточно близок, чтобы перевернуться. Значит, на этой модели, в float32, на ноутбуке batching сдвинул каждый logit и не изменил ни одного token.
Это описание благоприятных условий, а не успокоение, и одного изменения этих условий достаточно:
same weights, same prompts, greedy decoding, no seed involved
float32 vs bfloat16: 6 of 8 answers diverge
first divergence at step 23, on average
float32: "...it is scattered and dispersed into different colors,
including blue. The blue light is scattered more than other
colors, so it appears to come from the sky."
bfloat16: "...it is scattered and scattered, causing the colors of the
sun to be scattered and scattered, creating the appearance
of a blue color."Шесть из восьми ответов расходятся, и один из них заметно деградирует. Таблица главы 2 объясняет почему: bfloat16 хранит 7 бит mantissa, поэтому около величины logit 16 представимые значения отстоят на 0.125 — 16.0, затем 16.125, затем 16.25 — и округление может сдвинуть logit до 0.0625. Между тем 4,7 % шагов генерации, измеренных выше, имели разрыв top-two меньше 0.1. Вот и вся разница между двумя экспериментами: в float32 возмущение было в сто раз меньше ближайшего решения, а в bfloat16 оно того же размера. Production inference работает в 16-bit, на hardware с fused kernels и порядками редукции, которые никто не обещает сохранять. Является ли «численный шум пренебрежимо малым» — вопрос о precision и hardware, а не о модели.
Итак, четыре причины, как и обещала глава 9:
Floating-point сложение не ассоциативно
Ссылка на раздел: Floating-point сложение не ассоциативноБлок главы 2. Порядок суммы меняет её значение, поэтому любое изменение того, как делится редукция, меняет logits. Это субстрат; остальные три — способы изменить порядок.
Dynamic batching группирует ваш запрос с чужими
Ссылка на раздел: Dynamic batching группирует ваш запрос с чужимиContinuous batching из главы 13 делает inference доступным по цене — и означает, что форма матриц, через которые проходят ваши token, зависит от трафика. Измерено выше: 147 321 logit сдвинулся из-за изменения размера batch.
Mixture-of-experts routing зависит от batch
Ссылка на раздел: Mixture-of-experts routing зависит от batchБлок главы 9 уже сказал это. Router делает дискретный выбор на token на слой, с учётом лимитов вместимости каждого expert, посчитанных по batch. token, который в одиночку пошёл бы к expert 7, в компании идёт к expert 12. Это не разница округления; это другой набор весов.
Модель за именем меняется
Ссылка на раздел: Модель за именем меняетсяVersion string вроде -latest — это pointer, а pointer перенастраивают. Провайдеры также обновляют serving stack под фиксированным идентификатором версии. Ни то ни другое не объявляется с такой детализацией, которая позволила бы связать это с изменением вашего собственного вывода.
Параметр OpenAI seed честен насчёт этого единственным возможным способом: он поставляется вместе с полем system_fingerprint, идентифицирующим backend configuration, а документация говорит, что детерминизм — best-effort и что изменившийся fingerprint означает, что результаты могут отличаться. Читайте это буквально: провайдер сообщает вам, что он контролирует все четыре причины выше, вы не контролируете ни одну, а единственное, что он может предложить, — сказать вам после факта, что что-то сдвинулось.
Куда это ведёт дальше
Ссылка на раздел: Куда это ведёт дальшеВсё здесь было о ручке и её последствиях. Сделайте шаг назад — и появляется более сложная проблема: объект, который мы настраивали, является вероятностным распределением, а у вероятностных распределений нет интерфейса.
У function call он есть. У строки базы данных он есть. У handler POST, ожидающего JSON body с тремя обязательными полями, он есть, и всё остальное он отклонит. Между моделью и каждым другим component вашей системы находится contract, о котором одна сторона не может давать обещаний: модель произведёт что-то, выбранное из распределения, которое вы сформировали, но не зафиксировали, а коду на другой стороне нужно значение известного типа, иначе он выбросит ошибку.
Мост между этими мирами строится из материала этой главы, а не из parsing и retries. Если token сломает требуемую структуру, вы не делаете sampling и не надеетесь — вы ставите его logit в до того, как softmax вообще его увидит. Constrained decoding — это mask поверх того же вектора, который мы всю главу преобразовывали, и он превращает «пожалуйста, ответь в JSON» из просьбы в гарантию.
Глава 18 — об этом contract: tool calling, JSON Schema, structured outputs и о том, что нужно, чтобы сделать детерминированную систему безопасной для построения поверх вероятностной.
Источники и метод
Ссылка на раздел: Источники и методВсе измерения в этой главе получены из Qwen/Qwen2.5-0.5B-Instruct на CPU, float32, если не указано иное, с sampling, реализованным так, как написано в опциональном разделе, а не делегированным библиотеке. Это небольшая модель, и конкретные значения принадлежат ей; механизмы — нет. Статья Von Platen How to generate text with different decoding methods (Hugging Face, 2020) — текст, с которым сопоставлена эта глава, и всё ещё лучшее короткое введение в тот же материал. Для раздела о детерминизме: заметки PyTorch о воспроизводимости описывают, что seed фиксирует и не фиксирует на одной машине; документация OpenAI по seed и system_fingerprint описывает, что провайдер может и не может обещать; а обсуждение batch-invariant kernels от Thinking Machines (2025) — самое ясное публичное объяснение того, почему исправить это на уровне inference server возможно, но не бесплатно.
Сноски
Ссылка на раздел: Сноски-
Ackley, D. H., Hinton, G. E. and Sejnowski, T. J. A Learning Algorithm for Boltzmann Machines. Cognitive Science 9(1), pp. 147–169 (1985), где temperature в softmax приходит из статистической физики. Hinton, G., Vinyals, O. and Dean, J., Distilling the Knowledge in a Neural Network, arXiv:1503.02531 (2015), раздел 2, — место, где тот же параметр снова появляется в современном deep learning: как способ раскрыть полное распределение teacher, то есть soft labels из главы 13, а не sampling этой главы. ↩
-
Guo, C., Pleiss, G., Sun, Y. and Weinberger, K. Q. On Calibration of Modern Neural Networks. arXiv:1706.04599 (2017). Не путайте это с temperature в этой главе. Temperature scaling подбирает одно значение на валидационном наборе, чтобы уверенность модели соответствовала её точности; это post-hoc метод калибровки, применяемый к outputs классификатора. Temperature sampling — runtime control над тем, как генератор выбирает tokens. Формула та же, цель другая, общего значения нет. ↩
-
Holtzman, A., Buys, J., Du, L., Forbes, M. and Choi, Y. The Curious Case of Neural Text Degeneration. arXiv:1904.09751 (2019). Представляет nucleus sampling и измерение, показывающее, что maximisation-based decoding производит текст, чей профиль вероятностей совсем не похож на человеческий текст. ↩ ↩2
-
Fan, A., Lewis, M. and Dauphin, Y. Hierarchical Neural Story Generation. arXiv:1805.04833 (2018). Статья, популяризовавшая top-k sampling. ↩
-
Nguyen, M. et al. Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs. arXiv:2407.01082 (2024). ↩
-
Meister, C., Pimentel, T., Wiher, G. and Cotterell, R. Locally Typical Sampling. arXiv:2202.00666 (2022). ↩
-
Keskar, N. S., McCann, B., Varshney, L. R., Xiong, C. and Socher, R. CTRL: A Conditional Transformer Language Model for Controllable Generation. arXiv:1909.05858 (2019). Раздел 4.1 — оригинальный repetition penalty, тот, который делит. ↩