← Все главы

39 · Версия материала 9

Запустите небольшую LLM целиком

Проследите полный цикл небольшой декодерной языковой модели на Rust: обучение с выбором по валидации, сравнение по целевым позициям перекрывающихся окон, точное восстановление и генерацию с KV-кэшем. Отдельное правило оценивало бы каждый из 442 переходов внутри документов один раз, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся.

Сначала предскажите границы доступа, затем результат

В корпусе есть 88 документов обучающей выборки, 22 документа валидационной выборки и 22 документа тестовой выборки. Первый вопрос относится к потоку информации. Только обучающие документы могут определять ранги BPE-слияний, участвовать в обновлении параметров декодера и обучать частотную модель для сравнения. По валидационной выборке можно выбрать одно из уже обученных состояний. Тестовые документы не участвуют ни в обучении BPE, ни в обновлении параметров, ни в выборе состояния.

В результате обучения BPE только по обучающей выборке получаются восемь правил слияния и словарь размера 266266. При максимальной длине контекста декодера C=4C=4 и шаге 11 закодированные выборки образуют 18201820, 463463 и 436436 перекрывающихся каузальных окон. Для обновлений используются мини-пакеты по 1616 окон. При оценке каждой из трёх выборок мини-пакет содержит не более 128128 окон, поэтому для обучающей, валидационной и тестовой выборок получается соответственно 1515, 44 и 44 мини-пакета. Декодер состоит из одного блока с одной головой внимания; ширина модели и ширина слоя прямого распространения равны 44. Всего в нём 11881188 обучаемых скалярных параметров.

Перед тем как читать результат, предскажите:

  1. выберут ли два запуска обучения с зерном 3939 одно и то же состояние с побитовым совпадением;
  2. станет ли значение функции потерь на тестовой выборке доступно до фиксации этого состояния;
  3. изменится ли после восстановления из контрольной точки хотя бы один логит для отдельной пробы At;
  4. изменит ли генерация с кэшем хотя бы один выбор токена.

Оба запуска обучения выбирают шаг 3232 со средним NLL на валидации 3.8895318853.889531885. Только после выбора программа формирует мини-пакеты тестовой выборки и передаёт их объекту FinalEvaluator, созданному для этого запуска. Каждое из 436436 перекрывающихся тестовых окон содержит C=4C=4 целевые позиции:

Nslot=WtestC=4364=1744.N_{\mathrm{slot}}=W_{\mathrm{test}}C=436\cdot4=1744.

Каждая целевая позиция окна задаётся документом, началом окна и положением внутри окна. Соседние окна перекрываются, поэтому один и тот же переход внутри документа может войти в результат до четырёх раз. В четырёх каузальных позициях декодеру доступны соответственно один, два, три и четыре токена контекста внутри окна: C=4C=4 задаёт максимальную длину, а не длину контекста каждого предсказания. Биграммная модель всегда использует только непосредственно предшествующий токен.

В двух документах 44 перехода входят в одну позицию окна, ещё 44 — в две, ещё 44 — в три, а остальные 430430 — в четыре. Эти кратности дают показанный знаменатель:

41+42+43+4304=1744.4\cdot1+4\cdot2+4\cdot3+430\cdot4=1744.

Декодер и зафиксированная биграммная модель со сглаживанием α=1\alpha=1 оценивают одни и те же упорядоченные 17441744 позиции, включая повторы. Показанные значения — это средние NLL в натах на целевую позицию окна: 3.8660875473.866087547 для декодера и 3.9813427143.981342714 для биграммной модели. Перплексия по целевым позициям окон равна экспоненте соответствующего среднего:

PPLslot=exp ⁣(slot).\operatorname{PPL}_{\mathrm{slot}} =\exp\!\left(\mathcal L_{\mathrm{slot}}\right).

Точная перплексия декодера по позициям окон равна 47.75518020547.755180205, а биграммной модели — 53.58894058353.588940583. Перплексия безразмерна.

Следовательно, 3.8660875473.866087547 — среднее значение NLL, а не перплексия. Разница средних NLL на фиксированном примере равна 0.1152551670.115255167. Обе модели оценивают один и тот же упорядоченный набор позиций, поэтому сравнение справедливо для этого правила усреднения по позициям окон.

В двух тестовых документах всего 444444 закодированных токена, а переходов внутри документов — 442442:

Ntransition=d𝒟test(z(d)1)=4442=442.N_{\mathrm{transition}} =\sum_{d\in\mathcal D_{\mathrm{test}}}\left(\lvert z^{(d)}\rvert-1\right) =444-2=442.

При обычной однократной оценке переходов каждый из них вошёл бы в результат один раз. Декодер получил бы максимально доступный префикс не длиннее четырёх непосредственно предшествующих токенов, а для оценки использовалось бы только распределение в последней позиции. В главе 39 среднее NLL и перплексия по этому правилу не вычисляются, поэтому результаты по 17441744 позициям окон нельзя считать корпусной метрикой по 442442 переходам.

Порядок результатов сохраняется в последующих запусках, поэтому он полезен для регрессионной проверки. Это не независимая оценка способности модели обобщать на ранее не использованных данных и не доказательство общего превосходства архитектуры декодера.

Контрольная точка размером 3099430994 байта после повторного кодирования даёт те же байты и точно восстанавливает модель, оптимизатор, токенизатор и состояние генератора псевдослучайных чисел. Отдельная проба At кодируется как [67,118] и воспроизводит каждый бит логитов. Затем генерация начинается с промпта A, закодированного точным числовым ID токена 67. При τ=0.8\tau=0.8, k=4k=4 и зерне 3838 выбираются числовые ID [260,34,34], которые декодируются как т␠␠. Каждый знак обозначает один сгенерированный пробел. Пути с KV-кэшем и с расчётом по полному префиксу используют одинаковые случайные числа и принимают одинаковые решения.

