Бэклог
Технический долг, найденный при разборе кода. Каждая запись: где проблема, как её воспроизвести, как исправить.
Приоритеты: P1 — неверный результат или падение; P2 — расхождение с документацией или статьёй, дешёвые исправления; P3 — качество кода.
Бэклог составлен по master после влития fix/kv-cache (PR #8) и feat/gpt-activation (PR #9), на 2026-09-28; описания «Что» и «Воспроизведено» относятся к тому состоянию. Строки «Статус» обновляются по мере исправлений и отражают master после PR #58: на 2026-09-30 исправлены все 62 пункта. Пункты с пометкой «воспроизведено» проверены запуском (torch 2.8). Величины расхождений при префилле кусками зависят от seed и приведены для порядка.
Пункты 49–55 добавлены при сверке бэклога с кодом и первоисточниками, пункт 56 — при исправлении пункта 3, пункты 57–62 — при написании учебного пособия (раздел «Токенизатор, данные и обучение»). Они стоят в разделах своих архитектур, номера не перенумерованы, чтобы не ломать перекрёстные ссылки.
Модель: models/gpt/gpt.py, блок: core/gpt_decoder.py. Пункты 2, 3, 8 и 10 касаются общих модулей и затрагивают и другие архитектуры.
1. generate падает за пределами max_position_embeddings — P1
Заголовок раздела «1. generate падает за пределами max_position_embeddings — P1»- Где:
GPT.generate,GPT.forward,PositionalEmbeddings.forward. - Что: контекст не обрезается. Проверки длины смотрят на
seq_len, а не наstart_pos + seq_len: вGPT.forward(x.size(1)), вPositionalEmbeddings.forwardи вMultiHeadAttention.forward(проверяется только текущий кусок, без длины кэша).GPT2.forwardпри переданном кэше пропускает проверку совсем. - Воспроизведено:
max_position_embeddings=16, промпт 10 токенов,max_new_tokens=10. С кэшем —IndexError: index out of range in selfизnn.Embedding(на CUDA — device-side assert). Без кэша —ValueError. - Исправление: при
x.size(1) > max_seq_lenобрезать окноx[:, -max_seq_len:]и пересчитывать без кэша (абсолютные позиции сдвигаются, кэш становится невалидным). Проверятьstart_pos + seq_lenвforward,PositionalEmbeddingsиMultiHeadAttention. - Статус: исправлено в ветке
fix/p1-bugsдля всех шести моделей: общиеnext_generation_inputиcheck_sequence_lengthвcore/generation.py, проверкиstart_pos + seq_lenвforwardмоделей,PositionalEmbeddingsи всех трёх модулях attention. Генерация с кэшем и без совпадает с эталоном, пересчитывающим последниеmax_position_embeddingsтокенов на каждом шаге (test_kv_cache.py).
2. Нет causal-маски при кэше и seq_len > 1 — P1
Заголовок раздела «2. Нет causal-маски при кэше и seq_len > 1 — P1»- Где:
core/multi_head_attention.py,if cache is None: scores = scores.masked_fill(...). - Что: если передать кэш и несколько токенов (префилл кусками, спекулятивное декодирование), будущие токены внутри куска не маскируются.
generateподаёт по одному токену, поэтому там не проявляется. - Воспроизведено: префилл 4 + 6 токенов через кэш расходится с полным forward на 0.21 по логитам.
- Исправление: всегда накладывать маску со сдвигом
self._tril_mask[start_pos:start_pos + seq_len, :start_pos + seq_len]. - Статус: исправлено в ветке
fix/p1-bugs(вместе с пунктами 27 и 41): префилл любыми кусками совпадает с полным forward до 5e-7 во всех шести моделях (test_kv_cache.py).
3. attention_mask молча игнорируется — P1
Заголовок раздела «3. attention_mask молча игнорируется — P1»- Где: принимается в
GPT.forward,GptDecoder.forward,MultiHeadAttention.forward, но нигде не применяется. УGPT2.forwardтакого параметра нет вовсе (передача даётTypeError), маску принимает толькоGPT2.generate.hf-proxy/src/hf_proxy/hf_adapter.pyвforwardмаску отбрасывает (self.llm_model(input_ids)), а вgenerateпередаёт, ожидая, что она сработает. - Воспроизведено:
attention_maskиз нулей даёт логиты, побитово равные вызову без маски. - Уточнение: при правом паддинге маска не нужна: causal-маска и так не даёт настоящим токенам смотреть на стоящий после них паддинг (проверено: выход совпадает до 3e-7). Молча неверный результат получается при левом паддинге (расхождение 0.9–1.9) и при генерации после паддинга. Обучение через hf-proxy не страдало: коллатор дополняет справа.
- Исправление: пробросить маску до attention и накладывать её вместе с causal-маской (
[B, T]→[B, 1, 1, T_kv]). Либо, пока не реализовано, убрать параметр или бросатьNotImplementedError, чтобы не было молчаливой ошибки. - Статус: исправлено в ветке
fix/p1-bugsвторым способом.forwardвсех шести моделей принимаетattention_mask; маска из единиц и правый паддинг допускаются, на остальные маски с нулями (левый паддинг, пропуски, нули с кэшем, любые нули вgenerate) —NotImplementedError(check_attention_maskвcore/generation.py).hf_adapter.forwardпередаёт маску в модель. Поддержка левого паддинга вынесена в пункт 56; объяснение масок — в masks.md.
4. generate не валидирует аргументы, хотя докстринг обещает — P2
Заголовок раздела «4. generate не валидирует аргументы, хотя докстринг обещает — P2»- Где:
GPT.generate. - Что: в докстринге описаны
ValueErrorприtemperature ≤ 0, одновременныхtop_kиtop_p,top_k ≤ 0,top_p ∉ (0, 1]. В коде проверок нет. - Воспроизведено:
temperature=0.0иtop_k=5, top_p=0.9принимаются молча. Приtemperature ≤ 0(в том числе отрицательной) масштабирование просто пропускается.top_k=0падает с невнятнымRuntimeError: probability tensor contains either inf, nan or element < 0. - Исправление: добавить проверки из докстринга в начало метода.
- Статус: исправлено в ветке
test/tokenizer-and-temperature: общаяvalidate_sampling_args(core/generation.py) вызывается в началеgenerateвсех шести моделей. Проверки действуют приdo_sample=True; при жадной генерации параметры сэмплирования не влияют на результат и не проверяются (temperature=0допустима).
49. Top-p отбрасывает токен, пересекающий порог — P2
Заголовок раздела «49. Top-p отбрасывает токен, пересекающий порог — P2»- Где:
generateво всех шести моделях,sorted_mask = cum_probs <= top_p. - Что: маска оставляет только токены, у которых накопленная вероятность включая сам токен не больше
top_p. Токен, на котором сумма переходит порог, выкидывается, хотя в nucleus sampling (Holtzman et al., 2019;TopPLogitsWarperв HF) он входит в ядро. При вероятностях[0.5, 0.3, 0.2]иtop_p=0.7остаётся один токен вместо двух; приtop_pменьше вероятности самого частого токена ядро держится только за счёт принудительного первого токена. - Исправление: сдвинуть маску —
cum_probs - sorted_probs < top_p(илиsorted_mask[..., 1:] = sorted_mask[..., :-1].clone(); sorted_mask[..., 0] = True). Вносить в общую функцию выбора токена (пункт 18). - Попутно: при
temperature ≤ 0logits_scaled— тот же тензор, чтоlogits, и запись-infна месте в ветке top-p портит выходforward. Сейчас безвредно, но после вынесения в общую функцию лучше клонировать. - Статус: исправлено в ветке
refactor/shared-generate: выбор токена — общаяsample_next_token(core/generation.py), токен остаётся в ядре, если сумма вероятностей более вероятных токенов меньшеtop_p(какTopPLogitsWarperв HF). Логиты не изменяются на месте. На тех же весах и seed результат top-p поменялся у трёх моделей из шести, остальные режимы совпадают с прежними побитово.
56. Нет поддержки левого паддинга — P2
Заголовок раздела «56. Нет поддержки левого паддинга — P2»- Где: все шесть моделей,
check_attention_maskвcore/generation.py. - Что: генерация батчем промптов разной длины требует левого паддинга: строки должны кончаться в одной позиции. Для этого нужна маска ключей во всех трёх модулях attention и сдвиг позиций для каждой строки батча: иначе первый настоящий токен получает позицию, равную длине паддинга, а от позиции зависят
PositionalEmbeddingsи RoPE. До исправления такие маски отклонялись сNotImplementedError(пункт 3). - Исправление:
position_ids = attention_mask.cumsum(-1) − 1(как в HF) с передачей вPositionalEmbeddingsиRoPEпо строкам батча; маска ключей[B, 1, 1, T_kv]вместе с causal-маской; вgenerate— продление маски единицами для новых токенов и хранение её рядом с кэшем. Тест: логиты настоящих токенов с левым паддингом совпадают с прогоном без паддинга. - Статус: исправлено в ветке
feat/left-padding:padding_from_attention_mask(core/padding.py) строит поattention_maskмаску ключей и позицииcumsum − 1, какposition_idsв HF; модели передают их через декодеры вMultiHeadAttention,GroupedQueryAttention,MultiQueryAttention(параметрpadding),RoPEиPositionalEmbeddings(параметрpositions). Паддинг поддерживается в любом месте строки вforward, с кэшем маска всегда покрывает кэш ([batch, cache_len + seq_len], даже из одних единиц — иначе паддинг в кэше остался бы незамаскированным без ошибки);generateпринимает левый паддинг, наращивает маску и обрезает её вместе с окномmax_seq_len, правый паддинг вgenerate—ValueError. pad-токен как запрос видит только себя (иначе NaN). Без маски или с маской из единиц — побитово прежний результат. Проверено для всех шести моделей: каждая строка батча — как без паддинга, вforward, с кэшем и вgenerate, в том числе за пределамиmax_position_embeddings; сверка с HF (GPT-2, LLaMA, Mistral с окном, Gemma) на левом паддинге: логиты настоящих токенов и greedy-генерация совпадают.
Отклонения от GPT-1
Заголовок раздела «Отклонения от GPT-1»5. Нет weight tying — P2
Заголовок раздела «5. Нет weight tying — P2»- Что: в оригинальном коде OpenAI (
finetune-transformer-lm/train.py:tf.matmul(h, we, transpose_b=True), без bias) и в HuggingFace (OpenAIGPTLMHeadModel.lm_head, без bias, привязан кtokens_embed) выходная проекция делит веса с токенными эмбеддингами. В тексте статьи GPT-1 это явно не сказано — следует из кода. Здесь_linear— отдельныйnn.Linearс bias. - Последствия: примерно на
vocab_size × embed_dimпараметров больше; весаopenai-community/openai-gptнапрямую не загружаются. - Исправление:
_linear = nn.Linear(embed_dim, vocab_size, bias=False)и_linear.weight = _token_embeddings._embedding.weight, под флагом конфига, если нужна обратная совместимость чекпойнтов. - Статус: исправлено в ветке
feat/gpt-weight-tying: ключ конфигаtie_word_embeddings(по умолчаниюfalse— прежняя отдельная проекция с bias, старые чекпоинты загружаются). Сtrue_linearсоздаётся без bias и делит параметр с_token_embeddings._embedding.weight(output_projectionвcore/token_embeddings.py). Весаopenai-community/openai-gptзагружаются черезconvert_hf_state_dict(models/gpt/hf_weights.py): число параметров совпадает с HF (116 534 784), логиты — до 2.3e-5, greedy-генерация на 20 токенов — токен в токен. Заодно выяснилось, чтоafn="gelu"у HF OpenAIGPT — tanh-аппроксимация, то есть нашactivationпо умолчанию.
6. Нет dropout на весах внимания — P3
Заголовок раздела «6. Нет dropout на весах внимания — P3»- Что: в GPT-1 (разд. 4.1 статьи: «residual, embedding, and attention dropouts» 0.1;
attn_pdrop=0.1в HF) dropout применяется к весам после softmax. Здесь есть только dropout после выходной проекции. - Исправление:
weights = self._attn_dropout(F.softmax(scores, dim=-1)), отдельным параметром. - Статус: исправлено в ветке
feat/attention-dropout:MultiHeadAttentionпринимаетattention_dropoutи применяет его к весам после softmax;GPTчитаетconfig["attention_dropout"]. По умолчанию0.0— обучение с текущими конфигами побитово прежнее (nn.Dropout(0)не расходует генератор случайных чисел); значение из статьи,0.1, задаётся в конфиге явно. Остальные два dropout GPT-1 (эмбеддинги и выходы подблоков) по-прежнему задаются общимdropout.
7. Нет инициализации весов из статьи — P3
Заголовок раздела «7. Нет инициализации весов из статьи — P3»- Что: в статье (разд. 4.1) и в
train.pyвеса инициализируются N(0, 0.02). В репозитории используется инициализация PyTorch по умолчанию: std весовLinear≈ 0.1, эмбеддингов ≈ 1.0. - Исправление: метод
_init_weights(Linear/Embedding —normal_(0, 0.02), bias — нули) и вызовself.apply(...)в__init__. - Статус: исправлено в ветке
feat/gpt-init:init_normal_(core/weight_init.py) применяется вGPT.__init__— Linear и Embedding N(0, 0.02), bias нули; std задаётся ключомinitializer_range. Начальный loss свежей модели — 6.96 приln V = 6.91(было 7.07), разброс логитов 0.32 вместо 0.58. Чекпоинты и их выход не затрагиваются: загрузка перезаписывает инициализацию.
Качество кода
Заголовок раздела «Качество кода»8. use_cache=True по умолчанию и нет torch.no_grad() в generate — P2
Заголовок раздела «8. use_cache=True по умолчанию и нет torch.no_grad() в generate — P2»- Что: при обучении
forwardвозвращает ненужные K/V каждого слоя.generateбезno_gradу вызывающего строит autograd-граф на всю генерацию. - Воспроизведено: в
eval()логиты имеютrequires_grad=True, кэш возвращается по умолчанию. - Исправление:
use_cache=Falseпо умолчанию вforward(проверитьTrainerиhf_adapter), декоратор@torch.no_grad()наgenerate. - Статус: исправлено в ветке
refactor/shared-generate: уforwardвсех моделейuse_cache=Falseпо умолчанию (Trainerиhf_adapterвызывают без кэша,generateпередаётuse_cacheявно),BaseModel.generateпод@torch.no_grad().
9. Приведение dtype внутри FeedForward.forward — P3
Заголовок раздела «9. Приведение dtype внутри FeedForward.forward — P3»- Где:
core/feed_forward.py. - Что:
_layer1/_layer2переприсваиваются во время forward, если dtype входа отличается. Это скрывает ошибки dtype и рассинхронизирует состояние оптимизатора. - Исправление: убрать, приводить модель снаружи (
model.to(dtype)) или использоватьtorch.autocast. - Статус: исправлено в ветке
fix/feedforward-dtype: приведение убрано. Воспроизведено до исправления: один вызов с fp16 навсегда переводил веса в fp16 (после возврата к fp32 веса отличались от исходных до 1.2e-4), подtorch.autocastвесаFeedForwardстановились bf16 при fp32 у остальных слоёв.
10. Интерфейс BaseModel не соответствует моделям — P3
Заголовок раздела «10. Интерфейс BaseModel не соответствует моделям — P3»- Где:
core/base_model.py. - Что: объявлены
forward(input_ids, attention_mask) -> Tensorиgenerate(input_ids, max_length);GPTвозвращает(logits, cache)и принимаетmax_new_tokens,do_sampleи т.д. - Исправление: привести абстрактные сигнатуры к фактическим.
- Статус: исправлено в ветке
refactor/shared-generate:BaseModelобъявляет фактическийforward(x, use_cache=False, cache=None, attention_mask=None) -> (logits, cache)и реализует общийgenerate. Порядок параметровGPT.forwardисторически другой (x, attention_mask, use_cache, cache);generateи адаптер вызывают его по именам.
11. Документация противоречит коду — P2
Заголовок раздела «11. Документация противоречит коду — P2»- Докстринг
GptDecoderназывает блок «pre-LN» и приводит pre-LN псевдокод; в коде post-LN. - Пример в докстринге
GptDecoderиспользуетDecoder(...)и ожидает отdecoder(x)тензор, а возвращается кортеж. - Докстринг
GptDecoder.forwardназывает аргументmask(на делеattention_mask) и обещает тензор на выходе. - В References класса
GPTбитая ссылка на статью:research-covers/languageunsupervised/(нет дефиса, правильноlanguage-unsupervised). - Статус: исправлено в ветке
chore/docs-and-cleanup: докстрингиGptDecoderописывают post-LN, пример используетGptDecoderи кортеж на выходе, уforwardописаны фактические параметры; ссылка на статью GPT-1 исправлена. Неиспользуемые параметрыmaskудалены изforwardвсех модулей attention и декодеров (маска паддинга проверяется вforwardмодели, пункт 3); тесты, которые передавали маску и проверяли только форму, заменены проверками встроенной causal-маски.
12. Мусор в коде — P3
Заголовок раздела «12. Мусор в коде — P3»Закомментированный старый— удалён вместе с копиямиgenerateв концеgpt.pygenerate(пункт 18).- Неиспользуемые импорты:
Optional,Dictвgpt.py(mathвfeed_forward.pyудалён). Мёртвые проверки— удалены в веткеhasattr(torch, "bool")refactor/remove-dead-bool-checks(см. пункт 32).Сравнения— были только в копияхdo_sample == True,top_k != Nonegenerateи ушли вместе с ними (пункт 18).- Статус: исправлено в ветке
chore/docs-and-cleanup: неиспользуемыеOptional,Dictудалены.
Модель: models/gpt/gpt2.py, блок: core/gpt2_decoder.py.
Общие с GPT-1 пункты касаются GPT-2 так же и здесь не повторяются:
- 1, 2, 3 — генерация за
max_position_embeddings, маска при кэше иattention_mask(исправлены). До исправления: с кэшемIndexError, без кэшаValueError; префилл 4 + 6 расходился с полным forward на 0.12–0.16;GPT2.forwardне принималattention_mask,generateпринимал и игнорировал. - 56 — нет поддержки левого паддинга (исправлен).
- 4, 49 — валидация аргументов
generateи top-p (исправлены). - 8 —
use_cache=Trueпо умолчанию и нетno_grad(исправлен). - 9 — dtype в
FeedForward(исправлен). - 10 — интерфейс
BaseModel(исправлен).
Отклонения от GPT-2
Заголовок раздела «Отклонения от GPT-2»13. GELU: точная erf-версия вместо tanh-аппроксимации — P2
Заголовок раздела «13. GELU: точная erf-версия вместо tanh-аппроксимации — P2»- Что:
Gpt2DecoderиGptDecoderиспользовалиnn.GELU(), то есть erf. Оригинальный код OpenAI (gpt-2/src/model.py,finetune-transformer-lm/train.py) и HF (GPT2Config.activation_function="gelu_new"; вmodeling_openaiACT_FNS["gelu"]— этоgelu_new) используют tanh-аппроксимацию. Опция'gelu_exact'вFeedForwardна деле подключала tanh-аппроксимацию. - Воспроизведено: при одинаковых весах логиты отличались от эталона с tanh-GELU на ~1e-4. С tanh-GELU расхождение 5e-7.
- Статус: исправлено в ветке
fix/gelu-tanh:'gelu_exact'переименован в'gelu_tanh',Gpt2Decoderиспользует'gelu_tanh', у GPT-1 это значение по умолчанию дляconfig["activation"]('gelu'— точный erf-вариант — остаётся доступным).
14. Нет weight tying, у lm-head есть bias — P2
Заголовок раздела «14. Нет weight tying, у lm-head есть bias — P2»- Что: в оригинале (
gpt-2/src/model.py:tf.matmul(h, wte, transpose_b=True)) и в HF (GPT2LMHeadModel.lm_head,bias=False,tie_word_embeddings=True) выходная проекция делит веса сwte. Здесь_linear— отдельныйnn.Linearс bias. - Воспроизведено:
m._linear.bias is not None,m._linear.weight is not m._token_embeddings._embedding.weight. - Последствия: для конфигурации 124M лишних ~38M параметров (
50257 × 768). Весаopenai-community/gpt2напрямую не загружаются. - Исправление: как в пункте 5 для GPT-1.
- Статус: исправлено в ветке
feat/gpt-weight-tying, как пункт 5. Весаopenai-community/gpt2загружаются черезconvert_hf_state_dict: 124 439 808 параметров, как в HF (без tying — 163 087 441), логиты — до 7.6e-5, greedy-генерация — токен в токен.
15. Нет dropout на весах внимания — P3
Заголовок раздела «15. Нет dropout на весах внимания — P3»- Что: в HF-реализации GPT-2 (
attn_pdrop=0.1) dropout применяется к весам после softmax. Вgpt-2/src/model.pydropout нет вовсе — это код только для инференса. Здесь только dropout после выходной проекции (resid_pdrop). - Исправление: общее с пунктом 6, так как
MultiHeadAttentionобщий. - Статус: исправлено в ветке
feat/attention-dropout, как пункт 6:GPT2читаетconfig["attention_dropout"], по умолчанию0.0, в HF —0.1.
16. Нет инициализации весов из статьи — P3
Заголовок раздела «16. Нет инициализации весов из статьи — P3»- Что: статья GPT-2 (разд. 2.3) масштабирует веса residual-слоёв на
1/√N, где N — число residual-слоёв; в HF (GPT2PreTrainedModel._init_weights) это0.02 / √(2·num_layers)дляc_projв attention и MLP, остальные веса — N(0, 0.02). Значение 0.02 в статье не указано, оно из кода: вgpt-2/src/model.py0.02 для весов иwte, но 0.01 дляwpe, а масштабирования residual-проекций в коде нет. В репозитории инициализация PyTorch по умолчанию. - Исправление: как в пункте 7, плюс
normal_(0, 0.02 / math.sqrt(2 * num_layers))дляMultiHeadAttention._layerиFeedForward._layer2. - Статус: исправлено в ветке
feat/gpt-init: как пункт 7, плюсscale_residual_projections_—MultiHeadAttention._layerиFeedForward._layer2каждого блока N(0, 0.02 / √(2·num_layers)), как в HF.wpe— 0.02, как в HF (в коде OpenAI 0.01).
Качество кода
Заголовок раздела «Качество кода»17. _tril_mask сохраняется в state_dict — P2
Заголовок раздела «17. _tril_mask сохраняется в state_dict — P2»- Где:
core/multi_head_attention.py,register_buffer('_tril_mask', ...). Затрагивает все модели наMultiHeadAttention. - Что: буфер
max_seq_len × max_seq_lenpersistent: попадает в каждый чекпоинт по одному на слой и привязывает чекпоинт кmax_seq_len. - Воспроизведено: ключи
_decoders.{i}._heads._tril_maskвstate_dict. Приmax_position_embeddings=1024это 1 МБ на слой. - Исправление:
register_buffer(..., persistent=False). Старые чекпоинты при этом загружаются только сstrict=Falseили после удаления ключей. - Статус: исправлено в ветке
fix/nonpersistent-buffers(вместе с 25, 31, 47):register_buffer(..., persistent=False). Старые чекпоинты с этими ключами по-прежнему загружаются и соstrict=True:_load_from_state_dictмодуля отбрасывает устаревшие ключи. Проверено: чекпоинт, сохранённый прежним кодом, даёт побитово те же логиты (test_state_dict.py).
18. generate скопирован в шесть моделей — P2
Заголовок раздела «18. generate скопирован в шесть моделей — P2»- Где:
generateвgpt.py,gpt2.py,llama.py,mistral.py,mixtral.py,gemma.py. - Что: логика temperature/top-k/top-p/sampling одинакова, поэтому каждое исправление (пункты 1, 4, 19,
hasattr(torch, "bool")) нужно вносить шесть раз. - Исправление: вынести выбор следующего токена в общую функцию (например,
core/sampling.py) или вBaseModel.generateповерхforward(x, use_cache, cache). - Статус: исправлено в ветке
refactor/shared-generate: одинgenerateвBaseModelповерхself(x, use_cache, cache), копии в шести моделях удалены вместе с ихmax_seq_len(свойство тоже вBaseModel). Выбор токена —sample_next_token, проверки —validate_sampling_args,check_attention_mask(с пункта 56 —check_generation_mask),next_generation_inputвcore/generation.py.
19. Пограничные случаи в generate — P3
Заголовок раздела «19. Пограничные случаи в generate — P3»- Что:
top_k > vocab_sizeпадает вtorch.topk;- нет остановки по
eos_token_id, всегда генерируется ровноmax_new_tokens.
- Воспроизведено:
top_k=100приvocab_size=50—RuntimeError: selected index k out of range. - Исправление:
top_k = min(top_k, vocab_size); параметрeos_token_idс остановкой, когда все последовательности батча его сгенерировали. - Статус: исправлено в ветке
refactor/shared-generate:top_kограничивается размером словаря. Параметрыeos_token_idиpad_token_id: строка, сгенерировавшаяeos_token_id, дальше заполняетсяpad_token_id(по умолчанию тем жеeos_token_id), генерация останавливается, когда закончены все строки — как в HF.
20. head_size без проверки делимости — P3
Заголовок раздела «20. head_size без проверки делимости — P3»- Где:
GPT.__init__иGPT2.__init__,head_size=config["embed_dim"] // config["num_heads"]. - Что: при
embed_dim % num_heads != 0размер головы молча усекается, и внимание работает в пространстве меньшеembed_dim. - Воспроизведено:
embed_dim=30, num_heads=4принимается,head_size=7, Q/K/V — 28 измерений. - Исправление:
assert/ValueErrorв__init__. Связано с тем, что ключhead_sizeв конфигах не читается (см. известные ограничения). - Статус: исправлено в ветке
fix/config-validation(вместе с 28, 29, 36): общаяresolve_head_size(core/config_checks.py) в конструкторе всех шести моделей. Без явногоhead_sizeнеделимыйembed_dimдаётValueErrorс объяснением; в моделях с RoPE нечётныйhead_size— тожеValueErrorс упоминаниемembed_dimи числа голов (вRoPE.__init__assertзаменён наValueError).
21. Документация и мусор — P3
Заголовок раздела «21. Документация и мусор — P3»Пример в докстринге модуля:— исправлен наmodel.generate(input_ids, max_length=30)max_new_tokens/do_sampleвместе с пунктом 18. В докстринге классаmodel(input_ids)по-прежнему описан как возвращающий логиты, а возвращается кортеж.- Неиспользуемые импорты в
gpt2.py:FeedForward,Tensor. - Параметр
ropeвGpt2Decoder(и импортRoPE) — GPT-2 его не использует. - Статус: исправлено в ветке
chore/docs-and-cleanup: примеры в докстрингахGPT2распаковывают кортеж, неиспользуемые импорты удалены, параметрropeи импортRoPEвGpt2Decoderудалены.
Модель: models/llama/llama.py, блок: core/cached_decoder.py, attention: core/multi_head_attention.py с core/rope.py.
Общие с GPT пункты касаются LLaMA так же и здесь не повторяются:
- 1, 2, 3 — генерация за
max_position_embeddings, маска при кэше иattention_mask(исправлены). До исправления: с кэшем падалRuntimeError: shape '[1, 1, 1, <head_size / 2>]' is invalid for input of size 0изRoPE.forward(пустой срез cos/sin), без кэшаValueError; префилл 4 + 6 расходился с полным forward на 0.14–0.28;Llama.forwardне принималattention_mask,generateего игнорировал. В моделях с RoPE контекст нельзя просто обрезать окном и продолжить с кэшем: при обрезке K пересчитываются заново с новыми позициями. - 56 — нет поддержки левого паддинга (исправлен).
- 4, 49 — валидация аргументов
generateи top-p (исправлены). - 8 —
use_cache=Trueпо умолчанию и нетno_grad(исправлен). - 10 — интерфейс
BaseModel(исправлен). - 17 —
_tril_maskвstate_dict(исправлен). - 18 — дублирование
generate(исправлен). - 19 — пограничные случаи
generate:top_kбольше словаря, остановка поeos_token_id(исправлен). - 20 — проверка делимости
embed_dimна число голов (исправлен).
22. generate молча принимает любые именованные аргументы — P2
Заголовок раздела «22. generate молча принимает любые именованные аргументы — P2»- Где:
Llama.generate(..., attention_mask=None, **kwargs). - Что:
**kwargsнигде не используется, поэтому опечатки и аргументы из других API (max_length,eos_token_id) проглатываются без ошибки. - Воспроизведено:
generate(x, 2, do_sample=False, max_lenght=5)выполняется без ошибок. - Исправление:
hf-proxy/src/hf_proxy/hf_adapter.pyпробрасывает**kwargsвmodel.generate, поэтому просто убрать параметр нельзя. Явно перечислить поддерживаемые ключи и бросатьTypeErrorна остальных (или фильтровать ключи в адаптере). - Статус: исправлено в ветке
refactor/shared-generate: у общегоgenerateнет**kwargs, неизвестный именованный аргумент —TypeError.hf_adapter.generateпередаёт свои**kwargsкак есть, поэтому опечатки через адаптер тоже даютTypeError; настоящийtransformers.pipelineлишних ключей не передаёт (проверено).
Отклонения от LLaMA
Заголовок раздела «Отклонения от LLaMA»Докстринг Llama и llama.md уже упоминают bias и dropout; ниже — что из этого следует и чего там нет.
23. SwiGLU с hidden = 4·d вместо ⅔·4·d — P2
Заголовок раздела «23. SwiGLU с hidden = 4·d вместо ⅔·4·d — P2»- Где:
core/swi_glu.py,nn.Linear(emb_size, 4 * emb_size)для_gate,_up,_down. - Что: в LLaMA (разд. 2.2 статьи;
FeedForwardвfacebookresearch/llama/model.py) скрытая размерность —2/3 · 4d, округлённая вверх до кратногоmultiple_of=256, чтобы три матрицы SwiGLU весили столько же, сколько две матрицы обычного FFN с4d. Здесь три матрицы по4d. - Последствия: FFN примерно в 1.5 раза тяжелее, чем в статье. Для
d=4096: hidden 16384 вместо 11008, ~201M вместо ~135M параметров FFN на слой. При сравнении с GPT той же ширины LLaMA получает лишние параметры, и сравнение архитектур становится нечестным. - Исправление: параметр
hidden_dimвSwiGLU(по умолчанию — формула LLaMA, опциональноmultiple_of). Затрагивает Mistral и Mixtral, которые используют тот жеSwiGLU; меняет размеры весов, поэтому старые чекпоинты не загрузятся. - Статус: исправлено для LLaMA в ветке
feat/llama-hf-parity:SwiGLUпринимаетhidden_dim,Llamaчитает ключintermediate_size(по умолчанию прежние4 · embed_dim, старые чекпоинты загружаются). Формула LLaMA —llama_intermediate_size(embed_dim, multiple_of=256, ffn_dim_multiplier=None): 11008 для 7B, 13824 для 13B, 28672 для LLaMA 2 70B. Проверено на весахnickypro/tinyllama-15M/42M/110M(FFN 768, 1376, 2048 — по той же формуле сmultiple_of=32): логиты совпадают с HF до 4e-5, greedy — токен в токен. Для Mistral и Mixtral — в пункте 30.
24. Bias во всех Linear — P3
Заголовок раздела «24. Bias во всех Linear — P3»- Что: в LLaMA все проекции (
wq,wk,wv,wo,w1–w3,output) без bias. Здесь bias есть в Q/K/V, выходной проекции attention, трёх матрицах SwiGLU и голове на словарь. - Воспроизведено:
m._decoders[0]._heads._q.bias is not None,m._linear.bias is not None. - Последствия: веса Meta/HF LLaMA напрямую не загружаются (лишние ключи
*.bias). Для загрузки весов HF, помимо bias, нужна перестановка строкq_proj/k_proj: HF используетrotate_half(половины вектора), а здесь, как у Meta, — чередующиеся пары(2i, 2i+1). - Исправление: флаг
biasв конфиге (по умолчаниюFalseдля LLaMA) с пробросом вMultiHeadAttentionиSwiGLU. - Статус: исправлено для LLaMA в ветке
feat/llama-hf-parity: ключbias(по умолчаниюtrue, прежнее поведение) пробрасывается вMultiHeadAttention,CachedDecoder,SwiGLUи голову. Веса HF загружаются черезconvert_hf_state_dict(models/llama/hf_weights.py) с перестановкой строкq_proj/k_projпод RoPE на чередующихся парах; сверено с пятью моделями (nickypro/tinyllama-*,JackFram/llama-68m/160m): логиты до 1.1e-4, greedy с KV-кэшем совпадает. Для Mistral и Mixtral — в веткеfeat/mistral-mixtral-hf-parity(см. пункт 30).
Качество кода
Заголовок раздела «Качество кода»25. RoPE-буферы в state_dict, по копии на каждый слой — P2
Заголовок раздела «25. RoPE-буферы в state_dict, по копии на каждый слой — P2»- Где:
core/rope.py,register_buffer("cos_matrix", ...)иregister_buffer("sin_matrix", ...). Один объектRoPEзарегистрирован в модели и вMultiHeadAttentionкаждого слоя. - Что: буферы persistent, и
state_dictсодержит их подnum_layers + 1ключами:_position_embeddings.cos_matrix,_decoders.{i}._heads._rope.cos_matrixи т.д. Чекпоинт хранит одни и те же таблицы многократно и привязан кmax_position_embeddings— увеличить контекст без правкиstate_dictнельзя. - Воспроизведено: при
num_layers=2— три ключа*.cos_matrixи три*.sin_matrix. - Исправление:
persistent=False(как в пункте 17). Затрагивает все модели с RoPE: Mistral, Mixtral, Gemma. - Статус: исправлено в ветке
fix/nonpersistent-buffers:cos_matrixиsin_matrixсpersistent=False, вstate_dictих больше нет. Чекпоинт моделей с RoPE теперь загружается и в модель с большимmax_position_embeddings. Старые чекпоинты с этими ключами по-прежнему загружаются и соstrict=True:_load_from_state_dictмодуля отбрасывает устаревшие ключи. Проверено: чекпоинт, сохранённый прежним кодом, даёт побитово те же логиты (test_state_dict.py).
26. Документация и мусор — P3
Заголовок раздела «26. Документация и мусор — P3»- Закомментированный блок вычисления
start_posи строка# pos_out = ...вLlama.forward;неиспользуемая переменная(ушла вместе с копиямиvocab_sizeвgenerategenerate, пункт 18). - Неиспользуемые импорты:
Tensorвllama.py,FeedForwardвcached_decoder.py,Optionalвrope.py,swi_glu.py,rms_norm.py. - Докстринг
CachedDecoderописывает LayerNorm и GELU, хотя для LLaMA блок собирается сRMSNormиSwiGLU. - Комментарий к форме выхода в
RoPE.forward—[batch_size, seq_len, head_size], фактически 4D[batch, num_heads, seq_len, head_size]. - В README.md устарели пометки «⚠️ без GQA, вопреки докстрингу» в таблице и пункт «LLaMA — нет GQA, вопреки докстрингу» в известных ограничениях: докстринг уже исправлен, расхождения больше нет.
Нет— есть вsave/loadни вLlama, ни вBaseModelBaseModel(пункт 35).tests/models/test_llama.pyпроверяет только формы. Кэшированная генерация по одному токену сверяется с полным forward вtest_kv_cache.pyдля всех моделей, там же — префилл кусками и генерация заmax_position_embeddings(пункты 1, 2). Какие токены оставляют top-k/top-p, проверяетtest_generation.py(пункт 49).- Статус: исправлено в ветке
chore/docs-and-cleanup: закомментированный код вLlama.forwardи неиспользуемые импорты удалены, докстрингCachedDecoderописывает подставляемые нормализацию и FFN (для LLaMA — RMSNorm и SwiGLU), комментарий к форме выходаRoPEисправлен, устаревшие пометки о GQA в README.md убраны.
Mistral
Заголовок раздела «Mistral»Модель: models/mistral/mistral.py, блок: core/mistral_decoder.py, attention: core/group_query_attention.py с core/rope.py. GroupedQueryAttention общий с Mixtral, поэтому пункты 27–30 затрагивают и её.
Общие с предыдущими моделями пункты касаются Mistral так же и здесь не повторяются:
- 1, 3 — генерация за
max_position_embeddingsиattention_mask(исправлены). До исправления с кэшем падалRuntimeError: shape '[1, 1, 1, 4]' is invalid for input of size 0изRoPE.forward. Для Mistral это было особенно заметно: sliding window и rolling-buffer кэш позволяют генерировать сколь угодно долго, мешала только таблица cos/sin. Теперьgenerateпродолжает по последнимmax_position_embeddingsтокенам. - 56 — нет поддержки левого паддинга (исправлен).
- 4, 49 — валидация аргументов
generateи top-p (исправлены). - 8 —
use_cache=Trueпо умолчанию и нетno_grad(исправлен). - 10 — интерфейс
BaseModel(исправлен). - 18 — дублирование
generate(исправлен). - 19 — пограничные случаи
generate:top_kбольше словаря, остановка поeos_token_id(исправлен). - 22 —
**kwargsвgenerate(исправлен). - 24 — bias во всех
Linear: у Mistral 7B проекции тоже без bias. До исправления:_heads._q.bias is not None,_linear.bias is not None. Исправлен ключомbias(пункт 30). - 25 — RoPE-буферы в
state_dictпо копии на слой (исправлен).
27. Нет маски при кэше и seq_len > 1 в GroupedQueryAttention — P1
Заголовок раздела «27. Нет маски при кэше и seq_len > 1 в GroupedQueryAttention — P1»- Где:
GroupedQueryAttention.forward,if cache is None: scores = scores.masked_fill(...). - Что: то же, что пункт 2, но в отдельном модуле GQA, и ломается не только causal-часть, но и окно: токены куска видят будущее внутри куска, а ключи из кэша не обрезаются по окну для каждой строки. При одном новом токене маска не нужна: кэш содержит ровно
window_sizeпозиций, плюс сам токен — этоW + 1, как и в маске без кэша. - Воспроизведено: префилл 4 + 6 токенов через кэш расходится с полным forward на 0.2–0.3 по логитам (так же 5 + 5 и 6 + 4) при
window_size=4. Генерация по одному токену с кэшем совпадает с полным forward (3.6e-7). - Исправление: при кэше строить маску по абсолютным позициям: строки
start_pos … start_pos + T − 1, столбцы — позиции ключейstart_pos − len(k_cache) … start_pos + T − 1, разрешено0 ≤ i − j ≤ window_size. - Статус: исправлено в ветке
fix/p1-bugs: маска берётся срезом_tril_mask[start_pos:start_pos + T, start_pos − cache_len:start_pos + T]по абсолютным позициям. Префилл кусками, в том числе длиннее окна, совпадает с полным forward (test_kv_cache.py).
28. Ключ head_size в конфиге игнорируется — P2
Заголовок раздела «28. Ключ head_size в конфиге игнорируется — P2»- Где:
Mistral.__init__,head_size=config["embed_dim"] // config["num_q_heads"]дляRoPEиMistralDecoder. - Что: в
mistral_train.jsonзадан"head_size": 64, и он совпадает с256 // 4случайно. Если изменить одно из значений, второе молча не подстроится. - Воспроизведено: конфиг с
"head_size": 16приembed_dim=32, num_q_heads=4даётhead_size=8. - Исправление: читать
config.get("head_size", embed_dim // num_q_heads)и передавать это значение и вRoPE, и вMistralDecoder. Если размер задан явно,num_q_heads * head_sizeможет не равнятьсяembed_dim— выходная проекция_layerэто уже поддерживает. - Статус: исправлено в ветке
fix/config-validation: все шесть моделей читаютconfig.get("head_size")и передают его и в attention, и вRoPE; без ключа —embed_dim // <число голов>. Еслиhead_sizeзадан,num_heads · head_sizeможет отличаться отembed_dim.
29. Нет проверок num_q_heads и num_kv_heads — P2
Заголовок раздела «29. Нет проверок num_q_heads и num_kv_heads — P2»- Где:
GroupedQueryAttention.__init__,Mistral.__init__. - Что:
num_q_heads % num_kv_heads != 0принимается конструктором и падает только в первомforwardвнутри_repeat_kv_headsс непонятной ошибкойreshape;embed_dim % num_q_heads != 0молча усекает размер голов (как пункт 20).
- Воспроизведено:
num_q_heads=4, num_kv_heads=3—RuntimeError: shape '[1, 4, 10, 8]' is invalid for input of size 240приforward.embed_dim=32, num_q_heads=3— Q-проекция на 30 измерений. - Исправление:
ValueErrorв__init__с понятным сообщением для обоих условий. - Статус: исправлено в ветке
fix/config-validation:GroupedQueryAttention.__init__отклоняетnum_kv_heads < 1иnum_q_heads, не делящееся наnum_kv_heads; неделимыйembed_dimотклоняетresolve_head_size(пункт 20).
Отклонения от Mistral 7B
Заголовок раздела «Отклонения от Mistral 7B»30. Размер скрытого слоя SwiGLU — P3
Заголовок раздела «30. Размер скрытого слоя SwiGLU — P3»- Что: Mistral 7B использует
hidden_dim = 14336приdim = 4096(3.5·d,intermediate_sizeв HF). Здесь4·dв каждой из трёх матриц, то есть FFN примерно на 14% тяжелее. Исправление общее с пунктом 23: параметрhidden_dimвSwiGLU, для Mistral — из конфига. - Статус: исправлено в ветке
feat/mistral-mixtral-hf-parity:MistralиMixtralчитают ключintermediate_size(по умолчанию прежние4 · embed_dim, старые чекпоинты загружаются); у Mixtral он задаёт размер каждого эксперта (MoE(hidden_dim=...)). Вместе с ним ключbias(по умолчаниюtrue) убирает bias из Q/K/V, выхода attention (GroupedQueryAttention(bias=...)), SwiGLU, роутера и головы — это закрывает остаток пункта 24 для Mistral и Mixtral. ВесаMistralForCausalLM/MixtralForCausalLMзагружаются черезconvert_hf_state_dict(общий с LLaMA, K переставляется поnum_key_value_heads); сверено со случайными моделями HF: логиты до 1e-5, greedy с KV-кэшем дольше окна совпадает.
50. eps в RMSNorm зашит как 1e-6 — P3
Заголовок раздела «50. eps в RMSNorm зашит как 1e-6 — P3»- Где:
core/rms_norm.py,RMSNorm(dim, eps=1e-6); все модели и декодеры создаютRMSNormбезeps. - Что: у Mistral 7B
norm_eps = 1e-5(rms_norm_epsв HF), у LLaMA-1 — 1e-6, у Gemma — 1e-6. Задать значение из конфига нельзя. База RoPE 10 000 для LLaMA-1 и Mistral 7B v0.1 совпадает с оригиналом. - Исправление: читать
config.get("rms_norm_eps", 1e-6)и пробрасывать вRMSNormмодели и декодеров. - Статус: исправлено в ветке
feat/rms-norm-eps: LLaMA, Mistral, Mixtral и Gemma читаютconfig["rms_norm_eps"](по умолчанию1e-6) и передают его во все RMSNorm — по две в каждом блоке и финальную (LLaMA — черезfunctools.partial(RMSNorm, eps=...)вCachedDecoder, декодеры Mistral, Mixtral и Gemma — параметромnorm_eps).eps ≤ 0—ValueError. По умолчанию выход побитово прежний;epsне параметр, поэтому формат чекпоинтов не меняется. Конфиги экспериментов не менялись: для Mistral 7B и Mixtral 8x7B оригинальное значение1e-5задаётся в конфиге явно.
51. Dropout в attention и FFN — P3
Заголовок раздела «51. Dropout в attention и FFN — P3»- Что: в Mistral 7B dropout нет (в
mistral-inferenceего нет вовсе, в HFattention_dropout=0.0). Здесь dropout есть вGroupedQueryAttentionи вSwiGLU. Для LLaMA это указано в докстринге и llama.md, для Mistral — нигде. - Исправление: задокументировать в mistral.md или ставить
dropout=0.0по умолчанию. - Статус: сделано в ветке
docs/mistral-gemma-dropout: задокументировано в mistral.md (новый раздел «Отличия от Mistral 7B») и в таблице конфигурации. Значение по умолчанию поменять нельзя:dropout— обязательный ключ конфига. Проверено, чтоdropout: 0обнуляет все пять dropout модели (после эмбеддингов и в attention и SwiGLU каждого блока), так что для соответствия оригиналу достаточно конфига. Dropout в attention у Mistral не на весах внимания, а на выходе — как и был.
Качество кода
Заголовок раздела «Качество кода»31. _tril_mask в state_dict — P2
Заголовок раздела «31. _tril_mask в state_dict — P2»- Где:
GroupedQueryAttention.__init__,register_buffer("_tril_mask", ...). - Что: то же, что пункт 17, но в
GroupedQueryAttention, поэтому исправление вMultiHeadAttentionего не закроет. Маскаmax_seq_len × max_seq_lenхранится в каждом слое и привязывает чекпоинт кmax_seq_lenиwindow_size. - Воспроизведено: ключи
_decoders.{i}._heads._tril_maskвstate_dict. - Исправление:
persistent=False, либо строить маску на лету по позициям (заодно закрывает пункт 27). - Статус: исправлено в ветке
fix/nonpersistent-buffers(как пункт 17).
32. Совместимость с PyTorch < 1.2 сделана наполовину — P3
Заголовок раздела «32. Совместимость с PyTorch < 1.2 сделана наполовину — P3»- Где:
GroupedQueryAttention.__init__(mask.bool() if hasattr(torch, "bool") else mask.byte()),~self._tril_mask[...]вforward, top-k/top-p вMistral.generate,assert x.ndim == 4вRoPE.forward. - Что: на torch ≥ 1.2
hasattr(torch, "bool")всегда истинно, и uint8-ветка никогда не выполняется, то есть не тестируется. На torch < 1.2 она, скорее всего, логически верна: там~надByteTensorбыло логическим НЕ (побитовым стало в 1.2, PyTorch PR #22326), а uint8-маски допустимы вmasked_fillи индексации. На современном torch та же ветка сломалась бы (~для uint8 даёт[254, 255, …]), но выполниться там не может. Вероятнее ломает старый стенд другое: атрибутаTensor.ndimв torch 1.1, по всей видимости, ещё нет (не проверено запуском). Внешний стенд с torch < 1.2 прошла только версия на float-масках с== 0. - Исправление: выбрать одно. Либо перейти на float-маски и
masked_fill(mask == 0, ...)во всём коде и заменитьx.ndimнаx.dim(), либо отказаться от поддержки torch < 1.2 и убрать всеhasattr(torch, "bool")(см. пункт 12). - Статус: выбран второй вариант — поддержка torch < 1.2 в коде библиотеки не заявляется (
pyproject.tomlтребуетtorch>=2.3), все 36 проверокhasattr(torch, "bool")удалены в веткеrefactor/remove-dead-bool-checks. Выходыforward/generateвсех шести моделей побитово совпадают с прежними. Код для стенда со старым torch переносится отдельно и использует float-маски.
33. Документация и мусор — P3
Заголовок раздела «33. Документация и мусор — P3»- Докстринг
Mistral: название статьи выдумано («Mistral: Fast and Efficient Dense and Mixture of Experts Transformer Models»), настоящее — «Mistral 7B». - Докстринг
GroupedQueryAttention: ссылка «Self-attention with linear complexity (Vila et al.) arXiv:2302.05442» не соответствует статье (arXiv:2302.05442 — «Scaling Vision Transformers to 22 Billion Parameters», Dehghani et al.); утверждение, что GQA используется в GPT-4, не подтверждено; обещано требованиеnum_q_heads * head_size == emb_size, которое не проверяется. - Докстринг
MistralDecoderописывает «стек декодеров» с аргументомnum_layers, хотя это один блок и такого аргумента нет; «RMSNorm перед и после» — на деле только pre-norm. - Параметр
maskвGroupedQueryAttention.forwardиMistralDecoder.forwardпринимается и не используется. - Закомментированный код: старый
PositionalEmbeddingsиpos_outвMistral, старый блок кэширования и_repeat_kv_headsвGroupedQueryAttention.forward. - Неиспользуемые импорты:
sqrt,Tensorвmistral.py(vocab_sizeвgenerateушёл вместе с копиямиgenerate, пункт 18) (k_seq_lenвGroupedQueryAttention.forwardудалён вместе с исправлением пункта 27). - Комментарии в
GroupedQueryAttention.forward: сбитая нумерация шагов («Шаг 2», «3.», «5.», «8.», снова «3.», «4.») и неверные размерности (# [B, T, hs]там, где[B, H, T, hs]). - Кэш пересобирается через
torch.catи срез на каждом шаге. Для учебного кода это приемлемо, но настоящего rolling buffer (запись по индексуpos % W) нет, хотя документация так его называет. Нет— есть вsave/loadвMistralBaseModel(пункт 35).tests/models/test_mistral.pyпроверяет только формы; генерация по одному токену с кэшем покрытаtest_kv_cache.py. Там же — префилл кусками и генерация заmax_position_embeddings(пункты 1, 27). Нет тестов на проверки из пункта 29 и на чтениеhead_sizeиз конфига (пункт 28).- Статус: исправлено в ветке
chore/docs-and-cleanup: название статьи Mistral, ссылки и утверждения в докстрингеGroupedQueryAttention(GQA — Ainslie et al., без GPT-4, без требованияnum_q_heads * head_size == emb_size) и докстрингMistralDecoder(один pre-norm блок) исправлены; закомментированный код и неиспользуемые импорты удалены; комментарии вGroupedQueryAttention.forwardперенумерованы, размерности указаны 4D. Документация больше не называет кэш rolling buffer: mistral.md описывает дописывание черезtorch.catи обрезку срезом. Сам кэш не менялся. Неиспользуемые параметрыmaskудалены изforwardвсех модулей attention и декодеров (маска паддинга проверяется вforwardмодели, пункт 3); тесты, которые передавали маску и проверяли только форму, заменены проверками встроенной causal-маски.
Mixtral
Заголовок раздела «Mixtral»Модель: models/mixtral/mixtral.py, блок: core/mixtral_decoder.py, FFN: core/moe.py поверх core/swi_glu.py. Attention — тот же GroupedQueryAttention, что у Mistral.
Сама математика MoE верна: выход совпадает с наивным циклом по токенам (для каждого токена сумма softmax(top-k логитов) · expert(x)) с точностью 7e-8. Это то же, что Softmax(TopK(x·W_g)) в статье и softmax → top-k → перенормировка в HF.
Общие с предыдущими моделями пункты касаются Mixtral так же и здесь не повторяются:
- 1, 3 — генерация за
max_position_embeddingsиattention_mask(исправлены); 56 — нет поддержки левого паддинга (исправлен). - 4, 22 — валидация аргументов и
**kwargsвgenerate(исправлены). - 8 —
use_cache=Trueпо умолчанию и нетno_grad(исправлен). - 10, 18, 19 — интерфейс
BaseModel, дублированиеgenerate, пограничные случаи top-k (исправлены). - 23, 24 — SwiGLU с
4·dи bias во всехLinear. У Mixtral 8x7B эксперт —hidden_dim = 14336приdim = 4096, все проекции, включая роутер, без bias (исправлены ключамиintermediate_sizeиbias, пункт 30). - 25 — RoPE-буферы в
state_dictпо копии на слой (исправлен). - 27 — нет маски при кэше и
seq_len > 1(исправлен). До исправления префилл 6 + 8 токенов через кэш расходился с полным forward на 0.27–0.35 по логитам приwindow_size=5. - 49 — top-p отбрасывает пограничный токен (исправлен).
- 50, 51 —
epsRMSNorm из конфига (исправлен, ключrms_norm_eps) и dropout, которого в Mixtral 8x7B нет (задокументирован,dropout: 0убирает его). - 28 — ключ
head_sizeигнорировался (исправлен). - 29 — проверки голов (исправлен).
- 33 — мусор в
GroupedQueryAttention(исправлен). - 31, 32 —
_tril_maskвstate_dict, совместимость с torch < 1.2 (исправлены).
34. MoE падает в bf16/fp16 — P1
Заголовок раздела «34. MoE падает в bf16/fp16 — P1»- Где:
MoE.forward,weights_for_expert = torch.zeros(batch_size, seq_len, device=x.device). - Что: буфер весов создаётся без
dtypeи всегда float32. Запись в негоtopk_weights[...]в bf16/fp16 падает; обучение и инференс Mixtral в половинной точности невозможны. - Воспроизведено:
MoE(16, 4, 2).to(torch.bfloat16)на bf16-входе —RuntimeError: Index put requires the source and destination dtypes match, got Float for the destination and BFloat16 for the source. - Исправление:
dtype=x.dtype, либо переписать сборку выхода без промежуточного буфера (см. пункт 39). - Статус: исправлено в ветке
fix/p1-bugs(dtype=x.dtype); переписывание сборки выхода (пункт 39) не делалось. Тест: MoE в bf16/fp16 совпадает с float32 (test_moe.py).
35. Нет save/load, хотя докстринг их обещает — P2
Заголовок раздела «35. Нет save/load, хотя докстринг их обещает — P2»- Где: докстринг
Mixtral: «save(path)/load(path, device) — сохранение и восстановление обученной модели». - Что: методов нет ни в
Mixtral, ни вBaseModel. - Воспроизведено:
hasattr(Mixtral, "save"),hasattr(Mixtral, "load")—False. - Исправление: реализовать в
BaseModel(state_dict+config,loadкакclassmethod) — закроет и Mistral, и LLaMA. Версия для внешнего стенда уже содержит рабочий вариант с полным набором аргументов конструктора. - Статус: исправлено в ветке
feat/save-loadдля всех шести моделей:model.save(path)пишет один файл с классом модели, конфигом иstate_dict(без вычисляемых буферов, пункты 17, 25),Model.load(path, device)—classmethod, создаёт модель по сохранённому конфигу и возвращает её в режимеeval. Файл читается сweights_only=True; файл другой модели или голыйstate_dictдаютValueError.
36. top_k_experts=0 принимается — P3
Заголовок раздела «36. top_k_experts=0 принимается — P3»- Где:
MoE.__init__проверяет толькоtop_k_experts > num_experts. - Что: при
top_k_experts=0ни один эксперт не выбирается, FFN-ветка тождественно возвращает нули, модель молча превращается в attention-only. Отрицательное значение (top_k_experts=-1) конструктор тоже принимает. - Воспроизведено:
Mixtralсtop_k_experts=0строится и выполняетforwardбез ошибок, выход MoE ровно 0. - Исправление:
ValueErrorприtop_k_experts < 1. - Статус: исправлено в ветке
fix/config-validation:MoE.__init__требует1 ≤ top_k_experts ≤ num_expertsиnum_experts ≥ 1.
Отклонения от Mixtral 8x7B
Заголовок раздела «Отклонения от Mixtral 8x7B»37. Нет load-balancing loss у роутера — P2
Заголовок раздела «37. Нет load-balancing loss у роутера — P2»- Где:
MoE.forwardвозвращает только выход; логиты роутера наружу не отдаются,Trainerсчитает только cross-entropy. - Что: статья Mixtral вспомогательный loss не описывает, но HF-реализация (
load_balancing_loss_funcвmodeling_mixtral.py), как и Switch Transformer и GShard, добавляет при обученииnum_experts · Σ fᵢ · Pᵢ(доля токенов на эксперта × средняя вероятность роутера). Без него роутер склонен схлопываться на пару экспертов, остальные не обучаются, и MoE вырождается в узкий dense FFN. - Исправление: возвращать из
MoE(или копить в атрибуте)router_logits, считать aux loss в модели с коэффициентом из конфига (router_aux_loss_coef, в HFMixtralConfigпо умолчанию 0.001) и прибавлять вTrainer. Полезна и метрика загрузки экспертов в логах обучения. - Статус: исправлено в ветке
feat/moe-load-balancing-loss:MoEзапоминает логиты роутера,load_balancing_loss(core/moe.py) считает формулу HF по всем слоям с учётом маски паддинга (совпадает сload_balancing_loss_funcизtransformers4.57 до float, с маской и без),Mixtral.auxiliary_loss()умножает её наrouter_aux_loss_coef,TrainerиHFGPTAdapterприбавляют её к loss при обучении.BaseModel.auxiliary_loss()по умолчаниюNone. Коэффициент по умолчанию0— выключено, как и в HF (output_router_logits=False); обучение с текущими конфигами не меняется. На игрушечной задаче (8 экспертов, 300 шагов) с коэффициентом 0.02 доли загрузки экспертов — 0.10–0.15 вместо 0.06–0.18 без него, LM loss тот же. Метрика загрузки экспертов в логахTrainerне добавлялась.
38. Двойной dropout в MoE — P2
Заголовок раздела «38. Двойной dropout в MoE — P2»- Где:
nn.Dropoutвнутри каждогоSwiGLUи ещё один на выходеMoE. - Что: выход эксперта прорежается дважды, и эффективная вероятность выше заданной
dropout. В Mixtral dropout в FFN нет вовсе. - Воспроизведено: при
dropout=0.5вtrain()обнуляется 62% элементов выхода MoE вместо 50% (0.5 + 0.5 · 0.5²для двух экспертов). - Исправление: оставить один dropout — на выходе
MoE— и создавать экспертов сdropout=0.0(или добавить вSwiGLUфлаг). - Статус: исправлено в ветке
fix/moe-double-dropout: эксперты создаются сdropout=0.0, единственный dropout — на выходеMoE. Приdropout=0.5вtrain()обнуляется 50.0% элементов выхода вместо 62.4%. Меняется только обучение: вeval()dropout не действует, выход побитово прежний; параметров у dropout нет, формат чекпоинтов не меняется. То, что в Mixtral dropout в FFN нет совсем, по-прежнему не учтено — это общее отличие dropout от оригиналов (пункты 51, 55).
52. Sliding window attention, которого нет в Mixtral 8x7B — P2
Заголовок раздела «52. Sliding window attention, которого нет в Mixtral 8x7B — P2»- Где:
Mixtral.__init__передаётwindow_size=config["window_size"]вGroupedQueryAttention. - Что: Mixtral 8x7B использует плотное внимание на весь контекст 32k («fully dense context length of 32k tokens» в статье;
sliding_window=Noneв HFMixtralConfig). SWA — черта Mistral 7B v0.1, в Mixtral её нет. Здесь окно действует всегда. - Исправление: сделать
window_sizeнеобязательным (None— без окна) и по умолчанию для Mixtral не задавать; убрать ключ изmixtral_train.json. - Статус: исправлено в ветке
feat/mistral-mixtral-hf-parity:window_sizeнеобязателен вGroupedQueryAttention,MistralDecoder,MixtralDecoder,MistralиMixtral;None(ключа нет) — обычная causal-маска и кэш без обрезки. Ключ убран изmixtral_train.json. Конфиги сwindow_sizeработают как раньше. Сверено со случайнойMixtralForCausalLM(sliding_window=None) и сMistralForCausalLMс окном и без; попутно тестом подтверждено, что окно здесь на позицию шире HF:window_size = sliding_window − 1.
53. База RoPE 10 000 вместо 1 000 000 — P3
Заголовок раздела «53. База RoPE 10 000 вместо 1 000 000 — P3»- Где:
RoPE(head_size, max_seq_len, base=10_000); ни одна модель не передаётbase. - Что: у Mixtral 8x7B
rope_theta = 1e6(HFMixtralConfig), чтобы покрыть контекст 32k. Для LLaMA-1, Mistral 7B v0.1 и Gemma 10 000 верно. - Исправление: читать
config.get("rope_theta", 10_000)и передавать вRoPE; для Mixtral задать 1e6 в конфигах. - Статус: исправлено в ветке
feat/rope-theta: LLaMA, Mistral, Mixtral и Gemma читаютconfig["rope_theta"](по умолчанию10000) и передают его базой вRoPE— один объект на модель, общий для всех слоёв attention.RoPEотклоняет базу≤ 1. По умолчанию выход побитово прежний; таблицы cos/sin не сохраняются в чекпоинт (пункт 25), поэтому формат не меняется. Конфиги экспериментов не менялись: для Mixtral 8x7B значение1e6задаётся в конфиге явно (какrms_norm_epsв пункте 50).
54. Softmax роутера в dtype входа — P3
Заголовок раздела «54. Softmax роутера в dtype входа — P3»- Где:
MoE.forward, softmax по top-k логитам роутера. - Что предполагалось: HF считает
softmax(router_logits, dtype=torch.float), эталонный код Mistral —softmax(weights, dtype=torch.float).to(inputs.dtype); здесь softmax вызывался в dtype входа, и в bf16 веса экспертов должны были терять точность. - Проверено: предположение не подтвердилось. Встроенный
F.softmaxPyTorch для bf16/fp16 и так накапливает во float32, а остаток ошибки — финальное округление весов к dtype входа, которое делают и HF, и эталон. Веса в dtype входа и через float32: bf16 — совпали все 1.6 млн на CPU и 0.8 млн на MPS; fp16 — различаются 2 из 1.6 млн на CPU, совпали все на MPS; максимальное отличие от весов во float32 одинаково (bf16 — 1.95e-3, fp16 — 2.4e-4). На CUDA не проверялось; softmax там тоже накапливает во float32. - Исправление:
F.softmax(topk_logits.float(), dim=-1).to(x.dtype)— для явности и совпадения с эталоном, а не ради точности: результат не зависит от того, как softmax реализован на конкретном backend. - Статус: сделано в ветке
fix/router-softmax-fp32. На CPU и MPS выход MoE не меняется.
39. Неэффективная сборка выхода MoE — P3
Заголовок раздела «39. Неэффективная сборка выхода MoE — P3»- Где:
MoE.forward. - Что: на каждого эксперта создаётся полный буфер
[batch, seq_len]и выполняется вложенный цикл поtop_k, токены выбираются масками сравнения. Работает, но делает лишнюю работу. (На torch < 1.2 сравнения дают uint8, и индексация uint8-маской там допустима, так что несовместимости, скорее всего, нет; запуском не проверено.) - Исправление: плоский вход
[N, emb],(topk_indices == e).nonzero()даёт пары (токен, позиция в top-k), веса —topk_weights[token_idx, k_idx], выход —output.index_add_(0, token_idx, w · expert(x[token_idx])). Так же устроенMixtralExperts.forwardв HF. Этот вариант уже проверен во внешнем стенде и заодно закрывает пункт 34. - Статус: исправлено в ветке
perf/moe-index-add: плоский вход[N, emb], пары (токен, позиция в top-k) черезtorch.where, сборка черезindex_add_; полный буфер весов и вложенный цикл поtop_kубраны. Выход побитово совпадает с прежним (float32 и bf16, в том числе при обучении с dropout); на 4096 токенах — 23.6 → 20.6 мс при 8 экспертах, 21.9 → 11.1 мс при 64.
40. Документация и мусор — P3
Заголовок раздела «40. Документация и мусор — P3»- В References докстрингов нет самой статьи Mixtral — «Mixtral of Experts», Jiang et al., 2024, arXiv:2401.04088; есть только пост в блоге (mixtral.md статью цитирует).
- Ссылка на GQA в
mixtral_decoder.pyиmixtral.py—arXiv:2305.14236, правильноarXiv:2305.13245(Ainslie et al.). - Докстринг
MoEописывает роутер какsoftmax(W_r x), затем top-K — то есть вероятности без перенормировки. Код делает наоборот: top-K по логитам, затем softmax (что и верно, см. введение раздела). - Неиспользуемые импорты:
Tensor,sqrtвmixtral.py;Fвmixtral_decoder.py. ПараметрmaskвMixtralDecoder.forwardпередаётся вGroupedQueryAttention, где игнорируется. - Роутер создаётся с bias; в Mixtral
gate—Linear(dim, num_experts, bias=False)(частный случай пункта 24). - Тесты:
test_moe.pyпроверяет формы, градиенты и детерминизм, но не корректность против эталона; тест на bf16/fp16 добавлен с исправлением пункта 34, префилл кусками и генерация заmax_position_embeddings— вtest_kv_cache.py. Вtest_mixtral.pyтолько формы; генерация по одному токену с кэшем покрытаtest_kv_cache.py. - Статус: исправлено в ветке
chore/docs-and-cleanup: статья Mixtral добавлена в ReferencesMixtralиMixtralDecoder, ссылка на GQA исправлена, формула роутера в докстрингеMoEсоответствует коду, неиспользуемые импорты удалены; добавлен тестMoEпротив наивного цикла по токенам. Bias роутера остаётся частью пункта 24.
Модель: models/gemma/gemma.py, блок: core/gemma_decoder.py, attention: core/group_query_attention.py с core/rope.py, FFN: core/geglu.py. До пункта 45 блок Gemma был построен на отдельном модуле core/multi_query_attention.py, поэтому пункты 41 и 47 касаются его; теперь он остался только учебным модулем.
Кэшированная генерация по одному токену совпадает с полным forward (покрыто test_kv_cache.py).
Общие с предыдущими моделями пункты касаются Gemma так же и здесь не повторяются:
- 1, 3 — генерация за
max_position_embeddingsиattention_mask(исправлены). До исправленияGemma.forwardпропускал проверку длины при кэше, аMultiQueryAttentionсравнивал с лимитом толькоseq_len, безstart_pos. Неиспользуемые параметрыmaskвGemmaDecoder.forwardиMultiQueryAttention.forwardудалены (пункт 48):attention_maskпроверяется вGemma.forward. - 56 — нет поддержки левого паддинга (исправлен).
- 4, 49 — валидация аргументов
generateи top-p (исправлены). - 8 —
use_cache=Trueпо умолчанию и нетno_grad(исправлен). - 10, 18 — интерфейс
BaseModel, дублированиеgenerate(исправлены). - 19 — пограничные случаи
generate:top_kбольше словаря, остановка поeos_token_id(исправлен). - 20 — проверка делимости
embed_dimна число голов (исправлен). - 22 —
**kwargsвgenerate(исправлен). - 25 — RoPE-буферы в
state_dictпо копии на слой (исправлен). - 28 — ключ
head_sizeигнорировался (исправлен). - 32 — половинчатая совместимость с torch < 1.2 в top-k/top-p
generateи в_tril_mask(исправлен: проверкиhasattr(torch, "bool")удалены). Версия Gemma на float-масках с== 0прошла внешний стенд 2026-09-28. - 35 —
save/load, обещанные докстрингом (исправлен).
41. Нет causal-маски при кэше и seq_len > 1 в MultiQueryAttention — P1
Заголовок раздела «41. Нет causal-маски при кэше и seq_len > 1 в MultiQueryAttention — P1»- Где:
MultiQueryAttention.forward,if cache is None: scores = scores.masked_fill(...). - Что: то же, что пункты 2 и 27, но в третьем модуле attention. Токены куска, поданного вместе с кэшем, видят будущее внутри куска.
generateподаёт по одному токену, поэтому там не проявляется. - Воспроизведено: префилл 4 + 6 токенов через кэш расходится с полным forward на 0.14–0.18 по логитам.
- Исправление: всегда накладывать маску со сдвигом
self._tril_mask[start_pos:start_pos + seq_len, :start_pos + seq_len]и проверятьstart_pos + seq_len <= max_seq_len(закрывает часть пункта 1). Проверено в версии для внешнего стенда: префилл кусками совпадает с полным forward. - Статус: исправлено в ветке
fix/p1-bugs.
Отклонения от Gemma
Заголовок раздела «Отклонения от Gemma»Сравнение с Gemma 2B/7B (Gemma Team, 2024; GemmaConfig/GemmaModel в HF).
42. Эмбеддинги не масштабируются на √d — P2
Заголовок раздела «42. Эмбеддинги не масштабируются на √d — P2»- Что: в Gemma выход
embed_tokensумножается наsqrt(hidden_size)перед первым блоком (вgemma_pytorchи старых версиях HF —normalizerвGemmaModel.forward, в текущем HF —GemmaTextScaledWordEmbeddingс буферомembed_scale). Здесь эмбеддинги идут в декодер как есть. - Последствия: при tied embeddings (пункт 43) без масштабирования вход в первый блок на порядок меньше по норме, чем предполагает архитектура. Веса Gemma дают неверный результат даже при совпадении остальных слоёв.
- Исправление:
out = tok_out * math.sqrt(embed_dim)вGemma.forward(в HF константа приводится к dtype эмбеддингов). - Статус: исправлено в ветке
feat/gemma-hf-parity: ключscale_embeddings(по умолчаниюfalse) умножает выход эмбеддингов на√embed_dim, множитель приводится к dtype эмбеддингов, как в HF. Без него сверка с HF не проходит — это проверяет тест.
43. Нет weight tying, bias во всех Linear — P2
Заголовок раздела «43. Нет weight tying, bias во всех Linear — P2»- Что: в Gemma выходная проекция привязана к
embed_tokens(tie_word_embeddings=True), и все проекции без bias (attention_bias=False). Здесь_linear— отдельныйnn.Linearс bias, bias есть в Q/K/V, выходной проекции attention и трёх матрицах GeGLU. - Воспроизведено:
m._linear.bias is not None,m._decoders[0]._heads._q.bias is not None,m._linear.weight is not m._token_embeddings._embedding.weight. - Последствия: у Gemma словарь 256 000 токенов, поэтому отдельная голова — это лишние ~524M параметров для 2B (
256000 × 2048), то есть около пятой части модели (~21% от 2.5B). - Исправление: как в пунктах 5 и 24:
bias=Falseпод флагом конфига,_linear.weight = _token_embeddings._embedding.weight. - Статус: исправлено в ветке
feat/gemma-hf-parity: ключtie_word_embeddings(голова без bias делит матрицу с эмбеддингами —output_projection, как в пункте 5) иbias(Q/K/V, выход attention, GeGLU и голова). По умолчанию прежняя структура, старые чекпоинты загружаются.
44. GeGLU с hidden = 4·d вместо 8·d — P2
Заголовок раздела «44. GeGLU с hidden = 4·d вместо 8·d — P2»- Где:
core/geglu.py,nn.Linear(emb_size, 4 * emb_size)для_gate,_up,_down. - Что: в Gemma
intermediate_size= 16384 приhidden_size= 2048 (2B) и 24576 при 3072 (7B), то есть 8·d на каждую из матрицgate_projиup_proj. (В табл. 1 статьи «feedforward hidden dims» 32768 / 49152 — это сумма gate + up.) Здесь 4·d — FFN вдвое уже, чем в статье. Сама активация — tanh-GELU — совпадает сgelu_pytorch_tanhв HF. В отличие от пункта 23 (LLaMA), здесь FFN не тяжелее, а легче оригинала. - Исправление: параметр
hidden_dimвGeGLUс чтением из конфига, как предложено дляSwiGLUв пункте 23. - Статус: исправлено в ветке
feat/gemma-hf-parity:GeGLUпринимаетhidden_dimиbias,Gemmaчитает ключintermediate_size(по умолчанию4 · embed_dim; у Gemma —8 · embed_dim).
45. Нельзя выразить Gemma 7B: MQA всегда, head_size = d / heads — P2
Заголовок раздела «45. Нельзя выразить Gemma 7B: MQA всегда, head_size = d / heads — P2»- Что: MQA (одна K/V-голова) используется только в Gemma 2B. Gemma 7B — обычный MHA с 16 головами, и
head_dim = 256не равенhidden_size / num_heads(16 × 256 = 4096 ≠ 3072). ЗдесьMultiQueryAttentionвсегда с одной K/V-головой, аhead_sizeвсегдаembed_dim // num_q_heads(пункт 28). - Исправление: заменить
MultiQueryAttentionнаGroupedQueryAttentionсnum_kv_headsиз конфига (MQA — частный случайnum_kv_heads=1) и читатьhead_sizeиз конфига._layerуже умеет проецироватьnum_q_heads * head_size ≠ embed_dimобратно вembed_dim. Тогда же уйдёт отдельный модуль MQA и пункты 41 и 47 закроются вместе с 27 и 31. - Статус: исправлено в ветке
feat/gemma-hf-parity:GemmaDecoderпостроен наGroupedQueryAttentionбез окна сnum_kv_headsиз конфига (по умолчанию1— MQA). При одной K/V-голове GQA транслирует её, а не копирует, поэтому результат побитово совпадает с прежнимMultiQueryAttention, включая шаги с кэшем; кэш слоя стал(K, V, next_pos).MultiQueryAttentionостался вllm.coreкак учебный модуль. Вместе сhead_size(пункт 28) Gemma 7B —num_kv_heads: 16,head_size: 256приembed_dim: 3072— выразима.
46. RMSNorm без (1 + w) и вычислений во float32 — P3
Заголовок раздела «46. RMSNorm без (1 + w) и вычислений во float32 — P3»- Где:
core/rms_norm.py. - Что: в Gemma
GemmaRMSNormхранит вес, инициализированный нулями, и умножает на(1 + weight), а нормализацию считает во float32 и приводит результат обратно. Здесь вес инициализирован единицами и умножается напрямую, вычисления в dtype входа. При обучении с нуля параметризации эквивалентны, но веса Gemma без поправки+1не загрузятся корректно, а в bf16 нормализация менее точна. - Исправление: для загрузки весов — прибавлять 1 при конвертации. Для bf16 —
x.float()внутриforwardи.to(x.dtype)на выходе (затрагивает LLaMA, Mistral, Mixtral). - Замечание по загрузке весов HF в целом: помимо пунктов 42–46 нужна перестановка строк
q_proj/k_proj— HF Gemma используетrotate_half, здесь чередующиеся пары (как в пункте 24). - Статус: исправлено в ветке
feat/gemma-hf-parity:RMSNormдля float16/bfloat16 считает нормализацию во float32 и приводит к dtype входа перед умножением на вес, какLlamaRMSNorm(затрагивает LLaMA, Mistral, Mixtral и Gemma; во float32 побитово прежний результат). Параметризация(1 + w)не добавлялась:convert_hf_state_dictизllm.models.gemmaприбавляет 1 к весам RMSNorm. ВесаGemmaForCausalLMзагружаются; сверено со случайными моделями HF в форме 2B (MQA) и 7B (MHA,head_dim≠hidden / heads): логиты до ~1e-5, greedy с KV-кэшем совпадает. Остаётся отличие в bf16 в последних битах:GemmaRMSNormумножает на вес ещё во float32.
55. Dropout на эмбеддингах, в attention и GeGLU — P3
Заголовок раздела «55. Dropout на эмбеддингах, в attention и GeGLU — P3»- Что: в Gemma dropout нет (
attention_dropout=0.0в HF, вgemma_pytorchего нет). Здесь dropout стоит после эмбеддингов (Gemma.forward), вMultiQueryAttentionи вGeGLU. - Исправление: ставить
dropout=0.0по умолчанию; то же для Mistral (пункт 51). - Статус: сделано в ветке
docs/mistral-gemma-dropout: в gemma.md и таблице конфигурации указано, что dropout в оригинале нет иdropout: 0убирает его полностью (проверено: все пять dropout модели получаютp = 0). Значения по умолчанию нет:dropout— обязательный ключ конфига.
Качество кода
Заголовок раздела «Качество кода»47. _tril_mask в state_dict — P2
Заголовок раздела «47. _tril_mask в state_dict — P2»- Где:
MultiQueryAttention.__init__,register_buffer("_tril_mask", ...). - Что: то же, что пункты 17 и 31, но в
MultiQueryAttention, поэтому их исправление его не закроет. - Воспроизведено: ключи
_decoders.{i}._heads._tril_maskвstate_dict. - Исправление:
persistent=False. - Статус: исправлено в ветке
fix/nonpersistent-buffers(как пункт 17).
48. Документация и мусор — P3
Заголовок раздела «48. Документация и мусор — P3»- Докстринги
GemmaиGemmaDecoderописывают несуществующие варианты: «Multi-Query либо Grouped heads», «FFN с GeGLU/SwiGLU», «RMSNorm или LayerNorm»; псевдокод вGemmaDecoderиспользуетLayerNorm. В коде всегда MQA + GeGLU + RMSNorm. - Неверная ссылка на статью Gemma в докстрингах
Gemma,Gemma.generateиGemmaDecoder:arXiv:2403.07794, правильноarXiv:2403.08295. - Неиспользуемые импорты:
math,sqrt,Tensorвgemma.py;Fвgemma_decoder.py. - Комментарии в
MultiQueryAttention.forward: сбитая нумерация шагов («Шаг 2», «3.», «5.», снова «3.», «4.») и неверные размерности (# [B, T, hs]там, где[B, H, T, hs]). gemma_train.jsonсодержит ключи Mixtral (num_kv_heads,num_experts,top_k_experts,window_size), которые модель не читает. Ключи убраны из JSON.- Тесты:
test_gemma.pyпроверяет только формы.— заменён проверкой встроенной causal-маски. Префилл кусками и генерация заtest_forward_maskedвtest_gemma_decoder.pyсоздавал впечатление, что блок применяет маскуmax_position_embeddingsпокрытыtest_kv_cache.py. - Статус: исправлено в ветке
chore/docs-and-cleanup: докстрингиGemmaиGemmaDecoderописывают фактическую схему (MQA + GeGLU + RMSNorm), ссылка на статью Gemma исправлена, неиспользуемые импорты удалены, комментарии вMultiQueryAttention.forwardперенумерованы. Ключи Mixtral убраны изgemma_train.json, раздел о неиспользуемых ключах в gemma.md удалён. Неиспользуемые параметрыmaskудалены изforwardвсех модулей attention и декодеров (маска паддинга проверяется вforwardмодели, пункт 3); тесты, которые передавали маску и проверяли только форму, заменены проверками встроенной causal-маски.
Токенизатор, данные и обучение
Заголовок раздела «Токенизатор, данные и обучение»Пункты 57–62 найдены при написании глав tokenization.md и training.md учебного пособия (2026-09-29) и проверены запуском.
57. Паддинг входит в loss — P1
Заголовок раздела «57. Паддинг входит в loss — P1»- Где:
TextDataset,TextWithSpecialTokensDataset,StreamingTextDataset(llm/src/llm/datasets/) иTrainer.compute_lm_loss. - Что: датасеты дополняют последовательность до
block_sizeтокеномpad_token_idи возвращаютlabels = input_ids.clone(), то есть pad-позиции остаются в метках.F.cross_entropy(..., ignore_index=-100)вTrainerих не отбрасывает, хотя комментарий обещает, что «padding токены не участвуют в loss». Метку-100ставит только коллатор hf-proxy. - Воспроизведено: на учебном корпусе
experiments/llm_onlyприblock_size = 128около 94 % целей — предсказание pad после pad. Валидационный loss без маски — 1.23, с метками-100на паддинге — 5.92 приln V = 6.18(токенизатор, обученный на всех строках корпуса, V = 484): модель в основном учится повторять pad, а заниженный loss это скрывает. - Исправление: в
labelsзаменять паддинг на-100(labels[attention_mask == 0] = -100или прямо при дополнении). Заодно возвращатьattention_maskиз датасетов. - Статус: исправлено в ветке
fix/pad-in-loss: все три датасета собирают пример функциейlm_example(datasets/lm_example.py) и возвращаютinput_ids,attention_maskиlabelsс-100на паддинге. Паддинг определяется по месту, а не по значению токена:pad_token_idможет совпадать с настоящим токеном (0 по умолчанию, pad = EOS), и такие токены остаются в loss.Trainerпередаётattention_maskиз батча в модель (у Mixtral паддинг больше не входит в статистику роутера), а на батче без единой целиcompute_lm_lossвозвращает 0 вместо NaN. Тесты: маска и метки у всех датасетов, в том числе при pad = настоящему токену и pad = EOS; lossTrainerна GPT не зависит отblock_size; маска доходит до Mixtral.
58. BPETokenizer.encode не применяет слияния по порядку — P2
Заголовок раздела «58. BPETokenizer.encode не применяет слияния по порядку — P2»- Где:
BPETokenizer.encode(llm/src/llm/tokenizers/bpe_tokenizer.py). - Что: слово кодируется жадным поиском самого длинного токена из словаря, начиная с текущего символа (как WordPiece), а список
mergesпри кодировании не используется. BPE (Sennrich et al., 2016) кодирует новое слово, применяя выученные слияния в порядке их появления. Результаты расходятся, а поиск перебирает весьvocab_listна каждой позиции — O(n·V). - Воспроизведено: на корпусе из статьи Sennrich (low×5, lower×2, newest×6, widest×3) слово
nestпо слияниям кодируется какn est, здесь —ne s t. - Исправление: кодировать слово применением
mergesпо рангу (как в tokenization.md); жадный поиск оставить разве что как отдельный режим. - Статус: исправлено в ветке
fix/bpe-merges-encode:encodeразбивает слово методом_bpe_word— слияния по порядку ранга, какbpe()в GPT-2; разбиение слова кэшируется на время вызова. Жадный longest-match (_greedy_word) остался только для токенизатора без слияний (старые файлыsaveиsave_pretrainedhf-proxy без поляmerges): по словарю порядок слияний не восстановить.HFTokenizerAdapter.save_pretrainedтеперь сохраняетmerges,from_pretrainedих загружает — раньше загруженный адаптер кодировал бы по одному словарю. Тесты:nest→n est, слова корпуса разбиваются как при обучении, совпадение с BPE из HuggingFacetokenizersна всех словах длины 1–4 из алфавита примера Sennrich и 2 000 случайных словах длины 5–9, сохранение слияний в hf-proxy. На учебном корпусеexperiments(словарь 1000) оба способа кодируют все слова обучающей и валидационной выборок и тестовые промпты одинаково: каждое слово слилось в один токен.
59. Неизвестный символ без <unk> даёт None — P2
Заголовок раздела «59. Неизвестный символ без <unk> даёт None — P2»- Где:
BPETokenizer.encode,decode. - Что: если
<unk>не передан вspecial_tokensпри обучении,unk_token_idравенNone, иencodeвозвращаетNoneна месте неизвестного символа — модель упадёт на таком входе. Если<unk>есть,decodeпо умолчанию молча выбрасывает его. В учебных конфигахtest_promptsчастично английские при русском корпусеTRAIN_TEXTSи кодируются почти целиком в<unk>. - Исправление: всегда добавлять
<unk>в словарь (или бросатьValueErrorна неизвестный символ); промпты конфигов привести к языку корпуса. - Статус: исправлено в ветке
fix/bpe-unknown-symbolвторым способом: без<unk>в словареencodeбросаетValueErrorсо списком неизвестных символов. Словарь и id не меняются, поэтому обученные модели и сохранённые токенизаторы не затронуты.test_promptsвсех конфиговexperiments/llm_onlyзаменены русскими фразами из символов обучающей части корпуса; в них были и английские промпты, и символы, которых нет в корпусе, в русских (—,-,2). ВTEST_PROMPTSизexperiments/shared/configs.py(промпты HF-экспериментов)"Программирование"с кириллическойП, которой нет в корпусе, заменено на"Мир".decodeпо-прежнему выбрасывает<unk>приskip_special_tokens=True— как HuggingFace; это описано в tokenization.md.
60. Warmup длиннее всего обучения в учебных конфигах — P3
Заголовок раздела «60. Warmup длиннее всего обучения в учебных конфигах — P3»- Где:
experiments/llm_only/configs/*_train.json:warmup_steps = 50приnum_epochs = 3. - Что: на учебном корпусе за три эпохи получается около 18 шагов, и learning rate не поднимается выше ≈ 0.34 от заданного — всё обучение проходит внутри warmup. Первый шаг
LambdaLRделается сlr = 0. - Воспроизведено: средний loss по эпохам с
warmup_steps = 5— 5.34 → 1.33, с 50 — 6.07 → 3.74 (loss с учётом паддинга, пункт 57). - Исправление: задавать warmup долей от числа шагов (например, 5–10 %) или уменьшить значение в конфигах.
- Статус: исправлено в ветке
fix/warmup-steps:Trainerпринимаетwarmup_ratio— долю warmup от числа шагов, , как в HFTrainingArguments(вместе сwarmup_steps—ValueError; без обоих — 100 шагов, как раньше), и предупреждает, если warmup не короче всего обучения. Учебные конфигиexperiments/llm_onlyиTRAINING_CONFIGвexperiments/shared/configs.py(там была та же ошибка, его используетtrain_with_hf_trainer.py) переведены наwarmup_ratio = 0.1: 2 шага warmup из 18, learning rate доходит до заданного. Прогон GPT с паддингом, исключённым из loss (пункт 57): средний loss эпох 6.08 → 5.98 → 5.79 сwarmup_steps = 50и 6.04 → 5.27 → 4.84 сwarmup_ratio = 0.1. Первый шаг сlr = 0оставлен: так же устроен линейный warmup в HF.
61. get_optimizer: weight decay на всех параметрах — P3
Заголовок раздела «61. get_optimizer: weight decay на всех параметрах — P3»- Где:
get_optimizer(llm/src/llm/training/optimizer.py). - Что:
AdamW(model.parameters(), weight_decay=0.01)затухает все параметры, включая bias, веса нормализаций и эмбеддинги; обычно (GPT-2, LLaMA, HFTrainer) их исключают. Вариант"adam"— L2-регуляризация, а не decoupled decay; в варианте"sgd"weight_decayигнорируется. - Исправление: две группы параметров — с decay для матриц
Linearи без decay для bias, норм и эмбеддингов; описать поведение"adam"и"sgd"в докстринге. - Статус: исправлено в ветке
fix/weight-decay-groups:weight_decay_param_groupsделит параметры на матрицы (dim ≥ 2: весаLinear, в том числе роутер и эксперты MoE, и эмбеддинги) сweight_decayи остальное (bias, веса LayerNorm и RMSNorm) без него;get_optimizerпередаёт эти группы всем трём оптимизаторам. Эмбеддинги, в отличие от предложенного выше, затухают — как в GPT-1 («all non bias or gain weights», разд. 4.1), nanoGPT и HFTrainer, который исключает только bias и веса нормализаций; общая матрица при weight tying входит в группы один раз."sgd"теперь используетweight_decay(раньше молча игнорировал); поведение"adam"(L2) и"sgd"описано в докстринге. Тесты: состав групп для всех трёх оптимизаторов, каждый параметр ровно один раз (с weight tying и без), шаг AdamW при нулевом градиенте затухает веса наlr·λи не меняет bias и RMSNorm.
62. LLaMA, Mistral, Mixtral и Gemma без инициализации из статей — P3
Заголовок раздела «62. LLaMA, Mistral, Mixtral и Gemma без инициализации из статей — P3»- Где: конструкторы
Llama,Mistral,Mixtral,Gemma;init_normal_(core/weight_init.py) вызывают толькоGPTиGPT2(пункты 7, 16). - Что: остальные модели используют инициализацию PyTorch по умолчанию: эмбеддинги N(0, 1),
Linear— равномерное с std ≈ 1/√(3·fan_in). В HF-конфигах этих моделейinitializer_range = 0.02. На загрузку весов HF это не влияет, только на обучение с нуля. - Воспроизведено: начальный loss свежих моделей при V = 1000 — 7.05–7.09 против 6.96 у GPT. Gemma с
tie_word_embeddingsиscale_embeddingsдаёт начальный loss ≈ 258 вместо ln V ≈ 6.9: эмбеддинги N(0, 1), умноженные на √d, и та же матрица на выходе. - Исправление: применять
init_normal_сinitializer_range(по умолчанию 0.02) во всех моделях; для Gemma — обязательно приscale_embeddings. - Статус: исправлено в ветке
feat/init-llama-family:Llama,Mistral,MixtralиGemmaв конце конструктора вызываютself.apply(partial(init_normal_, std=initializer_range)), как_init_weightsв HF:Linear(в том числе роутер и эксперты MoE) иEmbedding— N(0, 0.02), bias — нули; веса RMSNorm — единицы, как их создаёт конструктор. Масштабирования residual-проекций, как у GPT-2, у этих моделей в HF нет. Начальный loss на учебных конфигах (V = 1000, три сида) — 6.95–6.96 приln V = 6.91; Gemma сtie_word_embeddingsиscale_embeddings— 7.06 вместо ≈258. Тесты: распределение весов,initializer_rangeиз конфига, начальный loss ≈ ln V для всех четырёх моделей и Gemma с общими эмбеддингами × √d. Попутно в тестах сверки с HF (LLaMA, Mistral/Mixtral, Gemma) вgenerateпередаётся явная маска: HF строит её поpad_token_id=0и принимал токен 0 в промпте за паддинг — с новой инициализацией случайный промпт получил такой токен.