Генерация текста
Что вы узнаете
Заголовок раздела «Что вы узнаете»- Как обученная языковая модель пишет текст: авторегрессивный цикл «предсказать → выбрать → дописать».
- Чем отличаются жадный выбор (greedy) и семплирование, что делают температура, top-k и top-p (nucleus), и в каком порядке они применяются в коде.
- Как останавливать генерацию по токену конца текста в батче (
eos_token_id,pad_token_id). - Зачем нужен KV-кэш, что такое фазы prefill и decode и сколько памяти занимает кэш.
- Что происходит, когда текст становится длиннее контекста модели .
- Как всё это устроено в
BaseModel.generate(core/base_model.py) иcore/generation.py.
Предварительные знания
Заголовок раздела «Предварительные знания»- Языковое моделирование: модель задаёт , авторегрессия, цепное правило.
- Эмбеддинги и выходная проекция: модель выдаёт логиты , вероятности — их softmax.
- Attention: K, V, causal-маска, размер KV-кэша; маски —
attention_maskи паддинг. - Позиционное кодирование: абсолютные позиции и RoPE.
Авторегрессивный цикл
Заголовок раздела «Авторегрессивный цикл»Языковая модель не выдаёт текст целиком. По префиксу она даёт распределение следующего токена. Цепное правило (см. language-modeling.md) раскладывает вероятность продолжения в произведение таких условных вероятностей:
где:
- — промпт (prompt), заданное начало текста длины ;
- — генерируемых токенов (
max_new_tokens); - — softmax логитов последней позиции модели.
Отсюда алгоритм: посчитать распределение следующего токена, выбрать из него токен, дописать его к последовательности и повторить. Выбранный токен становится частью входа на следующем шаге — поэтому генерация авторегрессивная (autoregressive) и строго последовательная: шаг нельзя начать, пока не выбран токен шага .
x ← промпт # [B, P]повторить max_new_tokens раз: logits ← model(x) # [B, T, V] z ← logits[:, −1, :] # логиты последней позиции, [B, V] x_next ← выбрать_токен(z) # greedy или семплирование, [B, 1] x ← concat(x, x_next) # [B, T + 1] если все строки сгенерировали eos: стопвернуть xНужны логиты только последней позиции: остальные предсказывают токены, которые уже известны. При обучении, наоборот, используются все позиции сразу (teacher forcing).
%%{init: {"flowchart": {"rankSpacing": 26, "nodeSpacing": 26}}}%%
flowchart TB
P(["промпт x [B, P]"]):::io --> In["вход шага<br/>next_generation_input"]:::gray
In --> M["model(x_input, cache)<br/>→ logits [B, T, V]"]:::blue
M --> Last["logits[:, −1, :]<br/>[B, V]"]:::gray
Last --> S["sample_next_token<br/>greedy / temperature / top-k / top-p"]:::purple
S --> Eos["законченные строки → pad_token_id<br/>обновить finished"]:::gray
Eos --> Cat["x ← cat(x, next_token)"]:::gray
Cat --> Stop{"max_new_tokens<br/>или все строки<br/>закончены?"}:::add
Stop -- нет --> In
Stop -- да --> Out(["x [B, P + шаги]"]):::io
M -. "новый KV-кэш" .-> In
classDef io fill:#ffffff,stroke:#999999,color:#1a1a1a;
classDef add fill:#ffffff,stroke:#666666,color:#1a1a1a;
classDef blue fill:#dae8fc,stroke:#6c8ebf,color:#1a1a1a;
classDef purple fill:#e1d5e7,stroke:#9673a6,color:#1a1a1a;
classDef gray fill:#f5f5f5,stroke:#666666,color:#1a1a1a;
Главный вопрос — как выбрать токен из распределения. Этому посвящены следующие разделы.
Жадный выбор (greedy decoding)
Заголовок раздела «Жадный выбор (greedy decoding)»Самое простое — брать самый вероятный токен:
где — логит токена на последней позиции . Softmax монотонен, поэтому argmax логитов совпадает с argmax вероятностей, и считать softmax не нужно. В коде: logits.argmax(dim=-1, keepdim=True) при do_sample=False.
Плюсы: результат детерминирован, воспроизводим, быстр. Жадный выбор удобен для отладки и сравнения реализаций: в этом репозитории совпадение с HuggingFace проверяют именно greedy-генерацией «токен в токен».
Минусы:
- Повторения. Жадный выбор легко попадает в цикл: фраза повысила вероятность самой себя, и модель повторяет её до конца. Holtzman et al. (2020, arXiv:1904.09751) показали, что у максимизирующих методов (greedy, beam search) вероятность повтора растёт с каждым повторением — петля самоусиливается. Даже случайная необученная модель из примера ниже в одной из строк уходит в цикл
15 19 20 35 15 19 20 35 …. - Скучный текст. Человеческий текст не состоит из самых вероятных слов. Holtzman et al. показали, что вероятность токенов человеческого текста заметно «скачет», а у greedy и beam search она неестественно высока и ровна.
- Локальная оптимальность. Цепочка из самых вероятных токенов на каждом шаге не обязательно самая вероятная последовательность целиком: токен с чуть меньшей вероятностью сейчас может открыть продолжение, у которого произведение вероятностей больше. Эту проблему частично решает beam search (см. в конце главы).
Семплирование из softmax
Заголовок раздела «Семплирование из softmax»Вместо argmax можно семплировать — выбирать токен случайно с вероятностями модели:
где:
- — логиты последней позиции;
- — распределение над словарём, ;
- — случайный выбор индекса с вероятностью .
В коде это torch.multinomial(probs, num_samples=1). Генерация становится разнообразной, и от петель повторения она уходит сама. Но у чистого семплирования обратная проблема: хвост распределения. Из десятков тысяч маловероятных токенов каждый почти невероятен, но вместе они набирают заметную массу. Если 1000 «мусорных» токенов имеют по 0.0001, то с вероятностью 10 % на этом шаге будет выбран один из них, а за 50 шагов такое случится почти наверняка. Одна неудачная выборка сбивает весь последующий текст. Температура, top-k и top-p управляют именно этим компромиссом: разнообразие против надёжности.
Для воспроизводимости семплирования задают seed: torch.manual_seed(0) перед generate.
Температура
Заголовок раздела «Температура»Определение
Заголовок раздела «Определение»Температура (temperature) делит логиты перед softmax:
где:
- — логит токена ;
- — температура (
temperature), по умолчанию 1 — исходное распределение модели; - — вероятность токена после масштабирования.
Название — из статистической физики: это распределение Больцмана, где играет роль энергии состояния, а — температуры. В нейросетях та же форма softmax с температурой используется, например, при дистилляции (Hinton, Vinyals, Dean, 2015, arXiv:1503.02531).
Интуиция. Отношение вероятностей двух токенов
где — разница логитов. При разница «растягивается», сильные токены становятся ещё сильнее — распределение острее, текст консервативнее. При разница «сжимается» — распределение ровнее, текст разнообразнее и рискованнее. Порядок токенов по вероятности температура не меняет.
Пределы
Заголовок раздела «Пределы»- : распределение стремится к one-hot на (при единственном максимуме) — это greedy.
- : распределение стремится к равномерному — логиты перестают значить что-либо.
Вывод
Пусть — единственный максимальный логит. Разделим числитель и знаменатель на :
При для показатель (числитель дроби отрицателен, знаменатель стремится к нулю сверху), поэтому . Слагаемое равно . Знаменатель стремится к 1, а числитель — к 1 при и к 0 иначе. Получаем , для .
Если максимумов несколько ( штук), то в знаменателе остаётся единиц, и вероятность делится поровну между ними: .
При каждый показатель , все , знаменатель стремится к , и .
В коде нулевая температура запрещена (деление на ноль): при do_sample=True и temperature <= 0 — ValueError. Для жадной генерации используйте do_sample=False: там температура не используется и не проверяется.
Численный пример
Заголовок раздела «Численный пример»Логиты трёх токенов :
| 0.5 | (4, 2, 0) | (0.867, 0.117, 0.016) |
| 1 | (2, 1, 0) | (0.665, 0.245, 0.090) |
| 2 | (1, 0.5, 0) | (0.506, 0.307, 0.186) |
| 100 | (0.02, 0.01, 0) | (0.337, 0.333, 0.330) |
Проверка для : , , , сумма ; . При отношение вместо при .
Top-k sampling (Fan, Lewis, Dauphin, 2018, arXiv:1805.04833) отрезает хвост грубо: оставляет самых вероятных токенов, обнуляет остальные и нормирует заново:
где:
- — множество токенов с наибольшими вероятностями (или логитами — порядок тот же);
- —
top_k, целое (в этой главе — параметр семплирования, а не число экспертов MoE); - — вероятность после отсечения.
На практике это делается на логитах: у токенов вне логит заменяется на , и softmax сам даёт нули и нормировку ().
Пример. , : остаются первые два, .
Недостаток: фиксировано, а форма распределения меняется от шага к шагу. Когда модель уверена (после «Эйфелева» почти наверняка «башня»), оставляет 49 лишних кандидатов. Когда вариантов много (начало нового предложения), может отрезать разумные продолжения. — это greedy.
Top-p (nucleus sampling)
Заголовок раздела «Top-p (nucleus sampling)»Определение
Заголовок раздела «Определение»Top-p, или nucleus sampling (Holtzman et al., 2020, arXiv:1904.09751), выбирает число кандидатов адаптивно. Ядро (nucleus) — минимальное по размеру множество самых вероятных токенов, суммарная вероятность которого не меньше порога :
где:
- — порог (
top_p), обычно 0.9–0.95 (в статье Holtzman et al. он обозначен просто , здесь индекс отличает его от вероятностей ; каллиграфическое — множество токенов, а не размер словаря ); - — вероятность токена (после температуры);
- минимальное множество набирается из самых вероятных токенов — сначала самый вероятный, потом следующий и так далее.
Затем, как и в top-k, вероятности вне ядра обнуляются, а внутри — нормируются. Когда модель уверена, ядро — один-два токена; когда не уверена — сотни.
Как это реализовано
Заголовок раздела «Как это реализовано»Отсортируем вероятности по убыванию: . Токен с рангом входит в ядро, если сумма вероятностей более вероятных токенов (без него самого) меньше порога:
где — -я по величине вероятность, а сумма при пустая и равна 0.
Почему это то же самое, что «минимальное множество с суммой ». Пусть — накопленная сумма (), и — первый ранг, на котором . Минимальное ядро — ранги : любое множество из меньшего числа токенов набирает не больше , потому что самых вероятных токенов дают наибольшую сумму среди множеств такого размера. Для сумма до токена (иначе был бы меньше), для — . Значит, условие выделяет ровно ранги , и токен , на котором сумма переходит порог, в ядро входит.
Два следствия: самый вероятный токен остаётся всегда (сумма до него ), а при остаётся весь словарь (все , если у последнего токена ненулевая вероятность).
В core/generation.py, функция sample_next_token:
sorted_probs, sorted_indices = torch.sort( torch.softmax(logits, dim=-1), descending=True, dim=-1)prob_before = torch.cumsum(sorted_probs, dim=-1) - sorted_probs # c_{r−1}keep_sorted = prob_before < top_p # условие входа в ядроkeep = torch.zeros_like(logits, dtype=torch.bool).scatter_(-1, sorted_indices, keep_sorted)logits = logits.masked_fill(~keep, float("-inf"))scatter_ возвращает маску из отсортированного порядка в порядок словаря. Так же работает TopPLogitsWarper в HuggingFace. Частая ошибка — условие cumsum <= top_p: оно выбрасывает токен, на котором сумма пересекает порог, и ядро оказывается меньше, чем задано.
Численный пример
Заголовок раздела «Численный пример»(уже отсортированы):
ранг r 1 2 3 4p_(r) 0.50 0.30 0.15 0.05c_(r−1) (до) 0.00 0.50 0.80 0.95c_r (включая) 0.50 0.80 0.95 1.00top_p | Условие | Ядро | |
|---|---|---|---|
| 0.3 | только | {1} | (1, 0, 0, 0) |
| 0.7 | {1, 2} | (0.625, 0.375, 0, 0) | |
| 0.9 | {1, 2, 3} | (0.526, 0.316, 0.158, 0) | |
| 1.0 | все | {1, 2, 3, 4} | (0.5, 0.3, 0.15, 0.05) |
При top_p = 0.7 сумма переходит порог на втором токене (), и он входит в ядро. Старое условие cumsum <= 0.7 оставило бы только первый токен. При top_p = 0.9 сумма ядра , и нормировка даёт , , .
Граничный случай: при top_p = 0.8 ровно , и третий токен входит в ядро, только если — нет. Но во float32 сумма может оказаться чуть больше или меньше 0.8, так что результат на точной границе зависит от округления.
Порядок применения в коде
Заголовок раздела «Порядок применения в коде»sample_next_token(logits, do_sample, temperature, top_k, top_p) в core/generation.py получает логиты последней позиции [B, V] и выполняет шаги строго в таком порядке:
%%{init: {"flowchart": {"rankSpacing": 22, "nodeSpacing": 22}}}%%
flowchart LR
Z(["logits [B, V]"]):::io --> D{"do_sample?"}:::add
D -- нет --> G["argmax"]:::gray
D -- да --> T["logits / temperature"]:::purple
T --> K["top_k: вне top-k → −∞"]:::purple
K --> P["top_p: softmax, сортировка,<br/>вне ядра → −∞"]:::purple
P --> S["softmax → multinomial"]:::purple
G --> Out(["токены [B, 1]"]):::io
S --> Out
classDef io fill:#ffffff,stroke:#999999,color:#1a1a1a;
classDef add fill:#ffffff,stroke:#666666,color:#1a1a1a;
classDef purple fill:#e1d5e7,stroke:#9673a6,color:#1a1a1a;
classDef gray fill:#f5f5f5,stroke:#666666,color:#1a1a1a;
do_sample=False→ сразуargmax, остальные параметры игнорируются.- Деление на температуру:
logits = logits / temperature(новый тензор; входные логиты не изменяются). - Если задан
top_k:top_k = min(top_k, V), логиты внеtorch.topk→ . Значение больше словаря означает весь словарь. - Если задан
top_p: ядро считается поsoftmaxуже масштабированных логитов, вне ядра → . softmaxиtorch.multinomial— один токен на строку батча.
Порядок важен в одном месте: температура применяется до top-p, поэтому она меняет размер ядра. Та же последовательность (температура, затем top-k, затем top-p) — в HuggingFace. Для top-k порядок с температурой безразличен: деление на положительное число не меняет, какие логитов наибольшие.
Пример. , top_p = 0.6. При ядро {1, 2}: . При вероятности возводятся в квадрат и нормируются (так как ): , и уже первый токен набирает — ядро {1}, генерация становится жадной.
Одновременно top_k и top_p задавать нельзя (см. ниже), поэтому порядок шагов 3 и 4 на практике не проявляется.
Проверка аргументов: validate_sampling_args
Заголовок раздела «Проверка аргументов: validate_sampling_args»generate первым делом вызывает validate_sampling_args(do_sample, temperature, top_k, top_p) из core/generation.py. Проверки действуют только при do_sample=True: при жадной генерации эти параметры не влияют на результат, и, например, temperature=0 там допустима.
Условие (при do_sample=True) | Результат |
|---|---|
temperature <= 0 | ValueError: деление на ноль или «перевёрнутое» распределение при отрицательной температуре |
заданы и top_k, и top_p | ValueError: «top_k и top_p нельзя задавать одновременно» |
top_k <= 0 | ValueError |
top_p вне | ValueError |
Запрет комбинации top_k + top_p — выбор этого репозитория: в HuggingFace их можно задавать вместе. Без проверок top_k=0 падал бы с невнятной ошибкой в torch.multinomial.
Конец текста: eos_token_id и pad_token_id
Заголовок раздела «Конец текста: eos_token_id и pad_token_id»Модель учится выдавать специальный токен конца текста (end of sequence, EOS). Генерировать после него бессмысленно. В батче строки заканчиваются в разное время, а тензор должен оставаться прямоугольным. generate решает это так:
if pad_token_id is None: pad_token_id = eos_token_idfinished = torch.zeros(x.size(0), dtype=torch.bool, device=x.device) # [B]... if eos_token_id is not None: next_token = next_token.masked_fill(finished.unsqueeze(-1), pad_token_id) finished |= next_token.squeeze(-1) == eos_token_id x = torch.cat([x, next_token], dim=1) if eos_token_id is not None and bool(finished.all()): breakfinished[b]— закончила ли строкаb. В начале всеFalse: EOS внутри промпта строку не завершает.- Строка, которая уже закончена, получает
pad_token_idвместо того, что выбрала модель. - Строка, выбравшая EOS на этом шаге, помечается законченной; сам EOS остаётся в выходе.
- Когда закончены все строки, цикл прерывается раньше
max_new_tokens. Тогда выход корочеP + max_new_tokens. pad_token_idпо умолчанию — тот жеeos_token_id, как в HF. Безeos_token_idпараметрpad_token_idни на что не влияет.
Пример. Две строки, EOS = 21, pad = 0. Первая строка выдала 21 на втором шаге, вторая не выдала EOS:
строка 0: [промпт …] 11 21 0 0 0 … ← после EOS — паддингстрока 1: [промпт …] 9 16 36 28 3 … ← генерирует дальшеГенерация идёт, пока не закончатся все строки или шаги. Законченные строки продолжают проходить через модель (батч не сжимается) — вычисления на них тратятся впустую, но код проще.
KV-кэш при генерации
Заголовок раздела «KV-кэш при генерации»Проблема: повторные вычисления
Заголовок раздела «Проблема: повторные вычисления»Без кэша на каждом шаге модель заново обрабатывает всю последовательность, хотя изменился только последний токен. Благодаря causal-маске представления старых позиций от новых токенов не зависят: K и V позиции в каждом слое одни и те же на всех шагах. Их можно посчитать один раз и хранить — это KV-кэш (доказательство корректности и формат кэша — в attention.md).
Сложность
Заголовок раздела «Сложность»Пусть промпт длины , генерируется токенов, итоговая длина . Назовём «проходом токена» вычисление всех линейных слоёв (проекции Q/K/V/O, FFN, голова) для одной позиции — это основная часть вычислений, операций.
Без кэша шаг () обрабатывает токенов:
С кэшем первый шаг обрабатывает промпт, а каждый следующий — один новый токен:
где , , — как выше; — при порядка .
Скалярные произведения в attention кэш тоже сокращает: без него шаг строит матрицу весов — всего ; с кэшем новый токен сравнивается с ключами — всего . От квадратичной стоимости attention по длине кэш не избавляет.
Пример: , . Без кэша проходов токена, с кэшем — в 75 раз меньше.
Prefill и decode
Заголовок раздела «Prefill и decode»С кэшем генерация делится на две фазы:
- Prefill — первый шаг: весь промпт проходит через модель одним вызовом, заполняя кэш. Много токенов обрабатываются параллельно, работа упирается в вычисления (большие матричные умножения).
- Decode — остальные шаги: в модель подаётся один токен и кэш. Вычислений мало, а читать из памяти нужно все веса модели и весь кэш — работа упирается в пропускную способность памяти.
В generate это получается само: на первом шаге кэша нет, и next_generation_input возвращает весь x; дальше — только x[:, -1:] и кэш. Позиция нового токена берётся из кэша функцией cache_start_pos: для кэша (K, V) — длина K, для кэша GQA (K, V, next_pos) — next_pos (K и V со скользящим окном обрезаны, и их длина не равна позиции).
Объём кэша
Заголовок раздела «Объём кэша»На каждый токен каждый слой хранит K и V для всех голов K/V (та же формула — в attention.md):
где:
- 2 — K и V;
- — число слоёв (
num_layers); - — число голов K/V (
num_kv_heads; в MHA ); - — размер головы (
head_size); - — число закэшированных токенов (у скользящего окна — не больше ширины окна);
- bytes — байт на число (2 для float16/bfloat16, 4 для float32).
Всё это умножается ещё на размер батча . Пример (Mistral 7B, , , , , float16): байт = 512 МиБ на одну последовательность. Для Gemma 2B (, , ) при тех же и float16 — байт = 72 МиБ. Сравнение MHA, GQA и MQA — в attention.md.
В generate кэш включён по умолчанию (use_cache=True). Результат с ним и без него одинаков (для greedy — токен в токен, это проверяют тесты test_kv_cache.py), меняется только скорость.
Выход за пределы контекста
Заголовок раздела «Выход за пределы контекста»Что делает generate
Заголовок раздела «Что делает generate»Модель умеет обрабатывать не больше = max_position_embeddings позиций: у обучаемых позиционных эмбеддингов GPT столько строк, таблицы RoPE и causal-маска построены на столько позиций. forward каждой модели проверяет это функцией check_sequence_length(seq_len, start_pos, max_seq_len): если start_pos + seq_len > max_seq_len, где start_pos = cache_start_pos(cache), — ValueError.
Если же генерировать дольше, generate не падает, а работает со скользящим окном последних токенов. Это решает next_generation_input(x, cache, use_cache, max_seq_len):
if x.size(1) > max_seq_len: return x[:, -max_seq_len:], None # окно последних T_max токенов, кэш сброшенif use_cache and cache is not None: return x[:, -1:], cache # decode: только новый токенreturn x, None # prefill или генерация без кэшаПока длина , работает обычная схема prefill → decode. Как только последовательность стала длиннее, на каждом следующем шаге модель пересчитывает последние токенов с нуля, с позициями . Кэш, который forward при этом возвращает, на следующем шаге выбрасывается. Поэтому генерация за пределами контекста идёт со скоростью генерации без кэша.
Почему кэш нельзя продолжить
Заголовок раздела «Почему кэш нельзя продолжить»Казалось бы, можно выбросить из кэша самый старый токен и продолжить. Это неверно по двум причинам.
- Позиции сдвигаются. После сдвига окна токен, бывший на позиции , оказывается на . У GPT позиционный эмбеддинг прибавляется к входу и входит во все K и V всех слоёв — закэшированные K/V посчитаны со старыми позициями. У RoPE ключ повёрнут на угол старой позиции. Для RoPE сдвиг всех позиций на одно и то же число скалярные произведения не меняет (они зависят от разности позиций), но продолжить нумерацию дальше нельзя: таблиц углов для таких позиций нет, а модель на них не обучалась.
- Старый контекст «вшит» в кэш. K и V во втором и следующих слоях вычислены из скрытых состояний, которые смотрели на выброшенный токен. Даже если исправить позиции, кэш описывает состояния с контекстом, которого в окне уже нет. Честный результат для окна — прогнать его заново.
Поэтому generate пересчитывает окно: результат совпадает с эталоном, который на каждом шаге заново прогоняет последние токенов (это проверяет test_kv_cache.py).
Скользящее окно внимания Mistral здесь не помогает: оно ограничивает, какие токены видны, но позиции продолжают расти, и check_sequence_length ограничивает их тем же max_position_embeddings.
attention_mask в generate
Заголовок раздела «attention_mask в generate»Батч промптов разной длины дополняют слева: новые токены должны дописываться сразу после текста каждой строки. generate принимает attention_mask той же формы, что промпт, [B, P] (1 — токен, 0 — паддинг), и проверяет её функцией check_generation_mask: последний токен каждой строки должен быть настоящим, иначе ValueError. Так отклоняется правый паддинг — с ним генерация продолжилась бы с pad-токена. Маска из одних единиц равносильна None.
Дальше маска растёт вместе с последовательностью: на каждом шаге к ней дописывается столбец единиц, и в forward она передаётся целиком — вместе с частью, соответствующей кэшу ([B, cache_len + 1]). Когда последовательность становится длиннее , маска обрезается вместе с ней до последних столбцов. Внутри forward маска превращается в маску ключей и позиции cumsum(mask) − 1, поэтому каждая строка батча даёт то же, что её промпт, сгенерированный отдельно. Подробно — в masks.md.
torch.no_grad
Заголовок раздела «torch.no_grad»BaseModel.generate обёрнут декоратором @torch.no_grad(). Без него autograd строил бы граф вычислений на всю генерацию: каждый шаг сохранял бы активации для обратного прохода, и память росла бы с числом шагов. При генерации градиенты не нужны. Без декоратора и логиты в режиме eval() имели бы requires_grad=True: eval() отключает dropout, но не autograd.
no_grad не переключает режим модели: dropout выключается только model.eval(). Перед генерацией обученной модели вызывайте model.eval() (метод BaseModel.load возвращает модель уже в eval).
Сигнатура BaseModel.generate
Заголовок раздела «Сигнатура BaseModel.generate»Один метод для всех шести моделей (core/base_model.py); наследники реализуют только forward и задают _max_seq_len:
@torch.no_grad()def generate(self, x, max_new_tokens, do_sample, temperature=1.0, top_k=None, top_p=None, use_cache=True, attention_mask=None, eos_token_id=None, pad_token_id=None) -> torch.Tensor| Параметр | Тип, по умолчанию | Смысл |
|---|---|---|
x | LongTensor [B, P] | промпт — индексы токенов |
max_new_tokens | int, обязательный | сколько токенов сгенерировать (меньше — если все строки выдали EOS) |
do_sample | bool, обязательный | False — greedy (argmax); True — семплирование |
temperature | float, 1.0 | температура ; только при do_sample=True |
top_k | int или None | оставить top_k самых вероятных токенов; больше словаря — весь словарь |
top_p | float из или None | nucleus sampling; нельзя вместе с top_k |
use_cache | bool, True | KV-кэш: тот же результат, быстрее |
attention_mask | Tensor [B, P] или None | маска промпта; паддинг — только слева (последний токен строки настоящий) |
eos_token_id | int или None | токен конца текста; остановка, когда все строки его выдали |
pad_token_id | int или None | чем заполнять законченные строки; по умолчанию eos_token_id |
Возвращает LongTensor [B, P + n], где n — число сделанных шагов (max_new_tokens или меньше). Промпт входит в выход.
Исключения: ValueError — неверные параметры семплирования (см. выше), attention_mask не той формы или с паддингом в конце строки; TypeError — неизвестный именованный аргумент: опечатка вроде max_lenght не проглатывается молча.
Примеры
Заголовок раздела «Примеры»Учебная модель со случайными весами — чтобы запустить без обучения:
import torchfrom llm.models.gpt import GPT
torch.manual_seed(0)model = GPT({"vocab_size": 50, "embed_dim": 32, "num_heads": 4, "num_layers": 2, "max_position_embeddings": 16, "dropout": 0.0}).eval()x = torch.randint(0, 50, (2, 10)) # промпт [B=2, P=10]
# greedy; 30 > 16 — выход за контекст обрабатывается окномout = model.generate(x, max_new_tokens=20, do_sample=False)print(out.shape) # torch.Size([2, 30])
# кэш не меняет результатsame = model.generate(x, max_new_tokens=20, do_sample=False, use_cache=False)print(torch.equal(out, same)) # True
# семплированиеtorch.manual_seed(42)model.generate(x, max_new_tokens=20, do_sample=True, temperature=0.8)model.generate(x, max_new_tokens=20, do_sample=True, top_k=10)model.generate(x, max_new_tokens=20, do_sample=True, top_p=0.9, eos_token_id=3, pad_token_id=0)
# ошибкиmodel.generate(x, 5, do_sample=True, top_k=5, top_p=0.9) # ValueErrormodel.generate(x, 5, do_sample=True, temperature=0) # ValueErrormodel.generate(x, 5, do_sample=False, temperature=0) # можно: greedy температуру не используетС токенизатором (см. tokenization.md) промпт получается кодированием текста, а выход декодируется обратно; для моделей с весами HF — примеры в gpt.md и gemma.md.
Другие методы декодирования
Заголовок раздела «Другие методы декодирования»В репозитории реализованы только greedy, температура, top-k и top-p. Для полноты — чем ещё пользуются на практике.
- Beam search (лучевой поиск). Хранит лучших частичных последовательностей и на каждом шаге расширяет каждую, оставляя лучших по суммарной log-вероятности. Приближает самую вероятную последовательность целиком — хорош для перевода (использовался, например, в Sutskever, Vinyals, Le, 2014, arXiv:1409.3215), но для открытой генерации даёт те же повторения и «скучный» текст, что greedy (Holtzman et al., 2020).
- Repetition penalty (штраф за повторения). Логиты уже сгенерированных токенов делятся на коэффициент , и эти токены становятся менее вероятными (в реализации HF отрицательные логиты на него умножаются). Предложен в CTRL (Keskar et al., 2019, arXiv:1909.05858).
- Speculative decoding (спекулятивное декодирование). Маленькая «черновая» модель быстро предлагает несколько токенов, большая проверяет их за один проход, а схема принятия/отклонения гарантирует, что распределение выхода совпадает с распределением большой модели (Leviathan, Kalman, Matias, 2023, arXiv:2211.17192; Chen et al., 2023, arXiv:2302.01318). Ускоряет фазу decode, упирающуюся в память.
Типичные ошибки и тонкости
Заголовок раздела «Типичные ошибки и тонкости»- Забыли
model.eval().no_gradне выключает dropout — генерация будет шумной и невоспроизводимой. temperature=0сdo_sample=True. ДаётValueError. Для детерминированного результата —do_sample=False.- Ожидание, что выход — только новые токены.
generateвозвращает промпт вместе с продолжением; новые токены —out[:, x.size(1):]. - Батч промптов разной длины. Паддинг в
generateне поддерживается; генерируйте по одному или выровняйте длины. - Длинная генерация медленная. После токенов каждый шаг пересчитывает всё окно, кэш не помогает.
- Top-p на границе. Если накопленная сумма ровно равна
top_p, включение следующего токена зависит от округления float32. - Сравнение семплирования с кэшем и без. Для greedy результаты совпадают токен в токен. Для семплирования с тем же seed они обычно тоже совпадают, но крошечные расхождения логитов в последних битах могут изменить выбор, если случайное число попало на границу.
- Генерация — цикл: логиты последней позиции → выбор токена → дописать → повторить.
- Greedy детерминирован, но склонен к повторениям; семплирование разнообразно, но рискует хвостом распределения.
- Температура делит логиты: — greedy, — равномерное распределение.
- Top-k оставляет лучших токенов; top-p — минимальное ядро с суммой , включая токен, пересекающий порог. В коде: температура → top-k → top-p → softmax → multinomial;
top_kиtop_pвместе запрещены. eos_token_idзавершает строки, законченные строки заполняютсяpad_token_id, цикл останавливается, когда закончены все.- KV-кэш сокращает проходы токенов с до ; prefill — промпт целиком, decode — по одному токену; объём кэша .
- За пределами
generateпересчитывает последние токенов без кэша: позиции сдвинулись, а старые K/V помнят выброшенный контекст.
Вопросы и упражнения
Заголовок раздела «Вопросы и упражнения»- Логиты . Чему равны вероятности при и в пределе ?
Ответ
При : для первых двух и для третьего. При максимумов два, и вероятность делится между ними поровну: .
- Вероятности . Какое ядро и какое распределение после нормировки получатся при
top_p = 0.3,0.5,0.75?
Ответ
Суммы до токена: . При top_p = 0.3 входит только первый токен: — хотя его вероятность 0.4 больше порога, самый вероятный токен остаётся всегда. При top_p = 0.5 входят ранги с суммой до : {1, 2} → ; второй токен пересекает порог () и входит. При top_p = 0.75 — {1, 2, 3} → .
- Покажите, что
do_sample=True, top_k=1даёт тот же результат, чтоdo_sample=False(если максимум единственный). Аtop_p— при каком значении?
Ответ
При top_k=1 все логиты, кроме максимального, заменяются на , softmax даёт one-hot, и multinomial всегда выбирает argmax. Для top-p нужно top_p : тогда уже у второго токена сумма до него , и в ядре остаётся один токен. Порог зависит от распределения на каждом шаге, поэтому «универсального» значения нет; очень малое top_p (например, ) почти всегда даёт greedy.
- Промпт , генерируется токенов. Во сколько раз меньше проходов токена нужно с кэшем? Как изменится ответ, если ?
Ответ
Без кэша: . С кэшем при : , в раз меньше. При : шаги с длиной входа — prefill 500 и ещё 12 шагов по 1 токену (длины 501…512). Остальные 487 шагов (длины 513…999) пересчитывают окно из 512 токенов. Итого . Без кэша при том же вход тоже обрезается до 512: . Выигрыш от кэша почти исчезает.
- Посчитайте объём KV-кэша LLaMA 7B (, , ) на 4096 токенов во float16 для батча из 8 последовательностей.
Ответ
На одну последовательность байт = 2 ГиБ (как в attention.md). На батч 8 — 16 ГиБ, больше, чем веса модели во float16 (~13,4 ГБ).
- Почему
generateне может просто удалить первый элемент из K и V каждого слоя при выходе за ? Приведите оба аргумента и скажите, какой из них остаётся для модели с RoPE.
Ответ
(1) Позиции всех токенов сдвигаются на единицу, а в кэше K/V посчитаны со старыми позициями; (2) K/V второго и дальнейших слоёв зависят от скрытых состояний, видевших выброшенный токен. Для RoPE сдвиг всех позиций сам по себе скалярные произведения не меняет, но продолжать нумерацию за нельзя (нет таблиц и модель там не обучалась), а аргумент (2) остаётся в силе для любой схемы позиций.
- (Код.) Сгенерируйте батч из 4 строк с
eos_token_idи убедитесь, что после первого EOS в каждой строке стоят толькоpad_token_id. Что вернётgenerate, если EOS выдали все строки на третьем шаге приmax_new_tokens=10?
Ответ
Тензор формы [4, P + 3]: цикл прерывается после шага, на котором закончилась последняя строка.
- Температура и nucleus: при каких ядро
top_p = 0.9для распределения станет больше трёх токенов? (Подсказка: при распределение выравнивается; проверьте численно, например .)
Ответ
При температуре вероятности пропорциональны . При : , сумма 1.866, нормировано . Сумма первых трёх , поэтому ядро — все четыре токена. При сумма первых трёх ещё . Порог — решение уравнения , численно : при больших температурах ядро — все четыре токена.
Литература
Заголовок раздела «Литература»- Sutskever, Vinyals, Le. Sequence to Sequence Learning with Neural Networks. 2014. arXiv:1409.3215 — beam search в нейросетевом переводе.
- Hinton, Vinyals, Dean. Distilling the Knowledge in a Neural Network. 2015. arXiv:1503.02531 — softmax с температурой.
- Fan, Lewis, Dauphin. Hierarchical Neural Story Generation. 2018. arXiv:1805.04833 — top-k sampling.
- Keskar et al. CTRL: A Conditional Transformer Language Model for Controllable Generation. 2019. arXiv:1909.05858 — repetition penalty.
- Holtzman, Buys, Du, Forbes, Choi. The Curious Case of Neural Text Degeneration. ICLR 2020. arXiv:1904.09751 — nucleus (top-p) sampling, вырождение greedy и beam search.
- Leviathan, Kalman, Matias. Fast Inference from Transformers via Speculative Decoding. 2023. arXiv:2211.17192.
- Chen et al. Accelerating Large Language Model Decoding with Speculative Sampling. 2023. arXiv:2302.01318.