Одно произведение связывает все решения о следующем токене

Полная модель по-прежнему отвечает на вопрос, поставленный в начале курса:

Pθ(z1:T)=t=1TPθ(ztz<t)P_\theta(z_{1:T})=\prod_{t=1}^{T}P_\theta(z_t\mid z_{<t})

Вероятность последовательности Pθ(z1:T)P_\theta(z_{1:T}) равна произведению условных вероятностей. При обучении минимизируется отрицательный логарифм этих условных вероятностей; по значению функции потерь на валидационной выборке выбирается один набор параметров θ\theta; итоговая оценка вычисляет функцию потерь для уже выбранного набора; при генерации новый ztz_t выбирается из условного распределения при известном z<tz_{<t}.

Эта факторизация задаёт каузальное ограничение. Вероятность в позиции tt может зависеть от z<tz_{<t}, но не от последующего токена. В общей факторизации z<tz_{<t} обозначает весь предшествующий префикс; ограниченный декодер из примера получает не более C=4C=4 ближайших предыдущих токенов. Зафиксированное разбиение вводит аналогичное ограничение для эксперимента: в этом запуске отложенные для теста документы не могут влиять на обучение токенизатора, обновления параметров или выбор состояния по валидации. Эта граница внутри одного запуска не делает известный результат снова независимым, когда он используется в последующем запуске.

При тестовом сравнении факторизация применяется к явно заданному списку наблюдений. Пусть ziz_i — наблюдаемый целевой токен в позиции окна ii, а cic_i — контекст, который действительно доступен в этой позиции. Тогда показанное среднее равно

slot=1Nsloti=1NslotlogPθ(zici).\mathcal L_{\mathrm{slot}} =-\frac{1}{N_{\mathrm{slot}}} \sum_{i=1}^{N_{\mathrm{slot}}}\log P_\theta(z_i\mid c_i).

В сумме Nslot=1744N_{\mathrm{slot}}=1744 слагаемых с одинаковым весом. Однако переходы получают неодинаковый вес: из-за перекрытия один переход внутри документа может повторяться в нескольких позициях окон. Отдельное правило оценивало бы каждый из 442442 переходов один раз, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся.

Не смешивайте позицию в последовательности с этапом процесса

  • PθP_\theta — распределение вероятностей, заданное декодером.
  • θ\theta содержит все обученные значения параметров, выбранные по функции потерь на валидации.
  • z1:Tz_{1:T} — одна последовательность от первого токена до токена TT.
  • TT — число токенов, совместная вероятность которых вычисляется.
  • t=1T\prod_{t=1}^{T} перемножает по одному условному множителю в каждой позиции.
  • tt — текущая позиция токена.
  • ztz_t — наблюдаемый токен в этой позиции.
  • z<tz_{<t} — предшествующий каузальный префикс в общей факторизации; в этом примере декодеру передаются не более C=4C=4 ближайших предыдущих токенов.
  • NslotN_{\mathrm{slot}} — число целевых позиций перекрывающихся окон, оцениваемых каждой моделью; здесь оно равно 17441744.
  • slot\mathcal L_{\mathrm{slot}} — среднее NLL в натах на целевую позицию окна, а безразмерная PPLslot\operatorname{PPL}_{\mathrm{slot}} — его экспонента.
  • NtransitionN_{\mathrm{transition}} считает 442442 перехода внутри документов. Отдельное правило оценивало бы каждый из них один раз с максимально доступным каузальным префиксом не длиннее четырёх токенов и только распределением в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся.

Номер шага обучения — не tt. Один шаг обновляет θ\theta по мини-пакету, а tt нумерует токен внутри факторизации языковой модели.

От короткого частотного контекста к авторегрессионным LLM на основе Transformer

Частотная биграммная модель оценивает следующий токен только по одному предыдущему токену; у неё нет обучаемых признаков, которые позволяли бы совместно использовать закономерности из разных контекстов, и она не учитывает более длинный каузальный префикс.

A Neural Probabilistic Language Model служит источником для этого ограниченного сравнения. Бенжио и соавторы описывают, как традиционные n-граммные модели обобщают наблюдения, опираясь на короткие перекрывающиеся фрагменты, и показывают нейронную вероятностную функцию, которая совместно обучается с распределёнными представлениями слов и даёт лучший результат при более длинном контексте; их модель не является Transformer и не охватывает все этапы программы из этого курса.

Generalization in Adaptive Data Analysis and Holdout Reuse обосновывает предупреждение о статусе свидетельств. Дворк и соавторы показывают, что многократное адаптивное использование обычной отложенной выборки может привести к переобучению на самой этой выборке; этот общий вывод не устанавливает фактов об учебном примере, его результате или локальном счётчике доступа.

Attention Is All You Need описывает следующий архитектурный этап. Васвани и соавторы задают Transformer и маскируют самовнимание декодера так, чтобы позиция не могла обращаться к последующим позициям; опубликованная ими архитектура состоит из кодировщика и декодера и не задаёт правила работы с данными, контрольными точками или генерацией в этом курсе.

Language Models are Few-Shot Learners показывает, как авторегрессионные модели этого семейства масштабировали дальше. Браун и соавторы обучают GPT-3 — авторегрессионную языковую модель на основе Transformer с 175 миллиардами параметров, построенную по архитектуре GPT-2, — и оценивают задачи с нулевым, одним и несколькими примерами без обновления параметров по градиенту и без дообучения. Результаты по масштабу и возможностям этой модели нельзя переносить на небольшой учебный запуск.

