35 · Версия материала 5
Сохраните состояние декодера и точно повторите одно заданное обновление
Разберитесь, как контрольная точка с версией формата сохраняет токенизатор, декодер, состояние AdamW из того же снимка цикла обучения и отдельный генератор для выбора токенов, отклоняет повреждённые байты и даёт одинаковый результат одного обновления, если вызывающий код передаёт обеим ветвям одинаковые входы, цели и скорость обучения.
Сохраните заявленное состояние компонентов, а не только веса
В главе 33 было выбрано состояние декодера с шага . Тренер возвращает его как единое выбранное состояние обучения: снимок модели, состояние AdamW, зафиксированное циклом обучения одновременно с моделью, и их общий номер завершённого шага. В этом фиксированном примере выбран последний шаг обучения, но глава 35 не выводит соответствие снимков из этого совпадения. Если в недоверенном файле указан выбранный шаг , а счётчик AdamW равен , загрузчик отклонит файл.
Для создания контрольной точки нужен этот снимок, выданный циклом обучения. Версия хранит снимок модели, состояние AdamW и оба номера шага, но не содержит отдельного доказательства общего происхождения модели и оптимизатора. При загрузке проверяется только равенство записанного выбранного шага и счётчика AdamW; одинаковые числа в произвольном файле сами по себе не доказывают общую траекторию обучения.
Контрольная точка хранит:
- версии схемы, формата данных токенизатора и алгоритма генератора для выбора токенов;
- пять упорядоченных байтовых представлений явно заданных токенов и все поля конфигурации декодера;
- именованных тензоров параметров в стабильном порядке;
- именованных тензора моментов AdamW, группы параметров, номер шага и точные накопленные степени и ;
- выбранный шаг и одно отдельное состояние SplitMix64 для выбора токенов.
На этой обязательной границе сразу после обновления градиенты уже не нужны для сохранения. В конфигурации AdamW хранится базовая скорость обучения, но не следующее значение, заданное вызывающим кодом, и не расписание главы 33.
Явно разделяйте сохранённые данные и обязанности вызывающего кода:
- В файле: токенизатор, конфигурация и биты параметров декодера, состояние AdamW из того же снимка цикла обучения, записанные номера шагов и генератор для выбора токенов.
- Для показанного обновления вызывающий код передаёт: входы
[0,1], цели[1,2]и скорость обучения . - Для продолжения обучения всё ещё нужны: сведения о корпусе и разбиении, токенизированные данные, порядок и текущая позиция пакетов, генератор, используемый при обучении, расписание скорости обучения, ограничение нормы градиента и правила валидации.
- За пределами контрольной точки: итоговый отчёт и сведения о происхождении тестовых данных из главы 34, градиенты на этой чистой границе и кэш внимания, которым займётся глава 38.
Сохранённое состояние SplitMix64 используется только для последующего выбора токенов. Это не начальное значение перемешивания пакетов из главы 33 и не другой генератор, используемый при обучении.
Пять ID токенов от до в этом примере соответствуют однобайтовым меткам
0—4. Вместе с записями модели и оптимизатора они дают
упорядоченных записей данных. Два кодирования одного состояния
дают одинаковые байт и одно и то же значение контрольной суммы
fnv1a64:2b8b6097eaed6a91.
Гарантия намеренно узкая: в одной и той же арифметической среде загрузка воспроизводит точные биты. При смене оборудования или арифметических ядер поведение чисел с плавающей точкой может измениться, даже если контрольная точка корректна. FNV-1a обнаруживает случайные повреждения, но не подтверждает подлинность файла и не защищает от злоумышленника.
Вычисляйте следующее смещение по форме и размеру элемента
Для канонической записи данных следующее абсолютное смещение в байтах равно
Заголовок занимает байт. Пять однобайтовых записей токенов занимают
, поэтому первый параметр начинается со смещения . Это
token_embedding.weight формы ; каждый его элемент хранится как
восьмибайтовое значение f64. Поэтому не включённая правая граница равна
Выравнивающие промежутки не добавляются. Запись f64 на диске не обязана
начинаться с границы, подходящей для выравнивания значения в памяти: загрузчик
копирует восемь байтов и декодирует их в порядке от младшего к старшему
(little-endian), а не преобразует указатель на файл в указатель на f64.
Проверяются произведение размеров осей, умножение на размер элемента, сложение
смещений и преобразование дискового u64 в usize текущего процесса. Начало
каждой записи должно совпадать с концом предыдущей, а не включённая правая
граница последней записи должна быть равна смещению . Промежуток,
перекрытие, арифметическое переполнение, усечение файла или лишний байт считаются
ошибкой.
Не смешивайте порядок записей, форму и представление значений
- — абсолютное смещение начала записи .
- — абсолютное смещение начала следующей записи.
- — число байтов на один элемент, заданное сохранённым типом данных записи
: для
u8, дляu32или дляf64. - — длина оси записи .
- — число элементов в записи.
- выбирает одну ось формы.
- следует стабильному порядку данных токенизатора, параметров и состояния оптимизатора.
- — полный размер заголовка и смещение первой записи данных, поэтому .
Дескриптор хранит назначение, имя, тип данных, форму, абсолютное смещение и число байтов. Эти поля решают разные задачи. Форма определяет, как значения образуют тензор; тип данных — как они представлены и сколько байтов занимает каждое значение; назначение и имя указывают на хранимое состояние и его компонент; канонический порядок фиксирует детерминированную последовательность записей. Совпадение длины файла не компенсирует изменённое имя или форму.
Пять сгруппированных диапазонов ниже охватывают все дескрипторов без промежутков. Сводные строки объединяют соседние дескрипторы, но загрузчик всё равно проверяет каждый из них отдельно.
| Запись или группа | Назначение | Тип данных | Форма или число элементов | Размер элемента, байт | Полуоткрытый диапазон байтов |
|---|---|---|---|---|---|
literal-token-0 | явно заданный токен | u8 | |||
от literal-token-1 до literal-token-3 | явно заданные токены | u8 | однобайтовые записи | ||
literal-token-4 | явно заданный токен | u8 | |||
token_embedding.weight | параметр модели | f64 | |||
| остальные параметры и моменты | состояние модели и AdamW | f64 | тензорные записи / скалярных значений |
Последовательность из первых байт достаточно коротка для побайтовой проверки:
4c 4c 4d 43 50 33 35 00 01 00 04 03 02 01 35 0b
Сначала идёт сигнатура LLMCP35, затем версия схемы , маркер порядка байтов
и начало закодированной длины заголовка. Расположение каждой записи данных
определяет таблица, а не правила выравнивания в памяти.
От комплектов файлов модели к проверяемым контрольным точкам LLM
Эта последовательность показывает, какое состояние нужно сохранять, чтобы не потерять смысл языковой модели и возможность продолжить вычисления.
По одному изолированному массиву значений параметров нельзя определить, с какими токенизатором и конфигурацией модели, формами тензоров, моментами оптимизатора и потоком псевдослучайных чисел нужно использовать эти байты. Если хранить эти части в отдельных файлах, со временем они могут перестать соответствовать друг другу.
Загрузчик модели GPT-2 от OpenAI показывает, из каких согласованных файлов состоял выпуск языковой модели в 2019 году. Загрузчик OpenAI получает данные, индекс и метаданные контрольной точки GPT-2 вместе с указателем на неё, гиперпараметрами, данными кодировщика и словарём BPE. Это показывает, что выпущенная модель была комплектом связанных файлов, а не одним изолированным файлом весов. Однако скрипт не описывает единый самодостаточный файл, точное продолжение обучения, восстановление оптимизатора или генератора псевдослучайных чисел, контрольную сумму либо учебный формат этой главы.
ZeRO показывает, почему объём составляющих состояния модели и оптимизатора становится существенным. Раджбхандари и соавторы относят к состоянию обучения крупной модели параметры, градиенты и состояние оптимизатора, например импульс и дисперсию Adam, а затем оценивают и распределяют эти составляющие по мере роста языковых моделей. Из такого учёта следует, что одних весов недостаточно, чтобы определить следующее адаптивное обновление. ZeRO исследует распределение данных в памяти; контрольные точки активаций в этой работе означают повторное вычисление активаций. Работа не задаёт формат долговременного хранения и не требует сохранять градиенты на рассматриваемой границе сразу после обновления.
Более поздняя спецификация формата safetensors задаёт тензорный формат с метаданными типов, форм и смещений. Формат safetensors описывает длину заголовка с порядком байтов от младшего к старшему, тип данных и форму каждого тензора, пары смещений, задающие полуоткрытые диапазоны байтов, и полностью индексированный буфер данных с построчным расположением без промежутков. Сам по себе он не задаёт токенизатор, конфигурацию декодера, оптимизатор, генератор псевдослучайных чисел, контрольную сумму или правила атомарной замены. Цель безопасной загрузки в safetensors отличается от обнаружения случайных повреждений, которому здесь служит контрольная сумма.
Выпуски нейронных языковых моделей включали согласованный набор файлов токенизатора, конфигурации и контрольных точек. По мере роста моделей состояние оптимизатора стало занимать значительную часть сохраняемых данных, а более поздние тензорные контейнеры позволили узнать тип данных, форму и смещения до загрузки значений.
Воспроизводимая контрольная точка LLM сочетает тензорный формат с описанием типов, форм и смещений со схемой приложения, которая явно разделяет сохранённое состояние компонентов и данные, оставленные вызывающему коду. Формат этой главы хранит токенизатор, декодер, состояние AdamW из того же снимка цикла обучения и состояние генератора для выбора токенов, но не обещает возобновить весь процесс обучения.
Путь к воспроизводимым современным LLM объединяет согласованные файлы
токенизатора и конфигурации, состояние оптимизатора и метаданные типов, форм и
смещений тензоров. Схема приложения должна явно указывать, что хранится в файле,
а что по-прежнему обязан передать вызывающий код. Контрольная сумма и правила атомарной замены в этой главе
относятся именно к учебному формату; они не приписываются GPT-2, ZeRO или
safetensors. Исполняемый пример на Rust получает байт из значений
параметров f64 выбранной модели и проверяет, что этот массив совпадает с
данными параметров в полном файле. Затем пример измеряет весь файл размером
байт: в нём есть пять записей токенизатора, именованных записей
параметров, записи моментов оптимизатора и состояние генератора для
выбора токенов. Это сопоставление не описывает GPT-2 или safetensors как
дамп памяти. Учебный формат не совместим с safetensors и не предлагается как
универсальный стандарт.
rust/demos/ch35-checkpoints/src/lib.rs#historical-checkpoint-contrast #[derive(Clone, Debug, PartialEq, Eq)]
pub struct HistoricalCheckpointContrast {
pub isolated_parameter_bytes: Vec<u8>,
pub isolated_parameter_tensors: usize,
pub isolated_parameter_scalars: usize,
pub checkpoint_records: usize,
pub tokenizer_records: usize,
pub optimizer_moment_records: usize,
pub checkpoint_file_bytes: usize,
pub checkpoint_sampling_rng_state: u64,
}
/// Contrasts model-derived value bytes with the complete local LLM checkpoint.
pub fn historical_checkpoint_contrast(
checkpoint: &Checkpoint,
encoded: &llm_from_scratch::checkpoint::EncodedCheckpoint,
) -> HistoricalCheckpointContrast {
let isolated_parameter_bytes = checkpoint
.model_state()
.bit_pattern()
.into_iter()
.flat_map(u64::to_le_bytes)
.collect();
let tokenizer_records = encoded
.tensors()
.iter()
.filter(|descriptor| {
matches!(
descriptor.role(),
CheckpointTensorRole::LiteralToken | CheckpointTensorRole::BpePairs
)
})
.count();
let optimizer_moment_records = encoded
.tensors()
.iter()
.filter(|descriptor| {
matches!(
descriptor.role(),
CheckpointTensorRole::OptimizerFirstMoment
| CheckpointTensorRole::OptimizerSecondMoment
)
})
.count();
HistoricalCheckpointContrast {
isolated_parameter_bytes,
isolated_parameter_tensors: checkpoint.model_state().parameter_names().len(),
isolated_parameter_scalars: checkpoint.model_state().scalar_count(),
checkpoint_records: encoded.tensors().len(),
tokenizer_records,
optimizer_moment_records,
checkpoint_file_bytes: encoded.bytes().len(),
checkpoint_sampling_rng_state: checkpoint.sampling_rng_state(),
}
} Кодируйте и проверяйте данные, затем заменяйте файл атомарно
AdamWStateEntry хранит имя и форму одного параметра, тензор первого момента,
все значения которого конечны, и тензор второго момента с конечными
неотрицательными значениями. AdamWState добавляет конфигурацию,
необязательное разбиение параметров на группы с затуханием весов и без него,
номер шага, точные накопленные степени и , а также таблицу,
ключами в которой служат стабильные имена. На шаге
степени должны быть в точности равны , а моментов быть не должно; на
последующих шагах степени должны принадлежать , а набор моментов — быть
непустым. При загрузке эти степени восстанавливаются напрямую, а не вычисляются
заново с помощью другой последовательности арифметических операций.
rust/crates/llm-from-scratch/src/training/adamw.rs#adamw-persistence-state /// One validated name-keyed moment pair prepared for persistence.
#[derive(Clone, Debug, PartialEq)]
pub struct AdamWStateEntry {
name: String,
moments: AdamWMomentState,
}
impl AdamWStateEntry {
pub fn new(
name: impl Into<String>,
shape: Vec<usize>,
first_moment: Vec<f64>,
second_moment: Vec<f64>,
) -> Result<Self, AdamWStateError> {
let name = name.into();
if name.is_empty() {
return Err(AdamWStateError::EmptyParameterName);
}
let elements = shape.iter().try_fold(1_usize, |product, &dimension| {
product.checked_mul(dimension)
});
let Some(elements) = elements else {
return Err(AdamWStateError::ShapeProductOverflow { name });
};
if first_moment.len() != elements || second_moment.len() != elements {
return Err(AdamWStateError::MomentLengthMismatch {
name,
shape,
expected: elements,
first: first_moment.len(),
second: second_moment.len(),
});
}
for (kind, values) in [
("first", first_moment.as_slice()),
("second", second_moment.as_slice()),
] {
if let Some((index, &value)) = values
.iter()
.enumerate()
.find(|(_, value)| !value.is_finite())
{
return Err(AdamWStateError::NonFiniteMoment {
name,
kind,
index,
value,
});
}
}
if let Some((index, &value)) = second_moment
.iter()
.enumerate()
.find(|(_, value)| **value < 0.0)
{
return Err(AdamWStateError::NegativeSecondMoment { name, index, value });
}
Ok(Self {
name,
moments: AdamWMomentState {
shape,
first: first_moment,
second: second_moment,
},
})
}
pub fn name(&self) -> &str {
&self.name
}
pub const fn moments(&self) -> &AdamWMomentState {
&self.moments
}
}
/// A complete graph-free AdamW continuation snapshot.
#[derive(Clone, Debug, PartialEq)]
pub struct AdamWState {
config: AdamWConfig,
groups: Option<AdamWParameterGroups>,
step: u64,
beta1_power: f64,
beta2_power: f64,
states: BTreeMap<String, AdamWMomentState>,
}
impl AdamWState {
pub fn new(
config: AdamWConfig,
groups: Option<AdamWParameterGroups>,
step: u64,
beta1_power: f64,
beta2_power: f64,
entries: Vec<AdamWStateEntry>,
) -> Result<Self, AdamWStateError> {
validate_beta_power("beta1", beta1_power, step)?;
validate_beta_power("beta2", beta2_power, step)?;
if (step == 0) != entries.is_empty() {
return Err(AdamWStateError::StatePresence {
step,
entries: entries.len(),
});
}
let mut states = BTreeMap::new();
for entry in entries {
if states.insert(entry.name.clone(), entry.moments).is_some() {
return Err(AdamWStateError::DuplicateParameterName { name: entry.name });
}
}
if let Some(groups) = &groups {
let expected = groups.parameter_names();
let actual = states.keys().cloned().collect::<Vec<_>>();
if step > 0 && expected != actual {
return Err(AdamWStateError::ParameterGroupsMismatch { expected, actual });
}
}
Ok(Self {
config,
groups,
step,
beta1_power,
beta2_power,
states,
})
}
pub const fn config(&self) -> AdamWConfig {
self.config
}
pub const fn parameter_groups(&self) -> Option<&AdamWParameterGroups> {
self.groups.as_ref()
}
pub const fn step_count(&self) -> u64 {
self.step
}
pub const fn beta1_power(&self) -> f64 {
self.beta1_power
}
pub const fn beta2_power(&self) -> f64 {
self.beta2_power
}
pub fn parameter_names(&self) -> impl ExactSizeIterator<Item = &str> {
self.states.keys().map(String::as_str)
}
pub fn state(&self, name: &str) -> Option<&AdamWMomentState> {
self.states.get(name)
}
}
fn validate_beta_power(name: &'static str, value: f64, step: u64) -> Result<(), AdamWStateError> {
let valid = if step == 0 {
value.to_bits() == 1.0_f64.to_bits()
} else {
value.is_finite() && (0.0..1.0).contains(&value)
};
if valid {
Ok(())
} else {
Err(AdamWStateError::InvalidBetaPower { name, value, step })
}
}
/// A malformed optimizer snapshot rejected before an AdamW instance is built.
#[derive(Clone, Debug, PartialEq)]
pub enum AdamWStateError {
InvalidBetaPower {
name: &'static str,
value: f64,
step: u64,
},
StatePresence {
step: u64,
entries: usize,
},
EmptyParameterName,
DuplicateParameterName {
name: String,
},
ShapeProductOverflow {
name: String,
},
MomentLengthMismatch {
name: String,
shape: Vec<usize>,
expected: usize,
first: usize,
second: usize,
},
NonFiniteMoment {
name: String,
kind: &'static str,
index: usize,
value: f64,
},
NegativeSecondMoment {
name: String,
index: usize,
value: f64,
},
ParameterGroupsMismatch {
expected: Vec<String>,
actual: Vec<String>,
},
}
impl fmt::Display for AdamWStateError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidBetaPower { name, value, step } => write!(
formatter,
"{name} power must be exactly 1 at step zero or finite in [0,1) later, got {value} at step {step}"
),
Self::StatePresence { step, entries } => write!(
formatter,
"AdamW step {step} is incompatible with {entries} persisted moment entries"
),
Self::EmptyParameterName => {
formatter.write_str("an AdamW persistence entry has an empty parameter name")
}
Self::DuplicateParameterName { name } => write!(
formatter,
"AdamW persistence parameter name {name:?} appears more than once"
),
Self::ShapeProductOverflow { name } => write!(
formatter,
"AdamW persistence shape for parameter {name:?} overflows usize"
),
Self::MomentLengthMismatch {
name,
shape,
expected,
first,
second,
} => write!(
formatter,
"AdamW persistence parameter {name:?} with shape {shape:?} needs {expected} moments, got {first} first and {second} second"
),
Self::NonFiniteMoment {
name,
kind,
index,
value,
} => write!(
formatter,
"AdamW persistence parameter {name:?} has non-finite {kind} moment at flat index {index}: {value}"
),
Self::NegativeSecondMoment { name, index, value } => write!(
formatter,
"AdamW persistence parameter {name:?} has negative second moment at flat index {index}: {value}"
),
Self::ParameterGroupsMismatch { expected, actual } => write!(
formatter,
"AdamW persistence groups name {expected:?}, but moments name {actual:?}"
),
}
}
}
impl Error for AdamWStateError {} После создания CheckpointTokenizer изменить нельзя. Конструктор явно заданного
словаря отклоняет пустой словарь, пустое байтовое представление токена и
повторяющиеся представления. Конструктор BPE принимает уже проверенный
токенизатор. Пары, прочитанные из недоверенного файла, попадают в состояние лишь
после того, как существующий конструктор BPE проверит их во время загрузки. Оба
пути сохраняют получившийся размер словаря. У структуры нет открытого метода,
который мог бы изменить токены или пары BPE и оставить прежний размер словаря. В
файле также записаны версии формата токенизатора и алгоритма SplitMix64.
Сохранённое состояние генератора продолжает только поток для последующего
выбора токенов; это не начальное значение для перемешивания пакетов из главы 33.
При создании контрольной точки из снимка и при загрузке файла различается
владение исходными данными. Checkpoint::from_snapshot получает по ссылке
состояние, выданное циклом обучения, в котором объединены снимок модели,
состояние AdamW, зафиксированное одновременно с моделью, и их общий номер шага.
Поскольку этот снимок должен остаться доступным после вызова, метод копирует его буферы в новую
независимую контрольную точку.
Checkpoint::from_bytes, напротив, возвращает контрольную точку, которой
принадлежат буферы тензоров, полученные при декодировании файла.
restore_independent_model копирует состояние модели, чтобы контрольная точка
осталась доступной. into_model забирает контрольную точку целиком и переносит
её буферы модели. Оба способа восстанавливают одинаковые значения параметров, а
созданный декодер связывает входные эмбеддинги и выходную проекцию с одним общим
параметром. Разница в том, можно ли после восстановления продолжать пользоваться
контрольной точкой. Само состояние без вычислительного графа хранит один тензор
token_embedding.weight и не содержит отдельного параметра выходной проекции.
В таком состоянии ещё нет компонентов декодера и общей действующей ссылки на
этот тензор; связь появляется только при восстановлении модели.
rust/crates/llm-from-scratch/src/checkpoint.rs#checkpoint-state-transfer /// Copies one trainer-issued selected model/AdamW/step bundle.
///
/// The sealed bundle prevents callers from attaching a free step label or an
/// independently chosen optimizer to the selected model state.
pub fn from_snapshot(
tokenizer: CheckpointTokenizer,
selected: &SelectedTrainingState,
sampling_rng_state: u64,
) -> Result<Self, CheckpointError> {
let selected_step =
u64::try_from(selected.step()).map_err(|_| CheckpointError::SizeOverflow {
context: "selected training step",
})?;
Self::from_owned_parts(
tokenizer,
selected.model_state().independent_snapshot(),
selected.optimizer_state().clone(),
selected_step,
sampling_rng_state,
)
}
fn from_owned_parts(
tokenizer: CheckpointTokenizer,
model_state: DecoderModelState,
optimizer_state: AdamWState,
selected_step: u64,
sampling_rng_state: u64,
) -> Result<Self, CheckpointError> {
let checkpoint = Self {
tokenizer,
model_state,
optimizer_state,
selected_step,
sampling_rng_state,
};
checkpoint.validate_parts()?;
Ok(checkpoint)
}
pub const fn tokenizer(&self) -> &CheckpointTokenizer {
&self.tokenizer
}
pub const fn model_state(&self) -> &DecoderModelState {
&self.model_state
}
pub const fn optimizer_state(&self) -> &AdamWState {
&self.optimizer_state
}
pub const fn selected_step(&self) -> u64 {
self.selected_step
}
pub const fn sampling_rng_state(&self) -> u64 {
self.sampling_rng_state
}
/// Compatibility accessor for the version-1 sampling RNG state.
pub const fn rng_state(&self) -> u64 {
self.sampling_rng_state()
}
/// Rebuilds an independent decoder while retaining the checkpoint.
pub fn restore_independent_model(&self) -> Result<DecoderModel, CheckpointError> {
self.model_state
.restore_independent_model()
.map_err(Into::into)
}
/// Consumes the checkpoint and moves its model buffers into one decoder.
pub fn into_model(self) -> Result<DecoderModel, CheckpointError> {
self.model_state.into_model().map_err(Into::into)
}
pub fn restore_optimizer(&self) -> AdamW {
AdamW::from_persistence_state(&self.optimizer_state)
} Конструкторы и загрузчик проверяют согласованность токенизатора, модели и
оптимизатора до того, как контрольная точка становится доступна вызывающему
коду. Поля Checkpoint закрыты, а открытые методы не позволяют их изменить,
поэтому encode не проверяет согласованность частей повторно. Вместо этого
кодировщик строит план записей. План самостоятельно хранит имена, формы,
назначения, типы данных и будущие смещения. Каждое значение TensorPayload
ссылается на байты явно заданного токена, срез пар BPE или срез значений f64;
тензоры модели и моменты AdamW используют вариант TensorPayload::Float64. План
не создаёт отдельный вектор закодированных байтов для каждой записи.
rust/crates/llm-from-scratch/src/checkpoint.rs#checkpoint-record-planning #[derive(Clone, Copy, Debug)]
enum TensorPayload<'a> {
Bytes(&'a [u8]),
BpePairs(&'a [TokenPair]),
Float64(&'a [f64]),
}
impl TensorPayload<'_> {
const fn dtype(self) -> CheckpointDType {
match self {
Self::Bytes(_) => CheckpointDType::U8,
Self::BpePairs(_) => CheckpointDType::U32,
Self::Float64(_) => CheckpointDType::F64,
}
}
fn byte_len(self) -> Result<usize, CheckpointError> {
match self {
Self::Bytes(values) => Ok(values.len()),
Self::BpePairs(pairs) => pairs
.len()
.checked_mul(2)
.and_then(|values| values.checked_mul(CheckpointDType::U32.byte_width()))
.ok_or(CheckpointError::SizeOverflow {
context: "BPE pair payload",
}),
Self::Float64(values) => values
.len()
.checked_mul(CheckpointDType::F64.byte_width())
.ok_or(CheckpointError::SizeOverflow {
context: "f64 tensor payload",
}),
}
}
fn write_to(self, bytes: &mut Vec<u8>) {
match self {
Self::Bytes(values) => bytes.extend_from_slice(values),
Self::BpePairs(pairs) => {
for pair in pairs {
put_u32(bytes, pair.left());
put_u32(bytes, pair.right());
}
}
Self::Float64(values) => {
for &value in values {
put_f64(bytes, value);
}
}
}
}
}
/// Owns record metadata while borrowing the values that will become file bytes.
#[derive(Debug)]
struct TensorRecordPlan<'a> {
descriptor: CheckpointTensorDescriptor,
payload: TensorPayload<'a>,
} Кодировщик измеряет предварительный вариант заголовка, назначает все абсолютные
смещения с проверкой переполнения и вычисляет полный размер файла. Затем он
резервирует один итоговый Vec<u8> для заголовка и всех записей данных, записывает
заголовок и таблицу дескрипторов и сразу преобразует каждое значение из плана в
канонические байты с порядком от младшего к старшему внутри итогового буфера.
FNV-1a охватывает весь файл; во время вычисления поле самой контрольной суммы
считается нулевым.
rust/crates/llm-from-scratch/src/checkpoint.rs#versioned-checkpoint-encoding /// Encodes one canonical little-endian file without native-memory casts.
pub fn encode(&self) -> Result<EncodedCheckpoint, CheckpointError> {
let mut records = self.tensor_record_plan()?;
let model_parameter_count = self.model_state.parameter_names().len();
let optimizer_state_count = self.optimizer_state.parameter_names().len();
let mut provisional_header = Vec::new();
write_fixed_header(&mut provisional_header, 0, 0, 0);
write_metadata(
&mut provisional_header,
self,
model_parameter_count,
optimizer_state_count,
&records,
)?;
let header_bytes = usize_to_u64(provisional_header.len(), "header length")?;
let mut next_offset = header_bytes;
let mut payload_bytes = 0_u64;
for record in &mut records {
record.descriptor.offset = next_offset;
next_offset = next_offset.checked_add(record.descriptor.byte_len).ok_or(
CheckpointError::SizeOverflow {
context: "tensor end offset",
},
)?;
payload_bytes = payload_bytes
.checked_add(record.descriptor.byte_len)
.ok_or(CheckpointError::SizeOverflow {
context: "payload length",
})?;
}
let total_bytes =
header_bytes
.checked_add(payload_bytes)
.ok_or(CheckpointError::SizeOverflow {
context: "complete file length",
})?;
let total_capacity = u64_to_usize(total_bytes, "complete file length")?;
let mut bytes = Vec::new();
bytes
.try_reserve_exact(total_capacity)
.map_err(|_| CheckpointError::Allocation {
context: "complete checkpoint file",
})?;
write_fixed_header(&mut bytes, header_bytes, payload_bytes, 0);
write_metadata(
&mut bytes,
self,
model_parameter_count,
optimizer_state_count,
&records,
)?;
if bytes.len() != u64_to_usize(header_bytes, "header length")? {
return Err(CheckpointError::Layout(
"two-pass header length changed".to_owned(),
));
}
for record in &records {
record.payload.write_to(&mut bytes);
}
if bytes.len() != total_capacity {
return Err(CheckpointError::Layout(
"encoded file length changed after layout".to_owned(),
));
}
let checksum = checkpoint_checksum(&bytes);
bytes[CHECKSUM_OFFSET..CHECKSUM_OFFSET + CHECKSUM_WIDTH]
.copy_from_slice(&checksum.to_le_bytes());
Ok(EncodedCheckpoint {
bytes,
header_bytes,
checksum,
tensors: records
.into_iter()
.map(|record| record.descriptor)
.collect(),
})
} Это не сериализация без выделения памяти и не сериализация без копирования
данных. Память по-прежнему выделяется отдельно для дескрипторов,
предварительного заголовка и итогового буфера всего файла. Каждое числовое
значение явно кодируется в порядке байтов от младшего к старшему; его внутреннее
представление в памяти текущего процессора не копируется в файл как готовая
последовательность байтов. Данные также не записываются на диск потоком:
save_atomic сначала получает весь закодированный буфер, а затем записывает и
синхронизирует его в каталоге назначения. Удалены только отдельные буферы
закодированных данных каждой записи. В совокупности они хранили лишнюю копию
всех байтов данных до сборки итогового файла.
Сначала загрузчик проверяет фиксированную часть заголовка, полный размер файла,
контрольную сумму, известные назначения и типы данных, а также диапазоны
дескрипторов; лишь затем он декодирует состояние, которое будет храниться в
контрольной точке. После чтения токенизатора каждая запись модели должна быть
помечена как параметр модели и иметь тип f64. Её байты образуют один Tensor;
на этом этапе загрузки проверяются имя параметра и конечность каждого значения.
DecoderModelState сохраняет эти буферы и предоставляет их упорядоченный список
через DecoderParameterSource. Этот интерфейс позволяет общей проверке структуры
декодера читать имена по ссылке и передаёт каждый тензор функции проверки только
на время одного вызова. Проверка сверяет конфигурацию, число и порядок
параметров, имена, формы и единственную запись token_embedding.weight. Для
этого не копируется ни один тензор, не создаются объекты NamedParameter или
компоненты декодера и не возникает общая ссылка входных эмбеддингов и выходной
проекции на действующий параметр. Вся эта проверка завершается до декодирования
тензоров оптимизатора.
rust/crates/llm-from-scratch/src/training/trainer.rs#decoder-state-layout-validation impl DecoderParameterSource for DecoderModelState {
fn len(&self) -> usize {
self.parameters.len()
}
fn name(&self, index: usize) -> &str {
&self.parameters[index].name
}
fn with_tensor<R>(&self, index: usize, inspect: impl FnOnce(&Tensor) -> R) -> R {
inspect(&self.parameters[index].value)
}
}
impl DecoderModelState {
/// Builds graph-free state after the caller has validated every parameter leaf.
pub(crate) fn try_from_leaf_validated_parameters(
config: DecoderModelConfig,
parameters: Vec<(String, Tensor)>,
) -> Result<Self, TrainerError> {
let state = Self {
config,
parameters: parameters
.into_iter()
.map(|(name, value)| StateParameter { name, value })
.collect(),
};
validate_parameter_layout(config, &state)?;
Ok(state)
}
} Затем загрузчик проверяет оптимизатор, согласованность токенизатора, модели и
оптимизатора и точное каноническое повторное кодирование. Лишь после всех
проверок он возвращает Checkpoint, которому принадлежат декодированные буферы
модели и оптимизатора. При ошибке на любом этапе загрузчик не возвращает
контрольную точку.
rust/crates/llm-from-scratch/src/checkpoint.rs#validated-checkpoint-loading /// Rejects the complete file before exposing any partially restored state.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CheckpointError> {
if bytes.len() < FIXED_HEADER_BYTES {
return Err(CheckpointError::Truncated {
context: "fixed header",
});
}
if bytes[..CHECKPOINT_MAGIC.len()] != CHECKPOINT_MAGIC {
return Err(CheckpointError::InvalidMagic);
}
let version = u16::from_le_bytes(
bytes[8..10]
.try_into()
.expect("the fixed header length was checked"),
);
if version != CHECKPOINT_VERSION {
return Err(CheckpointError::UnsupportedVersion { found: version });
}
let endian = u32::from_le_bytes(
bytes[10..14]
.try_into()
.expect("the fixed header length was checked"),
);
if endian != LITTLE_ENDIAN_MARKER {
return Err(CheckpointError::UnsupportedEndianness { found: endian });
}
let header_bytes = read_fixed_u64(bytes, 14);
let payload_bytes = read_fixed_u64(bytes, 22);
let stored_checksum = read_fixed_u64(bytes, CHECKSUM_OFFSET);
let declared_total = header_bytes.checked_add(payload_bytes);
if header_bytes < usize_to_u64(FIXED_HEADER_BYTES, "fixed header length")?
|| declared_total != Some(usize_to_u64(bytes.len(), "file length")?)
{
return Err(CheckpointError::FileExtent {
header: header_bytes,
payload: payload_bytes,
actual: bytes.len(),
});
}
let header_end = u64_to_usize(header_bytes, "header length")?;
if header_end > bytes.len() {
return Err(CheckpointError::FileExtent {
header: header_bytes,
payload: payload_bytes,
actual: bytes.len(),
});
}
let actual_checksum = checkpoint_checksum(bytes);
if stored_checksum != actual_checksum {
return Err(CheckpointError::ChecksumMismatch {
expected: stored_checksum,
actual: actual_checksum,
});
}
let header = decode_variable_header(&bytes[FIXED_HEADER_BYTES..header_end])?;
validate_descriptors(&header, header_bytes, bytes.len())?;
let tokenizer_count = match header.tokenizer_kind {
1 => header.tokenizer_vocabulary,
2 => 1,
tag => return Err(CheckpointError::UnknownTokenizerKind { tag }),
};
let tokenizer_end = tokenizer_count;
let model_end = tokenizer_end
.checked_add(header.model_parameter_count)
.ok_or(CheckpointError::SizeOverflow {
context: "model descriptor boundary",
})?;
let optimizer_descriptor_count =
header
.optimizer_state_count
.checked_mul(2)
.ok_or(CheckpointError::SizeOverflow {
context: "optimizer descriptor count",
})?;
let expected_total = model_end.checked_add(optimizer_descriptor_count).ok_or(
CheckpointError::SizeOverflow {
context: "optimizer descriptor boundary",
},
)?;
if header.descriptors.len() != expected_total {
return Err(CheckpointError::Layout(format!(
"descriptor table has {} records, expected {expected_total}",
header.descriptors.len()
)));
}
let tokenizer = decode_tokenizer(
header.tokenizer_kind,
header.tokenizer_vocabulary,
&header.descriptors[..tokenizer_end],
bytes,
)?;
let model_state = decode_model(
header.config,
&header.descriptors[tokenizer_end..model_end],
bytes,
)?;
let optimizer_state = decode_optimizer(
header.optimizer_config,
header.optimizer_groups,
header.optimizer_step,
header.beta1_power,
header.beta2_power,
&header.descriptors[model_end..],
bytes,
)?;
let checkpoint = Self::from_owned_parts(
tokenizer,
model_state,
optimizer_state,
header.selected_step,
header.sampling_rng_state,
)?;
let canonical = checkpoint.encode()?;
if canonical.bytes() != bytes {
return Err(CheckpointError::NonCanonical(
"decoded state does not reproduce the original bytes".to_owned(),
));
}
Ok(checkpoint)
} При сохранении в каталоге назначения через create_new создаётся уникальный
временный файл. В него полностью записываются и синхронизируются данные, после
чего он переименовывается с заменой целевого файла, а каталог синхронизируется. В
поддерживаемом сценарии Unix это атомарная замена в пределах одной файловой
системы. На других платформах возвращается явная ошибка о неподдерживаемой
операции, а предыдущая контрольная точка не удаляется.
rust/crates/llm-from-scratch/src/checkpoint.rs#atomic-checkpoint-save /// Replaces one file through a synchronized same-directory temporary name.
///
/// The course's supported Unix workflow relies on same-filesystem rename
/// semantics. Other targets return an explicit error instead of deleting an
/// existing destination before a non-atomic move.
pub fn save_atomic(
&self,
path: impl AsRef<Path>,
) -> Result<EncodedCheckpoint, CheckpointError> {
#[cfg(not(unix))]
{
let _ = path;
return Err(CheckpointError::UnsupportedAtomicReplacement);
}
#[cfg(unix)]
{
let path = path.as_ref();
let file_name = path.file_name().ok_or_else(|| {
CheckpointError::Layout("checkpoint destination has no file name".to_owned())
})?;
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let encoded = self.encode()?;
let (temporary_path, mut temporary) =
create_temporary(parent, file_name.to_string_lossy().as_ref())?;
let publication = (|| -> Result<(), CheckpointError> {
temporary
.write_all(encoded.bytes())
.map_err(|source| CheckpointError::Io {
operation: "write temporary",
path: temporary_path.clone(),
source,
})?;
temporary.sync_all().map_err(|source| CheckpointError::Io {
operation: "synchronize temporary",
path: temporary_path.clone(),
source,
})?;
drop(temporary);
fs::rename(&temporary_path, path).map_err(|source| CheckpointError::Io {
operation: "atomically replace",
path: path.to_owned(),
source,
})?;
File::open(parent)
.and_then(|directory| directory.sync_all())
.map_err(|source| CheckpointError::Io {
operation: "synchronize parent directory",
path: parent.to_owned(),
source,
})?;
Ok(())
})();
if publication.is_err() {
let _ = fs::remove_file(&temporary_path);
}
publication?;
Ok(encoded)
}
} Исполняемому примеру нужны две независимые ветви повтора одного обновления.
Исходная контрольная точка позже используется для проверки повреждений и
атомарного сохранения, поэтому метод restore_independent_model создаёт для
этой ветви независимую копию модели. Загруженная контрольная точка после
подготовки второй ветви больше не нужна. Пример сначала получает из неё
состояние AdamW и сохранённое состояние генератора для выбора токенов, затем
передаёт контрольную точку целиком методу into_model; тот переносит её тензоры
во второй декодер. Вызывающий код передаёт обеим ветвям входы , цели
и скорость обучения . Пример напрямую вычисляет функцию потерь,
выполняет обратное распространение и одно обновление AdamW, после чего сравнивает
все биты параметров, точное состояние оптимизатора, логиты и следующее значение
SplitMix64 для выбора токенов. Он не вызывает train_decoder, не восстанавливает
корпус или текущую позицию пакетов и не применяет расписание скорости обучения,
ограничение нормы градиента или правила валидации из главы 33.
rust/demos/ch35-checkpoints/src/lib.rs#learner-evidence pub fn learner_evidence() -> Result<LearnerEvidence, FixtureError> {
let selected = selection_evidence()?;
let selected_training_state = selected.result.selected_training_state();
let selected_step = u64::try_from(selected.result.selected_step())
.map_err(|_| FixtureError::Invariant("selected step does not fit u64"))?;
require(
selected_step == selected_training_state.optimizer_state().step_count(),
"trainer-issued selected model and optimizer no longer share one step",
)?;
let mut sampling_rng = SplitMix64::from_seed(SAMPLING_RNG_SEED);
let _ = sampling_rng.next_u64();
let saved_sampling_rng_state = sampling_rng.state();
let expected_sampling_rng_next = sampling_rng.next_u64();
let checkpoint = Checkpoint::from_snapshot(
literal_tokenizer()?,
selected_training_state,
saved_sampling_rng_state,
)?;
let encoded = checkpoint.encode()?;
let repeated = checkpoint.encode()?;
let loaded = Checkpoint::from_bytes(encoded.bytes())?;
let loaded_encoded = loaded.encode()?;
let original_model = checkpoint.restore_independent_model()?;
let loaded_optimizer = loaded.restore_optimizer();
let loaded_sampling_rng_state = loaded.sampling_rng_state();
let loaded_model = loaded.into_model()?;
let original_logits = logits_bits(&original_model)?;
let loaded_logits = logits_bits(&loaded_model)?;
let (updated_original, updated_original_optimizer) = apply_component_update(
original_model,
checkpoint.restore_optimizer(),
&LOGIT_INPUTS,
&LOGIT_TARGETS,
NEXT_LEARNING_RATE,
)?;
let (updated_loaded, updated_loaded_optimizer) = apply_component_update(
loaded_model,
loaded_optimizer,
&LOGIT_INPUTS,
&LOGIT_TARGETS,
NEXT_LEARNING_RATE,
)?;
let updated_original_bits = parameter_bits(&updated_original);
let updated_loaded_bits = parameter_bits(&updated_loaded);
let updated_original_logits = logits_bits(&updated_original)?;
let updated_loaded_logits = logits_bits(&updated_loaded)?;
let (changed_batch_model, changed_batch_optimizer) = apply_component_update(
checkpoint.restore_independent_model()?,
checkpoint.restore_optimizer(),
&[1, 2],
&[2, 3],
NEXT_LEARNING_RATE,
)?;
let changed_batch_diverges = parameter_bits(&changed_batch_model) != updated_original_bits
|| changed_batch_optimizer != updated_original_optimizer;
let (changed_learning_rate_model, changed_learning_rate_optimizer) = apply_component_update(
checkpoint.restore_independent_model()?,
checkpoint.restore_optimizer(),
&LOGIT_INPUTS,
&LOGIT_TARGETS,
NEXT_LEARNING_RATE * 2.0,
)?;
let changed_learning_rate_diverges = parameter_bits(&changed_learning_rate_model)
!= updated_original_bits
|| changed_learning_rate_optimizer != updated_original_optimizer;
let loaded_sampling_rng_next = SplitMix64::from_state(loaded_sampling_rng_state).next_u64();
let corruption = corruption_evidence(selected_training_state, &checkpoint, encoded.bytes())?;
let atomic = atomic_evidence(selected_training_state, &checkpoint)?;
require(
encoded.tensors().len() == 38,
"checkpoint record count changed",
)?;
require(
checkpoint.model_state().parameter_names().len() == 11,
"model parameter tensor count changed",
)?;
require(
checkpoint.model_state().scalar_count() == 144,
"model scalar count changed",
)?;
require(
updated_original_optimizer.step_count() == selected_step + 1,
"component-replay optimizer step count changed",
)?;
require(
changed_batch_diverges && changed_learning_rate_diverges,
"caller-supplied batch or learning-rate adversary no longer diverges",
)?;
require(
corruption.version_rejected
&& corruption.vocabulary_mismatch_rejected
&& corruption.step_mismatch_rejected
&& corruption.truncation_rejected
&& corruption.checksum_rejected,
"one corruption fixture was accepted",
)?;
require(
atomic.replaced_complete_file && atomic.temporary_files == 0,
"atomic replacement evidence changed",
)?;
let history = historical_checkpoint_contrast(&checkpoint, &encoded);
require(
history.isolated_parameter_bytes.len()
== history.isolated_parameter_scalars * std::mem::size_of::<f64>(),
"isolated parameter byte length changed",
)?;
require(
history.tokenizer_records
+ history.isolated_parameter_tensors
+ history.optimizer_moment_records
== history.checkpoint_records,
"checkpoint record-family counts changed",
)?;
Ok(LearnerEvidence {
checkpoint,
bytes_deterministic: encoded == repeated,
round_trip_identical: encoded == loaded_encoded,
logits_before_bits_identical: original_logits == loaded_logits,
logits_before_fingerprint: bits_fingerprint(&loaded_logits),
parameter_bits_after_identical: updated_original_bits == updated_loaded_bits,
optimizer_after_identical: updated_original_optimizer == updated_loaded_optimizer,
logits_after_bits_identical: updated_original_logits == updated_loaded_logits,
logits_after_fingerprint: bits_fingerprint(&updated_loaded_logits),
changed_batch_diverges,
changed_learning_rate_diverges,
sampling_rng_next_identical: expected_sampling_rng_next == loaded_sampling_rng_next,
sampling_rng_next: loaded_sampling_rng_next,
corruption,
atomic,
history,
encoded,
})
} Исполняемый отчёт фиксирует пять измеряемых границ:
roundtrip=bytes_deterministic:true loaded_bytes_identical:true logits_bits_identical:true logits_fingerprint:fnv1a64:6029064fe7cd162d sampling_rng_next_identical:true sampling_rng_next:0x9a8c505971939232
component_replay=caller_inputs:[0,1] caller_targets:[1,2] caller_learning_rate:0.006000 next_step:9 parameter_bits_identical:true optimizer_state_identical:true logits_bits_identical:true logits_fingerprint:fnv1a64:0b875a0c9f380d8f changed_batch_diverges:true changed_learning_rate_diverges:true
scope=tokenizer:stored model:stored optimizer:stored selected_step:stored optimizer_step:stored optimizer_base_learning_rate:stored sampling_rng:stored step_equality:validated model_lineage:not_stored corpus_identity:not_stored split_identity:not_stored epoch_materialization:not_stored epoch_cursor:not_stored batch_order:not_stored batch_cursor:not_stored shuffle_rng:not_stored training_rng:not_stored learning_rate_schedule:not_stored next_learning_rate:not_stored clipping_policy:not_stored validation_policy:not_stored gradients:not_stored trainer_capture:creation_required caller_next_batch:required caller_next_learning_rate:required clean_post_update:required whole_job_resume:false
reject=version:true vocabulary_mismatch:true step_mismatch:true truncation:true checksum:true
atomic=replaced_complete_file:true loaded_sampling_rng_state:0x9e3779b97f4a7c39 temporary_files:0 unix_same_directory:true
Поля о расхождении делают условие проверяемым: если изменить пакет или скорость обучения, результат изменится. Совпадение относится к одному заданному обновлению, а не к не сохранённой в файле траектории обучения.
Полный исполняемый пример также проверяет BPE на уровне байтов, разные размеры элементов, неподдерживаемые версии и порядок байтов, повреждение метаданных, лишние байты, несоответствия токенизатора и модели, отрицательные вторые моменты и детерминированное повторное кодирование.
rust/demos/ch35-checkpoints/src/main.rs fn main() -> Result<(), Box<dyn std::error::Error>> {
print!("{}", ch35_checkpoints::learner_report()?);
Ok(())
} Проверьте расположение байтов, прежде чем доверять данным
Загрузчику контрольной точки нужны точные порядок записей, размер элемента, форма и диапазон байтов. Блок-схема отбросила бы эти детали, а таблица дескрипторов сохраняет их для непосредственной проверки.
Читайте таблицу сверху вниз. Каждая строка должна начинаться точно там, где заканчивается предыдущая; её длина должна соответствовать типу данных и форме; последняя не включённая правая граница должна совпадать с объявленной длиной файла. Фиксированный префикс из байт позволяет сопоставить сигнатуру, версию схемы, маркер порядка байтов и начало длины заголовка с теми же байтами, которые получает загрузчик.
Поля строки reject показывают пять независимых причин отказа: неизвестную
версию, несовместимый словарь, несовпадающие номера шага, усечённый файл и
изменение проверяемого байта. Строки roundtrip, component_replay и scope
разделяют точное воспроизведение сохранённого состояния, одно обновление с
данными от вызывающего кода и состояние полного цикла обучения, которое осталось за пределами
файла.
Интерпретируйте результаты сдержанно. В этом примере
logits_bits_identical:true подтверждает точное побитовое воспроизведение
логитов. checksum:true показывает, что файл с одним изменённым проверяемым
байтом отклоняется, но не превращает FNV-1a в криптографическую подпись.
Предскажите смещения, затем проверьте повреждённые файлы
- Начните со смещения и тензора
f64формы . Предскажите его не включённую правую границу. - Тензор пар BPE формы хранит значения
u32. Сколько байтов он занимает? - Длина метки одного явно заданного токена увеличивается с одного байта до четырёх. Какие последующие смещения изменятся?
- Значения параметров не изменились, но изменилась форма в дескрипторе. Должна ли загрузка пройти, если контрольная сумма охватывает и метаданные, и значения?
- Веса загрузились точно, но моменты AdamW сброшены. Совпадёт ли следующее обновление?
- В недоверенном файле указан выбранный шаг , а счётчик AdamW остаётся на шаге . Какая проверка отклонит файл?
- Процесс останавливается после синхронизации временного файла, но до переименования. Какая версия целевого файла остаётся доступной?
- Доказывает ли совпадение FNV-1a, что злоумышленник не менял файл?
Проверьте ход рассуждений
- , потому что .
- байта.
- Не включённая правая граница записи изменённой метки и все последующие абсолютные смещения сдвигаются на три байта; каноническое повторное кодирование также меняет проверяемые метаданные.
- Нет. Поскольку контрольная сумма охватывает метаданные, при изменении формы её значение изменится. Даже с заново вычисленной контрольной суммой несогласованный дескриптор будет отклонён при проверке формы.
- Нет. Следующее адаптивное направление AdamW зависит от первого и второго моментов и накопленных степеней и .
- При загрузке записанный выбранный шаг должен совпадать со счётчиком AdamW.
- Доступной остаётся предыдущая полная версия целевого файла; неопубликованный временный файл можно удалить.
- Нет. FNV-1a обнаруживает случайные повреждения, но не подтверждает подлинность.
Главное заблуждение — считать контрольную точку либо только весами, либо готовым
снимком для полного возобновления цикла обучения. Этот файл позволяет однозначно
интерпретировать декодер, восстановить состояние AdamW из того же снимка цикла обучения
и продолжить отдельный поток случайных чисел для выбора токенов. Для продолжения
обучения также нужны корпус и
разбиение, порядок и текущая позиция пакетов, генератор обучения, входы и цели
следующего обновления, следующая скорость обучения и её расписание, а также
правила ограничения градиента и валидации. В примере один пакет и скорость
обучения задаются вручную; train_decoder не возобновляется.
Перед выбором токена загрузите то же состояние
Теперь в проверенном файле можно сохранить тот же смысл токенов, архитектуру и
биты параметров декодера, состояние AdamW из того же снимка цикла обучения, их общий
номер шага и отдельный поток случайных чисел для выбора токенов. Прежде чем вернуть
Checkpoint, загрузчик проверяет токенизатор, конфигурацию, имена, формы и
значения параметров. Поэтому декодированные значения снова можно однозначно
интерпретировать как параметры именно этой модели. Чтобы повторить одно заданное
обновление, вызывающий код всё равно должен передать те же входы, цели и скорость
обучения.
Глава 36 загрузит эту контрольную точку перед преобразованием логитов в выбор токена с помощью температуры и top-. В следующей главе можно будет менять правила случайного выбора токена, не подменяя токенизатор, конфигурацию модели, веса, состояние AdamW из того же снимка цикла обучения или поток случайных чисел для выбора токенов. Это не превращает файл в снимок для полного возобновления цикла обучения из главы 33.