В нейронных языковых моделях начали совместно обучать распределённые представления токенов и функции вероятности, учитывающие более длинный контекст; Transformer ввёл маскированное самовнимание, а последующие авторегрессионные языковые модели на основе Transformer масштабировали эту цель обучения.

Завершающий пример курса объединяет обучение токенизатора только по обучающей выборке, каузальные обновления для предсказания следующего токена, состояние, выбранное по валидации, изоляцию выбора в пределах запуска, регрессионную проверку фиксированного примера, точное сохранение и восстановление контрольной точки и генерацию, которая сохраняет состояние между шагами. Это локальные правила работы со свидетельствами, а не требования цитируемых статей.

Сопоставьте биграммную модель со сглаживанием α=1\alpha=1, обученную только по обучающей выборке, и выбранный по валидации каузальный декодер на одних и тех же упорядоченных целевых позициях перекрывающихся окон. Максимальная длина контекста декодера равна четырём токенам, а в четырёх позициях окна ему доступны от одного до четырёх токенов. Разницу средних NLL, сохраняемую при последующих запусках, используют для регрессионной проверки фиксированного примера; она не доказывает причинного влияния контекста или внимания и не является независимой оценкой способности модели обобщать. Рассуждение Бенжио и соавторов о перекрывающихся фрагментах не задаёт принятое в курсе усреднение по окнам с шагом 1.

Частотные n-граммы служили сильной базовой моделью с коротким контекстом; обучаемые распределённые представления и маскированное самовнимание позволили моделям обрабатывать более длинный контекст, а масштабированные авторегрессионные модели на основе Transformer стали одним из основных семейств современных LLM. Завершающий пример показывает локальные границы ответственности, а известное более низкое среднее NLL декодера по позициям окон представлено лишь как результат фиксированного примера для регрессионной проверки.

Сравнение в программе намеренно узкое: модель с однотокенным частотным контекстом и декодер с максимальной длиной контекста четыре токена оценивают на этом зафиксированном корпусе одни и те же 17441744 повторяющиеся целевые позиции окон. Более низкое среднее NLL по позициям окон проверяет сохраняемый порядок результатов фиксированного примера. Отдельное правило оценивало бы каждый из 442442 переходов один раз, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся. Показанный порядок — не независимая оценка способности модели обобщать на ранее не использованных данных, не доказательство общего превосходства одной архитектуры и не подтверждение полезного качества генерации.

Связать выполняемое программой сравнение с развитием языковых моделей rust/demos/ch39-end-to-end-llm/src/lib.rs#historical-contrast
#[derive(Clone, Debug, PartialEq)]
pub struct HistoricalContrast {
    pub window_slot_unit: &'static str,
    pub window_target_slots: usize,
    pub document_transition_occurrences: usize,
    pub bigram_context_tokens: usize,
    pub decoder_context_capacity: usize,
    pub decoder_window_slot_context_lengths: Vec<usize>,
    pub bigram_window_slot_mean_nll_nats: f64,
    pub decoder_window_slot_mean_nll_nats: f64,
    pub window_slot_gap_nats: f64,
}

/// Measures the fixture's short-context baseline against its causal decoder.
pub fn historical_contrast(evidence: &CapstoneRun) -> HistoricalContrast {
    let evaluation = evidence.final_evaluation();
    let decoder_context_capacity = evidence.training().model_config().max_positions();
    HistoricalContrast {
        window_slot_unit: WINDOW_SLOT_UNIT,
        window_target_slots: evaluation.window_target_slot_count(),
        document_transition_occurrences: evaluation.document_transition_occurrence_count(),
        bigram_context_tokens: 1,
        decoder_context_capacity,
        decoder_window_slot_context_lengths: (1..=decoder_context_capacity).collect(),
        bigram_window_slot_mean_nll_nats: evaluation.bigram().mean_nll(),
        decoder_window_slot_mean_nll_nats: evaluation.decoder().mean_nll(),
        window_slot_gap_nats: evaluation.loss_gap(),
    }
}

Соедините уже реализованные API, не дублируя алгоритмы

run_capstone сначала разбирает и проверяет данные корпуса и манифест разбиения. Функция обучает BPE по обучающей выборке, кодирует каждый документ отдельно, не склеивая их границы, обучает по тем же обучающим токенам биграммную модель со сглаживанием α=1\alpha=1 и строит отдельные наборы каузальных мини-пакетов для обновлений параметров и валидации. Набор тестовых мини-пакетов на этом этапе ещё не создаётся.

Два запуска декодера получают одинаковую инициализацию и одно и то же начальное значение генератора, задающего порядок мини-пакетов. Каждый выполняет все 3232 обновления. Реализация побитово сравнивает все записи о шагах обучения, контрольные значения функций потерь, моменты оптимизатора, выбранный шаг и значения параметров модели. Только после совпадения двух запусков и выбора состояния по валидации программа готовит итоговую оценку.

TrainingResult хранит и снимок состояния, выбранного по валидации, и независимый декодер с теми же значениями параметров. SelectedDecoder получает неизменяемые ссылки на оба объекта. Затем программа формирует набор тестовых мини-пакетов, сохраняет нужные для отчёта сведения, включая 436436 окон и 44 мини-пакета, и передаёт владение всем набором объекту FinalEvaluator. Этот объект владеет тестовыми мини-пакетами и допускает ровно один вызов итоговой оценки.

Непосредственно перед этим вызовом FinalEvaluator проверяет точное совпадение конфигурации декодера, имён параметров в установленном порядке, форм их тензоров и всех битов значений параметров с сохранённым снимком. Если декодер изменился после выбора, проверка завершится ошибкой до оценки на тестовых данных; ещё одна полная копия модели для этой проверки не создаётся. Затем FinalEvaluator выполняет свой единственный разрешённый вызов оценки по неизменяемой ссылке. Обе модели оцениваются на одном и том же упорядоченном наборе из 17441744 целевых позиций перекрывающихся окон, включая одинаковые повторы переходов. Максимальная длина контекста декодера равна четырём токенам, но в отдельных позициях ему доступны контексты длиной 11, 22, 33 и 44. Отдельное правило оценивало бы каждый из 442442 переходов один раз, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся.

Ограничение на один вызов относится к объекту FinalEvaluator, а не к снимку состояния или декодеру. Это проверяемая граница внутри данного запуска, а не утверждение, будто репозиторий в целом запрещает другому коду читать корпус. При последующей сборке отчёта используются вычисленные числа и сведения о метрике, поэтому копия всего набора тестовых мини-пакетов не нужна.

В конвейере соотношение, при котором среднее NLL декодера по позициям окон ниже, явно обозначено как условие регрессионной проверки фиксированного примера; сам FinalEvaluator остаётся нейтральным к тому, какая модель показывает меньшие потери. Учебные свидетельства используют decoder_lower_on_fixture:true и записывают scope:fixed-fixture-regression, within_run_selection_isolated:true, independent_generalization_estimate:false и architecture_superiority_evidence:false. Прежнее имя decoder_wins не является актуальным свидетельством.

Показанное в отчёте число 11881188 обучаемых скалярных параметров вычисляется вызовом primary.selected_state().scalar_count(). Этот метод суммирует длины тензоров параметров в сохранённом состоянии без графа вычислений, которое было выбрано по валидации. Метод не читает заранее сохранённый счётчик и не создаёт ещё один декодер только ради подсчёта.

Итоговая оценка и отдельная контрольная точка по-разному владеют данными. Оценка лишь временно обращается по неизменяемым ссылкам к выбранному состоянию и соответствующему декодеру. После оценки полный результат обучения и контрольная точка должны оставаться доступными одновременно. Поэтому Checkpoint::from_snapshot намеренно копирует выбранное состояние без графа вычислений и состояние оптимизатора, необходимое для продолжения обучения. Эти копии позволяют дальше пользоваться результатом обучения и контрольной точкой независимо друг от друга.

Повторное кодирование загруженной контрольной точки должно точно воспроизвести сохранённые байты. Биты модели и оптимизатора, ранги BPE-слияний, выбранный шаг и состояние генератора псевдослучайных чисел также должны совпасть. До передачи контрольной точки в into_model программа отдельно сохраняет токенизатор, выбранный шаг, состояние оптимизатора и состояние генератора, которые понадобятся для последующих проверок. Затем вызов loaded.into_model() передаёт методу владение контрольной точкой, а метод перемещает принадлежащие ей буферы модели в декодер; после вызова значение loaded больше нельзя использовать.

Логиты этого декодера для отдельной пробы At побитово сравниваются с логитами primary.selected_model(). Только после успешного сравнения загруженный декодер генерирует продолжение промпта A с KV-кэшем и с полным пересчётом префикса, начиная с сохранённого состояния генератора. Функция generation_evidence один раз кодирует промпт в один вектор prompt_ids. Генерация с KV-кэшем и эталонный расчёт по полному префиксу временно читают один и тот же вектор по неизменяемым ссылкам. Когда оба вызова завершены и локальный вектор больше не нужен для вычислений, структура GenerationEvidence получает его во владение. Перемещение Vec передаёт уже существующий буфер вместо создания копии ID токенов промпта для отчёта.

Сохранить число тестовых окон и пакетов до передачи тестового прохода объекту оценки, получить число параметров из выбранного состояния и затем передать буферы загруженной контрольной точки rust/crates/llm-from-scratch/src/pipeline.rs#end-to-end-capstone
pub fn run_capstone(
    corpus_source: &str,
    split_source: &str,
    checkpoint_path: impl AsRef<Path>,
    config: CapstoneConfig,
) -> Result<CapstoneRun, PipelineError> {
    let data = prepare_data(corpus_source, split_source, config)?;
    let prepared = prepare_training(&data, config)?;
    let primary = training_once(&prepared, config)?;
    let replay = training_once(&prepared, config)?;
    let replay_bitwise = training_replays_bitwise(&primary, &replay);
    require(replay_bitwise, "same-seed training replay changed bits")?;
    let selected_step = primary.selected_step();
    let selected_step_u64 = u64::try_from(selected_step)
        .map_err(|_| PipelineError::invariant("selected step does not fit u64"))?;
    require(
        selected_step == config.updates(),
        "validation no longer selects the final optimizer state",
    )?;
    require(
        selected_step_u64 == primary.selected_optimizer_state().step_count(),
        "selected model and optimizer no longer share one step",
    )?;
    require(
        primary.selected_state().bit_pattern() == primary.final_state().bit_pattern(),
        "selected and final model states diverged",
    )?;

    let provenance = map(
        PipelineStage::FinalEvaluation,
        EvaluationProvenance::new(
            data.partitions.corpus_checksum(),
            format!(
                "{}:{}",
                data.partitions.split_strategy(),
                data.partitions.corpus_checksum()
            ),
            format!(
                "byte-bpe-v{}-merges{}",
                TOKENIZER_LAYOUT_VERSION,
                data.tokenizer_evidence.learned_merges()
            ),
            config.context_length(),
        ),
    )?;
    let decoder = map(
        PipelineStage::FinalEvaluation,
        SelectedDecoder::new(
            primary.selected_state(),
            primary.selected_model(),
            primary.selected_step(),
            primary.selected_validation_loss(),
            Partition::Validation,
            &provenance,
        ),
    )?;
    let frozen_bigram = map(
        PipelineStage::FinalEvaluation,
        FrozenBigram::new(&data.bigram, Partition::Train, &provenance),
    )?;
    let test_epoch = epoch(
        &data.encoded,
        Partition::Test,
        config.context_length(),
        config.window_stride(),
        config.evaluation_batch_size(),
        BatchOrder::Sequential,
    )?;
    require(
        config.window_stride() == 1
            && epoch_matches_window_stride(&test_epoch, config.window_stride()),
        "the Chapter 39 evaluation must retain complete stride-one windows",
    )?;
    let test_window_count = test_epoch.window_count();
    let test_batch_count = test_epoch.batch_count();
    let mut evaluator = map(
        PipelineStage::FinalEvaluation,
        FinalEvaluator::new(test_epoch, provenance.clone()),
    )?;
    require(
        evaluator.access_count() == 0,
        "test evidence opened before model selection completed",
    )?;
    let final_evaluation = map(
        PipelineStage::FinalEvaluation,
        evaluator.evaluate_once(decoder, frozen_bigram),
    )?;
    let expected_window_target_slots = test_window_count
        .checked_mul(config.context_length())
        .ok_or_else(|| PipelineError::invariant("test window-target slot count overflowed"))?;
    let expected_document_transition_occurrences = data
        .encoded
        .documents(Partition::Test)
        .iter()
        .map(|document| document.token_ids().len().saturating_sub(1))
        .sum::<usize>();
    let multiplicity_slot_count = final_evaluation
        .transition_multiplicity_counts()
        .iter()
        .enumerate()
        .map(|(index, count)| (index + 1) * count)
        .sum::<usize>();
    let multiplicity_transition_count = final_evaluation
        .transition_multiplicity_counts()
        .iter()
        .sum::<usize>();
    require(
        final_evaluation.window_target_slot_count() == expected_window_target_slots
            && final_evaluation.document_transition_occurrence_count()
                == expected_document_transition_occurrences
            && multiplicity_slot_count == expected_window_target_slots
            && multiplicity_transition_count == expected_document_transition_occurrences,
        "test window slots, document transitions, and overlap multiplicities disagree",
    )?;
    require(
        final_evaluation.decoder_has_lower_loss(),
        "fixed capstone fixture no longer records lower decoder loss than its frozen bigram",
    )?;

    let rng_state = SplitMix64::from_seed(config.generation_seed()).state();
    let checkpoint = map(
        PipelineStage::Checkpoint,
        Checkpoint::from_snapshot(
            CheckpointTokenizer::byte_bpe(&data.tokenizer),
            primary.selected_training_state(),
            rng_state,
        ),
    )?;
    let encoded_checkpoint = map(
        PipelineStage::Checkpoint,
        checkpoint.save_atomic(checkpoint_path.as_ref()),
    )?;
    let loaded = map(
        PipelineStage::Checkpoint,
        Checkpoint::load(checkpoint_path.as_ref()),
    )?;
    let loaded_encoded = map(PipelineStage::Checkpoint, loaded.encode())?;
    let bytes_roundtrip = encoded_checkpoint.bytes() == loaded_encoded.bytes();
    require(
        bytes_roundtrip,
        "loaded checkpoint bytes differ from the saved record",
    )?;
    let loaded_tokenizer = map(PipelineStage::Checkpoint, loaded.tokenizer().restore_bpe())?
        .ok_or_else(|| PipelineError::invariant("checkpoint lost its byte BPE tokenizer"))?;
    let model_bits_exact =
        loaded.model_state().bit_pattern() == primary.selected_state().bit_pattern();
    let optimizer_bits_exact =
        adamw_state_bitwise(loaded.optimizer_state(), primary.selected_optimizer_state());
    let loaded_selected_step = loaded.selected_step();
    let loaded_optimizer_step = loaded.optimizer_state().step_count();
    let loaded_rng_state = loaded.sampling_rng_state();
    let loaded_model = map(PipelineStage::Checkpoint, loaded.into_model())?;
    let logit_probe_text = "At";
    let logit_probe_ids = data.tokenizer.encode_utf8(logit_probe_text);
    let prompt_logits_bitwise = logits_bits(primary.selected_model(), &logit_probe_ids)?
        == logits_bits(&loaded_model, &logit_probe_ids)?;
    let tokenizer_exact = loaded_tokenizer == data.tokenizer;
    require(
        model_bits_exact && optimizer_bits_exact && prompt_logits_bitwise && tokenizer_exact,
        "checkpoint reload changed model, optimizer, tokenizer, or probe logits",
    )?;
    let generation =
        generation_evidence(&loaded_model, &loaded_tokenizer, loaded_rng_state, config)?;

    let checkpoints = primary
        .checkpoints()
        .iter()
        .map(|checkpoint| TrainingCheckpointEvidence {
            step: checkpoint.step(),
            train_loss: checkpoint.train().mean_loss(),
            validation_loss: checkpoint.validation().mean_loss(),
            selected: checkpoint.selected(),
        })
        .collect();
    let training = TrainingEvidence {
        model_config: prepared.model_config,
        parameter_count: primary.selected_state().scalar_count(),
        window_counts: [
            prepared.train.window_count(),
            prepared.validation.window_count(),
            test_window_count,
        ],
        batch_counts: [
            prepared.train.batch_count(),
            prepared.validation.batch_count(),
            test_batch_count,
        ],
        checkpoints,
        selected_step,
        selected_validation_loss: primary.selected_validation_loss(),
        optimizer_step: primary.final_optimizer().step_count(),
        replay_bitwise,
    };
    let checkpoint_evidence = CheckpointEvidence {
        bytes: encoded_checkpoint.bytes().len(),
        header_bytes: encoded_checkpoint.header_bytes(),
        checksum: encoded_checkpoint.checksum_label(),
        tensor_records: encoded_checkpoint.tensors().len(),
        selected_step: loaded_selected_step,
        optimizer_step: loaded_optimizer_step,
        sampling_rng_state: loaded_rng_state,
        bytes_roundtrip,
        model_bits_exact,
        optimizer_bits_exact,
        tokenizer_exact,
        logit_probe_text: logit_probe_text.to_owned(),
        logit_probe_ids,
        prompt_logits_bitwise,
    };
    Ok(CapstoneRun {
        partitions: data.partitions,
        tokenizer: data.tokenizer_evidence,
        training,
        final_evaluation,
        checkpoint: checkpoint_evidence,
        generation,
    })
}

Функция run_capstone проверяет все итоговые условия совместно. Она возвращает ошибку, если нарушен порядок доступа к тестовым данным, изменилось условие регрессионной проверки фиксированного примера, по которому значение функции потерь декодера ниже, повторный запуск не совпадает побитово, после загрузки изменились состояние контрольной точки или логиты пробы либо генерация с кэшем выбрала хотя бы один другой токен. Это условие защищает известное поведение фиксированного примера, но не превращает порядок результатов в независимое свидетельство способности модели обобщать.

Совместно проверить итоговую оценку, повторный запуск, восстановление и генерацию rust/demos/ch39-end-to-end-llm/src/lib.rs#capstone-evidence
/// Runs the checked corpus-to-generated-text program and checks its final claims.
pub fn learner_evidence() -> Result<CapstoneRun, FixtureError> {
    let checkpoint = TemporaryCheckpoint::new();
    let evidence = run_capstone(
        CORPUS_JSON,
        SPLITS,
        checkpoint.path(),
        CapstoneConfig::tiny(),
    )?;
    require(
        evidence.final_evaluation().decoder_has_lower_loss(),
        "fixed capstone fixture no longer records lower decoder loss than its frozen bigram",
    )?;
    require(
        within_run_selection_isolated(&evidence),
        "within-run selection isolation evidence changed",
    )?;
    require(
        evidence.training().replay_bitwise(),
        "same-seed training replay changed bits",
    )?;
    require(
        evidence.checkpoint().bytes_roundtrip()
            && evidence.checkpoint().model_bits_exact()
            && evidence.checkpoint().optimizer_bits_exact()
            && evidence.checkpoint().tokenizer_exact()
            && evidence.checkpoint().prompt_logits_bitwise(),
        "checkpoint reload changed bytes, model, optimizer, tokenizer, or probe logits",
    )?;
    require(
        evidence.generation().tokens_exact()
            && evidence.generation().decisions_bitwise()
            && evidence.generation().rng_state_exact(),
        "cached generation changed reference decisions",
    )?;
    Ok(evidence)
}

Запустите cargo run —quiet —locked -p ch39-end-to-end-llm. Исполняемый файл напечатает этот точный отчёт:

chapter=39-end-to-end-llm
data=checksum:fnv1a64:723b071980ae8a22 split:fixed-paired-document-holdout-v1 documents:8/2/2 train_ids:[en-river-dawn,ru-river-dawn,en-clock-shop,ru-clock-shop,en-rain-library,ru-rain-library,en-bee-garden,ru-bee-garden] validation_ids:[en-night-station,ru-night-station] test_ids:[en-winter-window,ru-winter-window]
tokenizer=layout:1 requested:8 learned:8 training_only:true vocabulary:266 encoded_tokens:[1852,471,444]
model=layers:1 heads:1 width:4 feed_forward:4 context:4 parameters:1188 update_batch_size:16 evaluation_batch_size:128 windows:[1820,463,436] evaluation_batches:[15,4,4]
training=updates:32 seed:39 checkpoints:0:5.621745486/5.628342353/candidate;32:3.855502695/3.889531885/selected selected:32 validation:3.889531885 optimizer:32 replay_bitwise:true
test=access:1 documents:[en-winter-window,ru-winter-window] stride:1 windows:436 batches:4 window_target_slots:1744 document_transition_occurrences:442 transition_multiplicity_counts:[1x4,2x4,3x4,4x430] window_slot_fingerprint:fnv1a64:77b836869f848986 no_grad:true unchanged:true
slot_metric=unit:overlapping-window-target-slot decoder_window_slot_mean_nll_nats:3.866087547 decoder_window_slot_perplexity:47.755180205 bigram_window_slot_mean_nll_nats:3.981342714 bigram_window_slot_perplexity:53.588940583 window_slot_gap_nats:0.115255167 comparison_slot_set:shared-ordered-window-slots decoder_lower_on_fixture:true
transition_metric=unit:within-document-next-token-transition count:442 context_policy:longest-available-causal-prefix-up-to-4 newest_position_only:true reported:false mean_nll:not-reported perplexity:not-reported
evidence=scope:fixed-fixture-regression within_run_selection_isolated:true independent_generalization_estimate:false architecture_superiority_evidence:false
checkpoint=bytes:30994 header:2418 records:34 checksum:fnv1a64:67aeaaea603b291f selected:32 optimizer:32 rng:0x0000000000000026 bytes_roundtrip:true model_bits_exact:true optimizer_bits_exact:true tokenizer_exact:true logit_probe:At logit_probe_ids:[67,118] prompt_logits_bitwise:true
generation=prompt:A prompt_ids:[67] temperature:0.8 top_k:4 seed:38 generated:[260,34,34] text:"т  " prefixes:[1,2,3] stop:token-limit prefill:1 decode:2 final_cache:3 cached_scores:6 calculated_complete_prefix_scores:14 rng_initial:0x0000000000000026 rng_final:0xdaa66d2c7ddf7465 tokens_exact:true decisions_bitwise:true rng_exact:true
history=window_slot_unit:overlapping-window-target-slot window_target_slots:1744 document_transition_occurrences:442 bigram_context_tokens:1 decoder_context_capacity:4 decoder_window_slot_context_lengths:[1,2,3,4] bigram_window_slot_mean_nll_nats:3.981342714 decoder_window_slot_mean_nll_nats:3.866087547 window_slot_gap_nats:0.115255167
next=inspect, modify, test, and extend the complete decoder
Напечатать точный отчёт главы 39 о полном процессе от данных до генерации rust/demos/ch39-end-to-end-llm/src/main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    print!("{}", ch39_end_to_end_llm::learner_report()?);
    Ok(())
}

Проследите процесс: поздние результаты не влияют на ранние этапы

В начале схемы зафиксированы роли документов, а токенизатор обучается только по обучающей выборке. Затем каузальные окна поступают в декодер; выбранное по валидации состояние расположено перед локальной границей доступа к тестовым данным, выделенной двойной рамкой. В этом запуске значение функции потерь на тестовой выборке не влияет ни на обновление параметров, ни на выбор состояния. Карточка тестовой оценки называет 17441744 наблюдения целевыми позициями перекрывающихся окон и обозначает результаты как средние NLL в натах на целевую позицию окна. Отдельное правило оценивало бы каждый из 442442 переходов внутри документов один раз, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся. Известный порядок результатов служит регрессионной проверкой фиксированного примера, а не независимой оценкой способности модели обобщать.

На двух последних этапах проверяются разные виды равенства. Повторное кодирование восстановленной контрольной точки даёт те же байты; модель, оптимизатор, токенизатор, шаг и состояние генератора псевдослучайных чисел совпадают точно, а логиты для пробы At — побитово. Генерация с KV-кэшем из промпта A выбирает те же токены, что и эталонный расчёт по полному префиксу, и завершает работу с тем же состоянием генератора. Перед тремя выборами токена длины сохранённых префиксов равны 11, 22 и 33 токенам. При заполнении KV-кэша единственный токен промпта даёт логиты для первого выбора. Затем программа ещё дважды вызывает декодер, подавая сначала первый, затем второй сгенерированный токен; эти вызовы дают логиты для второго и третьего выборов. Путь с KV-кэшем вычисляет 1+2+3=61+2+3=6 элементов матриц оценок внимания; для того же расписания префиксов эталонный расчёт по полному префиксу даёт 12+22+32=141^2+2^2+3^2=14 элементов.

Сохраните односторонний порядок запуска и обозначьте статус результата

Проследите зафиксированные результаты программы на Rust: обучение BPE только по обучающим данным, выбор состояния и локально изолированное сравнение по 1744 целевым позициям перекрывающихся окон. Отдельное правило оценивало бы 442 перехода внутри документов по одному разу, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся. Затем проследите точное восстановление и генерацию с кэшем.

Ход программы

Номера задают порядок. На этапе тестирования показано сравнение по одним и тем же позициям окон. Отдельно обозначено правило для 442 переходов: каждый оценивается один раз, используется максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся. Двойные рамки отмечают BPE только по обучающим данным, выбор по валидации, локально изолированную оценку фиксированного примера и точное воспроизведение.

  1. Зафиксируйте роли документов

    Число документов — обучение / валидация / тест: 8/2/2

    fnv1a64:723b071980ae8a22

  2. Обучите BPE по обучающей выборке

    = слияния зависят только от обучающих данных

    Размер словаря: 266

    Число токенов после кодирования — обучение / валидация / тест
    [1852,471,444]
  3. Постройте каузальные окна

    Максимальная длина контекста декодера: C=4C=4

    Мини-пакет обновления / Мини-пакет оценки: 16/128

    Число перекрывающихся окон с шагом 1 — обучение / валидация / тест
    [1820,463,436]
    Число мини-пакетов оценки — обучение / валидация / тест
    [15,4,4]
  4. Обучите один блок декодера

    Обучаемые параметры: 1188

    L=1, H=1, D=4L=1,\ H=1,\ D=4

  5. Выберите по валидации
    начальное состояние-кандидат s=0s=0
    обучение 5.6217454865.621745486 валидация 5.6283423535.628342353
    состояние выбрано по валидации s=32s=32
    обучение 3.8555026953.855502695 валидация 3.8895318853.889531885
  6. Локально оцените фиксированный пример

    || один локальный доступ в этом запуске

    Целевые позиции перекрывающихся окон
    1744
    Переходы внутри документов в заданных позициях
    442
    Число переходов с кратностью 1× / 2× / 3× / 4×
    [1x4,2x4,3x4,4x430]
    Среднее NLL декодера, в натах на позицию окна
    3.8660875473.866087547
    Безразмерная перплексия декодера по позициям окон
    47.75518020547.755180205
    Среднее NLL биграммной модели, в натах на позицию окна
    3.9813427143.981342714
    Безразмерная перплексия биграммной модели по позициям окон
    53.58894058353.588940583
    Разница средних NLL, в натах на позицию окна
    0.1152551670.115255167
    Максимальная длина контекста декодера
    4
    Фактические длины контекста в позициях окон
    [1,2,3,4]
    Каждый переход один раз — максимально доступный каузальный префикс не длиннее четырёх токенов; только последняя позиция
    442 перехода внутри документов по одному разу; максимально доступный каузальный префикс не длиннее четырёх токенов; только последняя позиция; числовые значения среднего NLL и перплексии по этому правилу не приводятся

    = обе модели оценивают один и тот же упорядоченный набор позиций, включая повторы

    3.866087547<3.9813427143.866087547<3.981342714

  7. Сохраните и восстановите

    = байты и состояния модели, оптимизатора и токенизатора совпадают; логиты пробы — тоже

    Байты контрольной точки: 30994

    Записи тензоров: 34

    Текст пробы для проверки логитов после восстановления
    At
    ID токенов, которыми закодирована проба At
    [67,118]
  8. Генерация с KV-кэшем

    = решения с KV-кэшем и полным префиксом совпадают

    Промпт и ID токена: A [67]

    Параметры выбора токенов: τ=0.8, k=4\tau=0.8,\ k=4 seed=38

    Сгенерированные ID токенов: [260,34,34]

    Декодированный текст: т␠␠ ␠ — сгенерированный пробел.

    Длины сохранённых префиксов перед каждым выбором токена (в токенах)
    [1,2,3]
    Число токенов промпта, обработанных при заполнении KV-кэша
    1
    Число ранее сгенерированных токенов, которые по одному подаются декодеру для вычисления следующих логитов
    2
    Число элементов матриц оценок внимания при работе с KV-кэшем
    1+2+3=61+2+3=6
    Число элементов матриц оценок внимания при эталонном расчёте по полному префиксу
    12+22+32=141^2+2^2+3^2=14

Сначала предскажите, затем проверьте итоговую трассировку

  1. Документы какой выборки участвуют в обучении восьми правил BPE-слияния?
  2. Сколько параметров декодера получает обновления?
  3. Какое значение функции потерь на валидационной выборке подтверждает выбор шага 3232?
  4. Может ли значение функции потерь на тестовой выборке изменить этот выбор?
  5. Как из 436436 перекрывающихся тестовых окон получаются 17441744 целевые позиции окон и почему переходов внутри документов только 442442?
  6. Чему равна измеренная разница средних NLL по позициям окон, как перплексия связана со средним NLL каждой модели и какова область применимости этого порядка результатов?
  7. Какие свойства контрольной точки совпадают точно, а какое утверждение относится только к пробе At?
  8. Какие именно числовые ID токенов следуют за промптом A?
  9. Почему декодированное т␠␠ не свидетельствует о качестве перевода?
  10. Что проверяет второй запуск обучения с зерном 3939?

Заблуждение: access:1 означает, что во всей истории репозитория этот тестовый результат использовали только один раз. Счётчик относится к одному экземпляру оценщика в пределах одного запуска. Здесь состояние сначала фиксируется, а тестовые мини-пакеты формируются позже, поэтому тест не может изменить выбранное состояние в этом запуске. В последующих запусках известный порядок результатов повторно используют для регрессионной проверки: детерминизм делает такое повторное сравнение воспроизводимым, но не независимым.

Проверьте десять предсказаний
  1. Частоты пар для BPE рассчитываются только по 88 документам обучающей выборки.
  2. Одноблочный декодер содержит 11881188 обучаемых скалярных параметров.
  3. На шаге 3232 значение функции потерь на валидации равно 3.8895318853.889531885 — это меньше, чем 5.6283423535.628342353 на шаге 00.
  4. Нет. Объект итоговой оценки получает тестовые данные после выбора состояния и не обновляет параметры.
  5. В каждом из 436436 окон с шагом 11 есть C=4C=4 целевые позиции, поэтому 4364=1744436\cdot4=1744. Из-за перекрытия один переход внутри документа повторяется в числе позиций до четырёх раз. В двух отдельных тестовых документах таких переходов 4442=442444-2=442. Отдельное правило оценивало бы каждый из них один раз, использовало бы максимально доступный каузальный префикс не длиннее четырёх токенов и только распределение в последней позиции; числовые значения среднего NLL и перплексии по этому правилу не приводятся.
  6. Показанные значения — средние NLL в натах на целевую позицию окна, а 3.9813427143.866087547=0.1152551673.981342714-3.866087547=0.115255167. Перплексия по позициям окон равна exp(slot)\exp(\mathcal L_{\mathrm{slot}}); сами значения средних NLL не являются перплексиями. Порядок, при котором среднее NLL декодера ниже, сохраняется в последующих запусках для регрессионной проверки; он не является независимой оценкой способности модели обобщать и не доказывает превосходства архитектуры.
  7. Повторно закодированные байты, биты модели и оптимизатора, ранги BPE-слияний, выбранный шаг и состояние генератора псевдослучайных чисел совпадают точно; только логиты для явно заданной пробы At сравниваются между собой.
  8. Точные сгенерированные ID: [260,34,34].
  9. Это одна генерация с заданным зерном на небольшом двуязычном корпусе, а не семантический тест качества.
  10. Этот запуск проверяет, что при зафиксированных входных данных, версиях инструментов и среде вычислений все записанные значения обучения, валидации, оптимизатора и модели воспроизводятся побитово.

Теперь весь декодер в ваших руках

Теперь все части курса участвуют в одной работающей программе: зафиксированный двуязычный корпус превращается в BPE-токены и каузальные пакеты; валидационная выборка определяет состояние декодера до передачи тестовых пакетов локальному объекту FinalEvaluator; обе модели оценивают одни и те же упорядоченные целевые позиции перекрывающихся окон; отдельное правило оценивало бы каждый из 442442 переходов один раз с максимально доступным каузальным префиксом не длиннее четырёх токенов и только распределением в последней позиции, но числовые значения среднего NLL и перплексии по этому правилу не приводятся; известный порядок, при котором среднее NLL декодера ниже, остаётся только результатом фиксированного примера для регрессионной проверки; байты контрольной точки и сохранённое состояние точно восстанавливаются; отдельная проба At побитово воспроизводит логиты; генерация с кэшем из A возвращает декодированный текст.

Так завершается запланированный путь к современной декодерной LLM в учебном масштабе. Теперь можно изменить одну часть с чёткими границами — увеличить контекст или ширину блока, задать другой бюджет токенизатора либо заменить сэмплер — и точно определить, какие проверки данных, математики, обучения, оценки, сохранения состояния и работы модели при генерации нужно будет выполнить заново.