33 · Версия материала 10
Выполните все шаги обучения и выберите модель по валидации
Разберитесь, как цикл обучения декодера упорядочивает обратное распространение, ограничение нормы градиента, шаги AdamW по расписанию, валидацию без записи графа и выбор состояния без обращения к тестовым данным.
Задайте весь план до начала обучения
Выполните все шаги плана обучения декодера с заранее заданным числом обновлений, измерьте потери на валидационной выборке без записи графа в фиксированных контрольных точках и восстановите состояние модели, сохранённое в самой ранней точке с минимальными потерями. В пределах этого запуска обучения не обращайтесь к тестовым данным. В примере обучается одноблочный декодер из предыдущей главы: размер словаря , ширина остаточного потока , две головы внимания, ширина прямого распространения , длина контекста и обучаемых скаляра.
Детерминированное обучение выполняет восемь заданных обновлений на мини-пакетах. Используются четырёхсегментное расписание скорости обучения, ограничение общей нормы градиента на уровне и измерения после шагов . Ещё до первого прямого прохода зафиксированы следующие решения:
- пакеты обновлений берутся только из
Trainи следуют в заданном порядке; - каждому обновлению назначена одна конечная положительная скорость обучения;
- успешный шаг следует порядку
forward>backward>finite-check>clip>adamw-step>zero-grad; - обе функции потерь усредняются с весами по числу токенов и измеряются без записи графа;
- состояние может выбирать только
Validation, аTestна этой границе отклоняется.
Расписание имеет вид
Выполняются все восемь шагов: валидация не останавливает запуск досрочно. В этом примере пять значений функции потерь на валидации равны
поэтому выбран снимок . Здесь он случайно совпал с последним запланированным состоянием, но правило выбора не отдаёт последнему состоянию никакого предпочтения.
Обновляйте по обучающей выборке, выбирайте по валидационной
Для обновления градиент вычисляется в состоянии до этого обновления, а параметры и моменты переходят в следующее состояние вместе:
У двух функций потерь разные задачи. Градиент берётся из и изменяет параметры. Значение измеряется только в заданных контрольных точках и может заменить сохранённое состояние, но по нему не выполняют дифференцирование и оно не обновляет декодер.
Рассматривайте как один мысленный вектор, составленный из всех координат всех именованных параметров . Здесь — множество всех именованных обучаемых параметров, а — индекс скалярной координаты внутри параметра . Создавать объединённый тензор для этого не нужно. Одна общая норма определяется по формуле
Для верхнего предела цикл вычисляет по этой полной норме один множитель и применяет его ко всем координатам:
Поскольку , знаменатель не обращается в ноль. При нулевой или уже достаточно малой норме , а большая норма уменьшается до . На каждой заданной границе между участками расписания AdamW сохраняет накопленные моменты и счётчик шагов, но начинает использовать новое значение ; смена скорости не перезапускает оптимизатор.
Например, при и получаем . Цикл обучения передаёт AdamW этот единый множитель вместе с . Для каждого параметра и координаты в первый момент поступает , а во второй — квадрат . Таким образом, общая норма градиента после ограничения равна , а масштабирование выполняется до возведения в квадрат. Тензоры исходного градиента остаются без изменений в существующих листовых узлах параметров, пока цикл обучения не обнулит их после обновления. Поправка затухания AdamW не умножается на .
Потери на валидации взвешиваются по числу предсказываемых токенов, а не по числу пакетов. Если валидационный пакет содержит целевых токенов и имеет среднее значение , то
Выбор выполняется только по измеренному множеству . При точном равенстве строгое сравнение сохраняет более раннее состояние:
Не смешивайте шаги, градиенты и роли выборок
- — начальное состояние декодера, а — состояние после обновления .
- — нумеруемый с единицы индекс обновления; среди контрольных точек есть и .
- — функция потерь следующего токена для обучающего мини-пакета .
- мысленно объединяет все конечные координаты градиента именованных параметров до ограничения нормы.
- — единый множитель ограничения общей нормы, передаваемый в AdamW; если ограничение не требуется, он равен .
- — градиент после ограничения общей нормы, по которому AdamW обновляет оба момента.
- — положительный верхний предел общей нормы.
- — заранее заданная скорость обучения для обновления .
- и — состояния первого и второго моментов Adam, сохраняемые между шагами.
- — функция потерь на валидации без записи графа, используемая только для выбора.
- — множество индексов измеренных контрольных точек.
- — самая ранняя измеренная точка с минимальными потерями на валидации.
В программе Train, Validation и Test — конкретные варианты Partition.
Их смысловые роли таковы: обучающая выборка подгоняет параметры,
валидационная выбирает состояние, а тестовая предоставляет данные для оценки
после выбора. Реализация цикла обучения отклоняет Test на протяжении одного запуска
обучения; счётчик однократного доступа в главе 34 относится к одному локальному
экземпляру оценщика. Меньшие потери на обучающей выборке не заменяют проверку на
отложенной валидационной выборке.
Идентичность узлов связывает реестр параметров с вычислениями. Запись в реестре
и соответствующий компонент — эмбеддинг, блок или нормализация — ссылаются на
один и тот же узел TensorValue. AdamW записывает новое значение тензора в уже
существующий узел, поэтому все компоненты видят обновление без повторной сборки.
Таблица, которую совместно используют эмбеддинг и выходная проекция, также
остаётся одним узлом. Если бы реестр получил другой узел, ссылки внутри
компонентов остались бы у прежнего узла и это свойство нарушилось бы.
От отчёта по обучению к контрольной точке LLM, выбранной по валидации
Следующий этап — перейти от подгонки модели и отчёта по одному обученному состоянию к заранее заданным обновлениям на мини-пакетах. Такие обновления периодически создают состояния-кандидаты для валидации, а в пределах запуска реализация цикла обучения отклоняет отдельную тестовую выборку. Эти приёмы — часть пути к современным LLM.
A Neural Probabilistic Language Model показывает ранний пример такого разделения данных в нейронной языковой модели. Бенжио и соавторы разделяют обучающий, валидационный и тестовый текст, явно связывают валидацию с выбором модели и ранней остановкой и описывают стохастические обновления по одному примеру для нейронной языковой модели прямого распространения. Обновления по всему корпусу или по одному примеру и отчёт только по обучающей выборке сами по себе не задают масштабируемую периодичность обновлений и независимое правило выбора между состояниями языковой модели.
Sequence to Sequence Learning with Neural Networks добавляет практические средства управления обучением последовательностей. Суцкевер, Виньялс и Ле сообщают о пакетах последовательностей, заранее заданном правиле уменьшения скорости обучения и масштабировании градиента, когда его общая норма превышает фиксированный порог в рекуррентной модели последовательностей.
Attention Is All You Need переносит эти идеи в эпоху Transformer. Васвани и соавторы обучают Transformer на пакетах с заданным числом токенов, используют зависящее от номера шага расписание с разогревом и убыванием обратно пропорционально квадратному корню и периодически записывают контрольные точки. Использованное ими усреднение контрольных точек не совпадает с выбором валидационного минимума в этой главе.
Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer прямо описывает выбор контрольной точки по результатам валидации. Раффель и соавторы сохраняют контрольные точки дообучения с фиксированной периодичностью, выбирают лучшую по результату на валидации и прямо избегают использования тестовой выборки для выбора модели.
Language Models are Few-Shot Learners показывает, как эти приёмы применяются при крупномасштабном обучении модели только с декодером. Браун и соавторы переносят Adam, расписание скорости обучения, масштабирование размера пакета в зависимости от числа токенов и ограничение общей нормы градиента в обучение языковой модели только с декодером масштаба GPT-3.
В работах по нейронным языковым моделям роли обучающей, валидационной и тестовой выборок были разделены; системы для последовательностей и Transformer добавили мини-пакеты, явные расписания, ограничение нормы и периодически сохраняемые состояния; в более поздних работах с преобразованием текста в текст прямо указано, что контрольную точку выбирают по валидации, не используя тестовые данные для выбора модели. При обучении LLM только с декодером многократно формируют пакеты токенов, дифференцируют обучающую цель, контролируют величину градиента, применяют расписание скорости обучения и оценивают состояния-кандидаты на отложенной валидационной выборке, оставляя тестовые данные для оценки после выбора; счётчик однократного доступа в этом курсе относится к одному локальному экземпляру оценщика. Этот счётчик не описывает всю историю репозитория.
Путь к современному обучению LLM объединяет строгое разделение ролей выборок, воспроизводимые пакеты, конечные градиенты, контроль нормы, явное расписание скорости обучения, периодическую валидацию без записи графа и выбор контрольной точки. В этих работах используются разные архитектуры и схемы обучения, поэтому фиксированное начальное значение генератора, точно заданная периодичность и выбор самой ранней точки при равенстве — локальные учебные решения, а не общепринятая практика. Небольшой пример в коде показывает, зачем нужен отдельный сигнал выбора: ряд обучающих потерь выбирает последнюю точку, тогда как ряд потерь на отложенной валидационной выборке может выбрать более раннюю.
rust/demos/ch33-training-selection/src/lib.rs#historical-selection-contrast #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HistoricalSelection {
pub training_only_step: usize,
pub validation_step: usize,
}
fn earliest_minimum(values: &[f64]) -> usize {
values
.iter()
.enumerate()
.min_by(|left, right| left.1.total_cmp(right.1))
.map_or(0, |(index, _)| index)
}
/// Contrasts a falling training trace with an earlier validation minimum.
pub fn historical_selection() -> HistoricalSelection {
HistoricalSelection {
training_only_step: earliest_minimum(&[2.0, 1.5, 1.2]),
validation_step: earliest_minimum(&[2.0, 1.3, 1.4]),
}
} Явно задайте полный план обучения в Rust
LearningRateSchedule хранит ровно одну конечную положительную скорость для
каждого обновления. TrainerConfig требует измерения при , строго
возрастающих номеров контрольных точек, последней точки после заключительного
обновления и положительного конечного предела общей нормы. Порядок операций
представлен данными, а не скрыт в логике цикла.
rust/crates/llm-from-scratch/src/training/trainer.rs#training-plan /// The exact learner-visible order of one successful parameter update.
pub const UPDATE_EVENT_ORDER: [&str; 6] = [
"forward",
"backward",
"finite-check",
"clip",
"adamw-step",
"zero-grad",
];
/// One finite positive learning rate for every planned update.
#[derive(Clone, Debug, PartialEq)]
pub struct LearningRateSchedule {
rates: Vec<f64>,
}
impl LearningRateSchedule {
pub fn new(rates: Vec<f64>) -> Result<Self, TrainerError> {
if rates.is_empty() {
return Err(TrainerError::EmptyLearningRateSchedule);
}
for (index, &value) in rates.iter().enumerate() {
if !value.is_finite() || value <= 0.0 {
return Err(TrainerError::InvalidScheduledLearningRate {
step: index + 1,
value,
});
}
}
Ok(Self { rates })
}
pub fn steps(&self) -> usize {
self.rates.len()
}
pub fn learning_rate(&self, step: usize) -> Option<f64> {
step.checked_sub(1)
.and_then(|index| self.rates.get(index))
.copied()
}
pub fn rates(&self) -> &[f64] {
&self.rates
}
}
/// Fixed update, validation, and clipping policy for one complete run.
#[derive(Clone, Debug, PartialEq)]
pub struct TrainerConfig {
schedule: LearningRateSchedule,
validation_steps: Vec<usize>,
max_gradient_norm: f64,
}
impl TrainerConfig {
pub fn new(
schedule: LearningRateSchedule,
validation_steps: Vec<usize>,
max_gradient_norm: f64,
) -> Result<Self, TrainerError> {
if !max_gradient_norm.is_finite() || max_gradient_norm <= 0.0 {
return Err(TrainerError::InvalidMaximumGradientNorm {
value: max_gradient_norm,
});
}
if validation_steps.is_empty() {
return Err(TrainerError::EmptyValidationSteps);
}
if validation_steps[0] != 0 {
return Err(TrainerError::ValidationMustStartAtZero {
actual: validation_steps[0],
});
}
let final_step = schedule.steps();
for (index, &step) in validation_steps.iter().enumerate() {
if step > final_step {
return Err(TrainerError::ValidationStepOutOfRange { step, final_step });
}
if index > 0 && step <= validation_steps[index - 1] {
return Err(TrainerError::ValidationStepsNotIncreasing {
previous: validation_steps[index - 1],
next: step,
});
}
}
let actual = *validation_steps
.last()
.expect("a nonempty validation schedule has a last step");
if actual != final_step {
return Err(TrainerError::ValidationMustEndAtFinalStep {
expected: final_step,
actual,
});
}
Ok(Self {
schedule,
validation_steps,
max_gradient_norm,
})
}
pub const fn schedule(&self) -> &LearningRateSchedule {
&self.schedule
}
pub fn validation_steps(&self) -> &[usize] {
&self.validation_steps
}
pub const fn max_gradient_norm(&self) -> f64 {
self.max_gradient_norm
}
} Чтобы оценка не записывала граф, запись на ленту автодифференцирования нужно
отключить до прямого прохода. Вызов value или detach после прямого прохода
был бы запоздалым: промежуточный граф уже успел бы появиться. Область no_grad,
локальная для потока выполнения, не добавляет родительские рёбра при создании
операций, поддерживает вложенность и восстанавливает запись при выходе, в том
числе из-за паники.
rust/crates/llm-from-scratch/src/autograd/tensor_core.rs#no-grad-scope thread_local! {
static NO_GRAD_DEPTH: Cell<usize> = const { Cell::new(0) };
}
struct NoGradGuard;
impl Drop for NoGradGuard {
fn drop(&mut self) {
NO_GRAD_DEPTH.with(|depth| {
depth.set(
depth
.get()
.checked_sub(1)
.expect("a no-grad guard must balance one entered scope"),
);
});
}
}
fn no_grad_active() -> bool {
NO_GRAD_DEPTH.with(|depth| depth.get() != 0)
}
/// Runs `operation` without recording reverse-mode parent edges.
///
/// The scope is thread-local, nestable, and restored even if `operation`
/// unwinds. Forward arithmetic and finite-value checks are unchanged, but every
/// result created inside the scope is untracked and cannot mutate parameter
/// gradients through `backward`.
pub fn no_grad<T>(operation: impl FnOnce() -> T) -> T {
NO_GRAD_DEPTH.with(|depth| {
depth.set(
depth
.get()
.checked_add(1)
.expect("no-grad nesting depth must fit usize"),
);
});
let _guard = NoGradGuard;
operation()
} evaluate_no_grad вычисляет среднее за эпоху с весами по числу токенов и
проверяет, что ни один результат не отслеживает градиент. До оценки сохраняются
биты градиентов; изменение хотя бы одного бита отклоняет вызов.
rust/crates/llm-from-scratch/src/training/trainer.rs#no-grad-evaluation /// Evaluates one epoch without recording parent edges or mutating gradients.
pub fn evaluate_no_grad(
model: &DecoderModel,
epoch: &MiniBatchEpoch,
) -> Result<Evaluation, TrainerError> {
if epoch.window_count() == 0 {
return Err(TrainerError::EmptyEpoch {
role: if epoch.partition() == Partition::Validation {
TrainerEpochRole::ValidationSelection
} else {
TrainerEpochRole::TrainEvaluation
},
});
}
let before = gradient_bits(model)?;
let mut weighted_sum = 0.0;
let mut token_count = 0_usize;
let mut recorded_graphs = 0_usize;
for (batch_index, batch) in epoch.batches().iter().enumerate() {
let loss = no_grad(|| {
model.loss(
batch.inputs(),
&[batch.batch_width(), batch.context_length()],
batch.targets(),
)
})?;
if loss.tracks_gradient() {
return Err(TrainerError::ValidationRecordedGraph {
partition: epoch.partition(),
batch: batch_index,
});
}
recorded_graphs += usize::from(loss.tracks_gradient());
let scalar = scalar_loss(&loss)?;
weighted_sum += scalar * batch.token_count() as f64;
token_count += batch.token_count();
}
let mean_loss = weighted_sum / token_count as f64;
if !mean_loss.is_finite() {
return Err(TrainerError::NonFiniteLoss { value: mean_loss });
}
if gradient_bits(model)? != before {
return Err(TrainerError::ValidationChangedGradient);
}
Ok(Evaluation {
mean_loss,
token_count,
batch_count: epoch.batch_count(),
recorded_graphs,
})
} Перед первым обновлением train_decoder должен создать рабочий декодер,
независимый от модели, которую вызывающий код передал по ссылке. Поэтому
DecoderModelState::snapshot один раз копирует тензор каждого именованного
параметра в состояние, не связанное с графом вычислений. Затем это состояние
сразу передаётся в into_model: метод получает его во владение и переносит
имена и буферы тензоров в рабочий декодер без повторного копирования. Переданный
оптимизатор также копируется один раз. После этого все восемь обновлений
выполняются одним и тем же рабочим декодером и одним и тем же оптимизатором; для
каждого мини-пакета их не создают заново.
Снимки, создаваемые во время обучения, нужны по другой причине. Если очередная
контрольная точка даёт новый минимум потерь на валидации, её значения параметров
должны сохраниться, хотя последующие обновления продолжают менять рабочий
декодер. Поэтому в этот момент создаётся полная независимая копия состояния. По
завершении TrainingResult хранит выбранное состояние, не связанное с графом
вычислений: оно точно фиксирует результат выбора по валидации. Рядом хранится
отдельный декодер с теми же значениями, готовый к последующей оценке. Оба объекта
должны оставаться доступными, поэтому restore_independent_model один раз
дополнительно копирует буферы параметров.
rust/crates/llm-from-scratch/src/training/trainer.rs#decoder-state-snapshot #[derive(Clone, Debug, PartialEq)]
struct StateParameter {
name: String,
value: Tensor,
}
/// Graph-free owned values for one complete decoder state.
#[derive(Debug, PartialEq)]
pub struct DecoderModelState {
config: DecoderModelConfig,
parameters: Vec<StateParameter>,
}
impl DecoderModelState {
/// Copies a live decoder into graph-free state that can outlive later updates.
pub fn snapshot(model: &DecoderModel) -> Self {
Self {
config: model.config(),
parameters: model
.parameters()
.iter()
.map(|parameter| StateParameter {
name: parameter.name().to_owned(),
value: parameter.tensor().value_snapshot(),
})
.collect(),
}
}
/// Copies this state when two independent owners must retain the same values.
pub fn independent_snapshot(&self) -> Self {
Self {
config: self.config,
parameters: self.parameters.clone(),
}
}
pub const fn config(&self) -> DecoderModelConfig {
self.config
}
pub fn parameter_names(&self) -> impl ExactSizeIterator<Item = &str> {
self.parameters
.iter()
.map(|parameter| parameter.name.as_str())
}
pub fn scalar_count(&self) -> usize {
self.parameters
.iter()
.map(|parameter| parameter.value.len())
.sum()
}
pub fn bit_pattern(&self) -> Vec<u64> {
self.parameters
.iter()
.flat_map(|parameter| {
parameter
.value
.as_slice()
.iter()
.map(|value| value.to_bits())
})
.collect()
}
/// Rebuilds an independent decoder while retaining this state snapshot.
pub fn restore_independent_model(&self) -> Result<DecoderModel, TrainerError> {
self.independent_snapshot().into_model()
}
/// Consumes graph-free state and moves every tensor buffer into one decoder.
pub fn into_model(self) -> Result<DecoderModel, TrainerError> {
let Self { config, parameters } = self;
let parameters = parameters
.into_iter()
.map(|parameter| NamedParameter::from_tensor(parameter.name, parameter.value))
.collect::<Result<Vec<_>, _>>()?;
DecoderModel::from_parameters(config, parameters).map_err(Into::into)
}
}
/// One validation-selected model and its AdamW state captured at the same step.
///
/// Only the trainer can construct this bundle. Callers may inspect it, but they
/// cannot attach a freely supplied step label or optimizer from another point in
/// the run before passing it to a checkpoint.
///
/// ```compile_fail
/// use llm_from_scratch::training::trainer::SelectedTrainingState;
///
/// let _counterfeit = SelectedTrainingState {
/// step: 8,
/// model_state: todo!(),
/// optimizer_state: todo!(),
/// };
/// ```
#[derive(Debug, PartialEq)]
pub struct SelectedTrainingState {
step: usize,
model_state: DecoderModelState,
optimizer_state: AdamWState,
}
impl SelectedTrainingState {
pub const fn step(&self) -> usize {
self.step
}
pub const fn model_state(&self) -> &DecoderModelState {
&self.model_state
}
pub const fn optimizer_state(&self) -> &AdamWState {
&self.optimizer_state
}
} Проверив конечность всех исходных градиентов и вычислив одну норму по всем
именованным координатам, цикл обучения получает один множитель . Он
передаёт существующие именованные параметры рабочего декодера, и
методу step_with_learning_rate_and_gradient_scale того же рабочего
оптимизатора. Метод возвращает только номер выполненного шага; подробная
трассировка из главы 22 внутри этого цикла не нужна.
Сначала AdamW полностью проверяет предстоящее обновление. При обновлении первого момента AdamW использует , а при обновлении второго — , поэтому масштабирование выполняется до возведения в квадрат. Отдельная поправка затухания вычисляется из прежнего значения параметра и не умножается на . AdamW подготавливает новые тензоры всех параметров, оба состояния моментов и следующее значение счётчика шагов и лишь затем запрашивает исключительный доступ на запись к значениям всех параметров. Ошибка при подготовке или получении доступа не меняет ни один параметр и ни одно поле оптимизатора. При этом AdamW хранит полностью проверенные новые значения тензоров до момента атомарной записи. Это необходимое временное состояние транзакции оптимизатора, а не вектор параметров для замены, созданный циклом обучения.
После успешных проверок AdamW записывает каждый подготовленный тензор в уже
существующий узел TensorValue и фиксирует подготовленное состояние
оптимизатора. Имена, порядок и идентичность узлов не меняются; таблица, общая
для эмбеддинга и выходной проекции, также остаётся одним узлом. Реестр и
компоненты декодера хранят ссылки на те же узлы, поэтому следующий прямой проход
видит новые значения без повторной сборки декодера.
AdamW намеренно оставляет исходный градиент без изменений в том же узле
параметра. Цикл обучения сравнивает возвращённый номер шага с номером обновления
в плане, вызывает zero_grad() для каждого рабочего параметра и проверяет, что
все координаты градиента равны нулю, прежде чем начинать следующий прямой
проход. Именно это явное действие цикла предотвращает накопление градиентов
между мини-пакетами. Цикл не создаёт на каждом шаге новый декодер, новый
оптимизатор или предназначенный для замены Vec<NamedParameter>.
rust/crates/llm-from-scratch/src/training/adamw.rs#adamw-execution-and-trace-api /// Reads the accumulated gradients and atomically updates every live leaf.
///
/// All arithmetic, tensor construction, and optimizer-state changes are
/// prepared first. An error leaves both the supplied parameters and this
/// optimizer bit-identical. A successful commit preserves every parameter
/// node and leaves its accumulated gradient for the caller to clear.
/// The result is only the committed step number; use `step_with_trace` when
/// the elementwise update vectors are needed for inspection.
pub fn step(&mut self, parameters: &[NamedParameter]) -> Result<u64, AdamWError> {
self.step_with_config(parameters, self.config, 1.0, NoAdamWTrace)
}
/// Applies the same transaction while recording every elementwise update.
pub fn step_with_trace(
&mut self,
parameters: &[NamedParameter],
) -> Result<AdamWStep, AdamWError> {
let observer = RecordAdamWTrace::with_capacity(parameters.len());
self.step_with_config(parameters, self.config, 1.0, observer)
}
/// Applies one validated scheduled learning rate without resetting moments.
///
/// The override belongs only to this update; `config()` keeps the optimizer's
/// base rate. An invalid rate or any later preparation error leaves the
/// parameters, moments, powers, and step counter unchanged.
pub fn step_with_learning_rate(
&mut self,
parameters: &[NamedParameter],
learning_rate: f64,
) -> Result<u64, AdamWError> {
let step_config = self.config.with_learning_rate(learning_rate)?;
self.step_with_config(parameters, step_config, 1.0, NoAdamWTrace)
}
/// Applies one scheduled rate and one validated global gradient scale.
///
/// The scale multiplies only the gradient used by Adam's moments. The
/// decoupled weight-decay branch continues to use the unscaled parameter.
pub fn step_with_learning_rate_and_gradient_scale(
&mut self,
parameters: &[NamedParameter],
learning_rate: f64,
gradient_scale: f64,
) -> Result<u64, AdamWError> {
let step_config = self.config.with_learning_rate(learning_rate)?;
self.step_with_config(parameters, step_config, gradient_scale, NoAdamWTrace)
}
/// Applies a scheduled learning rate and records the complete update trace.
pub fn step_with_learning_rate_and_trace(
&mut self,
parameters: &[NamedParameter],
learning_rate: f64,
) -> Result<AdamWStep, AdamWError> {
let step_config = self.config.with_learning_rate(learning_rate)?;
let observer = RecordAdamWTrace::with_capacity(parameters.len());
self.step_with_config(parameters, step_config, 1.0, observer)
} DecoderModel::from_parameters остаётся точкой сборки декодера, но не участвует
в обычном шаге AdamW. Сначала into_model переносит каждый буфер из состояния,
которое получил во владение, в новый листовой узел параметра. Затем
from_parameters проверяет получившийся список листовых узлов, не забирая их во
владение, по общим правилам схемы декодера: конфигурация должна быть допустимой,
тензоров должно быть ровно , в каждой позиции списка должно стоять
требуемое имя, а форма каждого тензора должна соответствовать его компоненту.
При этой проверке буферы тензоров не копируются, а компоненты ещё не связываются
с параметрами. Успешная проверка означает только, что список имеет допустимую
схему.
После проверки from_parameters передаёт эмбеддингу, блокам и итоговому
RMSNorm ссылки на эти листовые узлы. Только эта привязка снова делает один узел
эмбеддинга общим для выбора строк и выходной проекции. Копирование ссылок не
копирует буферы тензоров. Такая сборка выполняется при преобразовании состояния
в модель; после обычного шага AdamW пересобирать декодер не нужно.
rust/crates/llm-from-scratch/src/models/decoder.rs#decoder-parameter-rebuild /// Rebuilds every component handle from one exact stable-order parameter set.
///
/// State restoration uses this construction boundary to create an isolated
/// decoder. Ordinary optimizer steps instead update the existing leaves, so
/// the registry, components, and tied embedding keep their live aliases.
pub fn from_parameters(
config: DecoderModelConfig,
parameters: Vec<NamedParameter>,
) -> Result<Self, DecoderModelError> {
validate_parameter_layout(config, parameters.as_slice())?;
let expected = parameters.len();
let embedding = Embedding::from_parameter(parameters[0].clone())
.map_err(DecoderModelError::Embedding)?;
let mut blocks = Vec::new();
blocks.try_reserve_exact(config.layers).map_err(|_| {
DecoderModelError::LayerAllocationFailed {
layers: config.layers,
}
})?;
for layer in 0..config.layers {
let start = 1 + layer * BLOCK_PARAMETER_SUFFIXES.len();
let attention_norm = RmsNorm::from_gain(parameters[start].clone(), config.rms_epsilon)
.map_err(|source| DecoderModelError::Block {
layer,
source: DecoderBlockError::AttentionNorm(source),
})?;
let attention = MultiHeadAttention::from_parameters(
parameters[start + 1].clone(),
parameters[start + 2].clone(),
parameters[start + 3].clone(),
parameters[start + 4].clone(),
config.heads,
config.max_positions,
config.rope_base,
)
.map_err(|source| DecoderModelError::Block {
layer,
source: DecoderBlockError::Attention(source),
})?;
let feed_forward_norm =
RmsNorm::from_gain(parameters[start + 5].clone(), config.rms_epsilon).map_err(
|source| DecoderModelError::Block {
layer,
source: DecoderBlockError::FeedForwardNorm(source),
},
)?;
let feed_forward = SwiGlu::from_parameters(
parameters[start + 6].clone(),
parameters[start + 7].clone(),
parameters[start + 8].clone(),
)
.map_err(|source| DecoderModelError::Block {
layer,
source: DecoderBlockError::FeedForward(source),
})?;
blocks.push(
DecoderBlock::from_parts(
attention_norm,
attention,
feed_forward_norm,
feed_forward,
)
.map_err(|source| DecoderModelError::Block { layer, source })?,
);
}
let final_norm = RmsNorm::from_gain(parameters[expected - 1].clone(), config.rms_epsilon)
.map_err(DecoderModelError::FinalNorm)?;
Self::from_parts(config, embedding, blocks, final_norm)
} train_decoder сначала отклоняет выборки с неверными ролями и несовместимые
эпохи. Затем он создаёт один независимый рабочий декодер, выполняет все
запланированные обновления, измеряет потери только в заданных контрольных точках
и сохраняет новый выбранный снимок лишь при строгом уменьшении потерь на
валидации. После последнего обновления функция возвращает сохранённый выбранный
снимок и отдельный декодер с собственной копией тех же параметров. Модель и
оптимизатор, переданные вызывающим кодом, остаются неизменными даже при ошибке.
rust/crates/llm-from-scratch/src/training/trainer.rs#complete-training-loop /// Executes every planned update, then restores the earliest validation minimum.
///
/// The supplied model and optimizer remain unchanged. Update batches may cycle
/// deterministically, but validation never stops the run or consults test data.
pub fn train_decoder(
initial_model: &DecoderModel,
initial_optimizer: &AdamW,
update_epoch: &MiniBatchEpoch,
train_evaluation: &MiniBatchEpoch,
validation: &MiniBatchEpoch,
config: &TrainerConfig,
) -> Result<TrainingResult, TrainerError> {
if initial_optimizer.step_count() != 0 {
return Err(TrainerError::OptimizerNotFresh {
step: initial_optimizer.step_count(),
});
}
let context_length = update_epoch.context_length();
let model_config = initial_model.config();
validate_epoch(
TrainerEpochRole::Update,
Partition::Train,
context_length,
model_config,
update_epoch,
)?;
validate_epoch(
TrainerEpochRole::TrainEvaluation,
Partition::Train,
context_length,
model_config,
train_evaluation,
)?;
validate_epoch(
TrainerEpochRole::ValidationSelection,
Partition::Validation,
context_length,
model_config,
validation,
)?;
let model = DecoderModelState::snapshot(initial_model).into_model()?;
let mut optimizer = initial_optimizer.clone();
let mut checkpoints = vec![checkpoint(0, &model, train_evaluation, validation)?];
let mut selected_step = 0_usize;
let mut selected_validation_loss = checkpoints[0].validation.mean_loss();
let mut selected_state = DecoderModelState::snapshot(&model);
let mut selected_optimizer_state = optimizer.persistence_state();
let mut steps = Vec::with_capacity(config.schedule.steps());
for step in 1..=config.schedule.steps() {
let batch_index = (step - 1) % update_epoch.batch_count();
let batch = &update_epoch.batches()[batch_index];
let loss = model.loss(
batch.inputs(),
&[batch.batch_width(), batch.context_length()],
batch.targets(),
)?;
let train_loss = scalar_loss(&loss)?;
loss.backward_with_seed(
&Tensor::from_vec(Vec::new(), vec![1.0])?.view(),
GraphRetention::Release,
)?;
drop(loss);
let norm = gradient_norm(&model, config.max_gradient_norm)?;
let learning_rate = config
.schedule
.learning_rate(step)
.expect("the loop stays inside the validated schedule");
let optimizer_step = optimizer.step_with_learning_rate_and_gradient_scale(
model.parameters(),
learning_rate,
norm.scale,
)?;
let expected_optimizer_step = u64::try_from(step).unwrap_or(u64::MAX);
if optimizer_step != expected_optimizer_step {
return Err(TrainerError::OptimizerStepMismatch {
expected: expected_optimizer_step,
actual: optimizer_step,
});
}
clear_and_verify_gradients(&model)?;
let batch_windows = batch
.provenance()
.iter()
.map(|window| format!("{}@{}", window.document_id(), window.start()))
.collect();
steps.push(TrainingStep {
step,
batch_windows,
learning_rate,
train_loss,
gradient_norm_before: norm.before,
gradient_norm_after: norm.after,
gradient_scale: norm.scale,
clipped: norm.scale < 1.0,
finite_gradients: true,
parameter_nodes_preserved: true,
cleared_gradients: true,
events: UPDATE_EVENT_ORDER,
});
if config.validation_steps.binary_search(&step).is_ok() {
let measured = checkpoint(step, &model, train_evaluation, validation)?;
if measured.validation.mean_loss() < selected_validation_loss {
selected_step = step;
selected_validation_loss = measured.validation.mean_loss();
selected_state = DecoderModelState::snapshot(&model);
selected_optimizer_state = optimizer.persistence_state();
}
checkpoints.push(measured);
}
}
for measured in &mut checkpoints {
measured.selected = measured.step == selected_step;
}
let final_state = DecoderModelState::snapshot(&model);
let selected_model = selected_state.restore_independent_model()?;
Ok(TrainingResult {
steps,
checkpoints,
selected_validation_loss,
selected_training_state: SelectedTrainingState {
step: selected_step,
model_state: selected_state,
optimizer_state: selected_optimizer_state,
},
final_state,
selected_model,
final_optimizer: optimizer,
})
} Детерминированный пример дважды выполняет весь план, проверяет побитовую воспроизводимость, подтверждает срабатывание ограничения нормы и отсутствие графов при измерении, а также проверяет, что параметры выбранного декодера побитово совпадают с сохранённым состоянием.
Для точного сравнения состояний нужен собственный вектор битовых представлений,
а не отдельные копии тензоров параметров. parameter_bits временно читает каждый
тензор параметра, преобразует его скалярные значения в u64 и после завершения
чтения сохраняет только полученный вектор битовых представлений.
rust/demos/ch33-training-selection/src/lib.rs#learner-evidence #[derive(Debug)]
struct PreparedEpochs {
updates: MiniBatchEpoch,
train_evaluation: MiniBatchEpoch,
validation: MiniBatchEpoch,
test_probe: MiniBatchEpoch,
}
fn epoch(
partition: Partition,
documents: &[(&str, &[u32])],
order: BatchOrder,
batch_size: usize,
) -> Result<MiniBatchEpoch, FixtureError> {
let documents = documents
.iter()
.map(|(id, token_ids)| BatchDocument::new(id, partition, token_ids))
.collect::<Result<Vec<_>, _>>()?;
let windows = CausalWindowConfig::new(CONTEXT_LENGTH, 1)
.map_err(|_| FixtureError::Invariant("fixed window configuration changed"))?;
let batches = MiniBatchConfig::new(batch_size, order)?;
MiniBatchEpoch::build(partition, &documents, windows, batches).map_err(Into::into)
}
fn prepared_epochs() -> Result<PreparedEpochs, FixtureError> {
let train_documents = [
("train-a", TRAIN_A.as_slice()),
("train-b", TRAIN_B.as_slice()),
];
let validation_documents = [
("validation-a", VALIDATION_A.as_slice()),
("validation-b", VALIDATION_B.as_slice()),
];
let updates = epoch(
Partition::Train,
&train_documents,
BatchOrder::Shuffled { seed: SHUFFLE_SEED },
BATCH_SIZE,
)?;
let train_evaluation = epoch(
Partition::Train,
&train_documents,
BatchOrder::Sequential,
5,
)?;
let validation = epoch(
Partition::Validation,
&validation_documents,
BatchOrder::Sequential,
4,
)?;
let test_probe = epoch(
Partition::Test,
&validation_documents,
BatchOrder::Sequential,
4,
)?;
require(
updates.window_count() == 20,
"training window count changed",
)?;
require(updates.batch_count() == 10, "training batch count changed")?;
require(
train_evaluation.window_count() == 20,
"training evaluation window count changed",
)?;
require(
validation.window_count() == 14,
"validation window count changed",
)?;
Ok(PreparedEpochs {
updates,
train_evaluation,
validation,
test_probe,
})
}
pub fn fixture_model_config() -> DecoderModelConfig {
DecoderModelConfig::new(
VOCABULARY_SIZE,
MODEL_WIDTH,
HEADS,
FEED_FORWARD_WIDTH,
LAYERS,
CONTEXT_LENGTH,
10_000.0,
1e-6,
)
}
pub fn fixture_trainer_config() -> Result<TrainerConfig, FixtureError> {
TrainerConfig::new(
LearningRateSchedule::new(LEARNING_RATES.to_vec())?,
VALIDATION_STEPS.to_vec(),
MAX_GRADIENT_NORM,
)
.map_err(Into::into)
}
fn fixture_optimizer(model: &DecoderModel) -> Result<AdamW, FixtureError> {
let mut decay = Vec::new();
let mut no_decay = Vec::new();
for parameter in model.parameters() {
if parameter.name().ends_with(".gain") {
no_decay.push(parameter.name().to_owned());
} else {
decay.push(parameter.name().to_owned());
}
}
let groups = AdamWParameterGroups::new(decay, no_decay)?;
let config = AdamWConfig::new(LEARNING_RATES[0], 0.9, 0.999, 1e-8, 0.01)?;
Ok(AdamW::with_parameter_groups(config, groups))
}
#[derive(Debug)]
struct SingleRun {
result: TrainingResult,
input_model_unchanged: bool,
input_optimizer_unchanged: bool,
test_partition_rejected: bool,
}
fn parameter_bits(model: &DecoderModel) -> Vec<u64> {
let mut bits = Vec::new();
for parameter in model.parameters() {
bits.extend(
parameter
.tensor()
.value()
.as_slice()
.iter()
.map(|value| value.to_bits()),
);
}
bits
}
fn run_once() -> Result<SingleRun, FixtureError> {
let epochs = prepared_epochs()?;
let model = DecoderModel::new(
fixture_model_config(),
&mut SplitMix64::from_seed(INIT_SEED),
)?;
require(
model.parameters().len() == 11,
"parameter tensor count changed",
)?;
require(
model.parameter_count() == 144,
"parameter scalar count changed",
)?;
let initial_bits = parameter_bits(&model);
let optimizer = fixture_optimizer(&model)?;
let result = train_decoder(
&model,
&optimizer,
&epochs.updates,
&epochs.train_evaluation,
&epochs.validation,
&fixture_trainer_config()?,
)?;
let test_partition_rejected = matches!(
train_decoder(
&model,
&optimizer,
&epochs.test_probe,
&epochs.train_evaluation,
&epochs.validation,
&fixture_trainer_config()?,
),
Err(TrainerError::WrongPartition {
role: llm_from_scratch::training::trainer::TrainerEpochRole::Update,
expected: Partition::Train,
actual: Partition::Test,
})
);
let model_after = parameter_bits(&model);
Ok(SingleRun {
result,
input_model_unchanged: model_after == initial_bits,
input_optimizer_unchanged: optimizer.step_count() == 0
&& optimizer.parameter_names().next().is_none(),
test_partition_rejected,
})
}
fn replay_equal(left: &SingleRun, right: &SingleRun) -> bool {
left.result.steps() == right.result.steps()
&& left.result.checkpoints() == right.result.checkpoints()
&& left.result.selected_step() == right.result.selected_step()
&& left.result.selected_validation_loss().to_bits()
== right.result.selected_validation_loss().to_bits()
&& left.result.selected_state().bit_pattern() == right.result.selected_state().bit_pattern()
&& left.result.final_state().bit_pattern() == right.result.final_state().bit_pattern()
}
#[derive(Debug)]
pub struct LearnerEvidence {
pub result: TrainingResult,
pub replay_bitwise: bool,
pub input_model_unchanged: bool,
pub input_optimizer_unchanged: bool,
pub test_partition_rejected: bool,
/// Compatibility evidence for the final-evaluation boundary: rejection
/// before a forward pass implies that selection consumed zero test epochs.
pub test_reads: usize,
pub history: HistoricalSelection,
}
pub fn learner_evidence() -> Result<LearnerEvidence, FixtureError> {
let first = run_once()?;
let second = run_once()?;
require(
first.result.steps().len() == LEARNING_RATES.len(),
"step count changed",
)?;
require(
first.result.checkpoints().len() == VALIDATION_STEPS.len(),
"checkpoint count changed",
)?;
require(
first
.result
.steps()
.iter()
.all(|step| step.events() == &UPDATE_EVENT_ORDER),
"operation order changed",
)?;
require(
first.result.steps().iter().any(|step| step.clipped()),
"fixture no longer exercises clipping",
)?;
require(
first
.result
.checkpoints()
.last()
.unwrap()
.train()
.mean_loss()
< first.result.checkpoints()[0].train().mean_loss(),
"training loss did not improve",
)?;
require(
first.result.checkpoints().iter().all(|checkpoint| {
checkpoint.train().recorded_graphs() == 0
&& checkpoint.validation().recorded_graphs() == 0
}),
"evaluation recorded a graph",
)?;
require(
first.result.final_optimizer().step_count() == LEARNING_RATES.len() as u64,
"optimizer did not execute every scheduled step",
)?;
let selected = first
.result
.checkpoints()
.iter()
.filter(|checkpoint| checkpoint.selected())
.collect::<Vec<_>>();
require(selected.len() == 1, "selection marker count changed")?;
require(
first.test_partition_rejected,
"test partition was not rejected during preflight",
)?;
require(
selected[0].step() == first.result.selected_step(),
"selected checkpoint and restored state disagree",
)?;
require(
first.result.selected_state().bit_pattern()
== parameter_bits(first.result.selected_model()),
"restored selected model changed snapshot bits",
)?;
let replay_bitwise = replay_equal(&first, &second);
let test_partition_rejected = first.test_partition_rejected;
Ok(LearnerEvidence {
result: first.result,
replay_bitwise,
input_model_unchanged: first.input_model_unchanged,
input_optimizer_unchanged: first.input_optimizer_unchanged,
test_partition_rejected,
test_reads: usize::from(!test_partition_rejected),
history: historical_selection(),
})
} rust/demos/ch33-training-selection/src/main.rs fn main() {
print!(
"{}",
ch33_training_selection::learner_report().expect("Chapter 33 fixture must remain valid")
);
} Выполните cargo run --quiet --locked -p ch33-training-selection. Отчёт
перечисляет пять измеренных контрольных точек и выбранное по валидации состояние,
а также показывает результаты проверок ограничения нормы, сохранения
идентичности узлов, явного обнуления градиентов, отклонения попытки передать
тестовую выборку, владения и побитовой воспроизводимости.
Читайте только измеренные точки и не дорисовывайте кривую
Записанный запуск содержит фиксированную конфигурацию, одну и ту же последовательность из шести операций для каждого из восьми обновлений, четыре сегмента расписания, все восемь записей об обновлениях, ось по измеренным потерям, пять пар контрольных точек, одну запись выбора и проверенные границы. Вместе эти данные показывают связь оптимизации с выбором модели, не заполняя неизмеренные шаги.
rust/demos/ch33-training-selection/src/diagram_trace.rs#training-selection-trace fn axis(losses: impl Iterator<Item = f64>) -> (f64, f64, [f64; 3]) {
let values = losses.collect::<Vec<_>>();
let smallest = values.iter().copied().fold(f64::INFINITY, f64::min);
let largest = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
let minimum = (smallest * 10.0).floor() / 10.0;
let mut maximum = (largest * 10.0).ceil() / 10.0;
if maximum <= minimum {
maximum = minimum + 0.1;
}
let midpoint = (minimum + maximum) / 2.0;
(minimum, maximum, [minimum, midpoint, maximum])
}
pub fn diagram_trace() -> Result<String, FixtureError> {
let evidence = learner_evidence()?;
let result = &evidence.result;
let mut lines = vec![
"TRAINING_SELECTION_TRACE_V1".to_owned(),
format!(
"CONFIG|seed=33|updates={}|batch_size=2|context=2|validation_every=2|clip_norm={MAX_GRADIENT_NORM:.6}|runtime_limit_ms={RUNTIME_LIMIT_MS}",
LEARNING_RATES.len()
),
"ORDER|events=forward>backward>finite-check>clip>adamw-step>zero-grad".to_owned(),
format!(
"SCHEDULE|start=1|end=2|learning_rate={:.6}",
LEARNING_RATES[0]
),
format!(
"SCHEDULE|start=3|end=4|learning_rate={:.6}",
LEARNING_RATES[2]
),
format!(
"SCHEDULE|start=5|end=6|learning_rate={:.6}",
LEARNING_RATES[4]
),
format!(
"SCHEDULE|start=7|end=8|learning_rate={:.6}",
LEARNING_RATES[6]
),
];
for step in result.steps() {
lines.push(format!(
"UPDATE|step={}|batch={}|learning_rate={:.6}|train_loss={:.6}|grad_norm_before={:.6}|grad_norm_after={:.6}|clipped={}|finite={}|nodes_preserved={}|cleared={}",
step.step(),
step.batch_windows().join(","),
step.learning_rate(),
step.train_loss(),
step.gradient_norm_before(),
step.gradient_norm_after(),
step.clipped(),
step.finite_gradients(),
step.parameter_nodes_preserved(),
step.cleared_gradients()
));
}
let (minimum, maximum, ticks) = axis(result.checkpoints().iter().flat_map(|checkpoint| {
[
checkpoint.train().mean_loss(),
checkpoint.validation().mean_loss(),
]
}));
lines.push(format!(
"AXIS|min={minimum:.6}|max={maximum:.6}|ticks=[{:.6},{:.6},{:.6}]",
ticks[0], ticks[1], ticks[2]
));
for checkpoint in result.checkpoints() {
lines.push(format!(
"CHECKPOINT|step={}|train_loss={:.6}|validation_loss={:.6}|selected={}|train_graphs={}|validation_graphs={}",
checkpoint.step(),
checkpoint.train().mean_loss(),
checkpoint.validation().mean_loss(),
checkpoint.selected(),
checkpoint.train().recorded_graphs(),
checkpoint.validation().recorded_graphs()
));
}
lines.extend([
format!(
"SELECT|step={}|validation_loss={:.6}|criterion=validation-only|snapshot=true|test_partition_rejected={}",
result.selected_step(),
result.selected_validation_loss(),
evidence.test_partition_rejected
),
format!(
"PROOF|fixed_seed_batches=true|schedule_exact=true|finite_gradients=true|parameter_nodes_preserved=true|cleared_gradients=true|clipping_observed={}|train_loss_decreased=true|validation_no_grad=true|selection_matches_argmin=true|test_partition_rejected={}|replay_bitwise={}|input_unchanged={}",
result.steps().iter().any(|step| step.clipped()),
evidence.test_partition_rejected,
evidence.replay_bitwise,
evidence.input_model_unchanged && evidence.input_optimizer_unchanged
),
"END_TRAINING_SELECTION_TRACE".to_owned(),
]);
Ok(lines.join("\n") + "\n")
} Разделите обновление параметров и выбор модели по валидации
Повторите одну и ту же последовательность из шести операций для каждого из восьми обновлений, затем сравните десять отдельных измерений на обучающей и валидационной выборках в пяти контрольных точках, не проводя между ними выдуманную кривую.
- Круг: измерение на обучающей выборке
- Ромб: измерение на валидационной выборке
- Двойное подчёркивание: состояние, выбранное по валидации
- В промежутках нет измеренных значений или интерполированной линии
Один успешный шаг состоит из шести операций
Цикл освобождает граф обратного прохода, проверяет все градиенты, один раз ограничивает общую норму декодера, записывает обновление с заданной скоростью в существующие узлы параметров и явно обнуляет их исходные градиенты.
-
Прямой проход на обучении
-
Обратный проход и освобождение графа
-
Проверка конечности градиентов
-
Ограничение общей нормы
-
Шаг AdamW с заданной скоростью
-
Явное обнуление исходных градиентов
Измеряйте состояния только в заданных контрольных точках
Таблица и отметки сохраняют точные значения с шестью знаками после запятой. Измерены только шаги 0, 2, 4, 6 и 8; расстояние между ними на схеме не добавляет новых вычислений.
Измеряйте состояния только в заданных контрольных точках
- 0
- 2
- 4
- 6
- 8
| Шаг | Потери на обучении | Потери на валидации | Выбранное состояние |
|---|---|---|---|
| Не выбрано | |||
| Не выбрано | |||
| Не выбрано | |||
| Не выбрано | |||
| Выбрано |
Разделяйте обучение, выбор модели и тестовую оценку
Валидация может выбрать сохранённое состояние, но не обновить его; реализация цикла обучения отклоняет Test на протяжении запуска, прежде чем в следующей главе будет создан локальный оценщик.
Первая точка с минимумом на валидации
criterion=validation-only, test_partition_rejected=true Обучающие и валидационные измерения не записывают граф
Подтверждено данными запуска
train_graphs=0, validation_graphs=0 Информационная граница между выборками
Train → Прямой проход на обучении
Validation → Первая точка с минимумом на валидации
Test → Отклонено до любого обновления
Ограничение нормы, расписание и воспроизводимость
Норма ограничена до шага оптимизатора
Исходные узлы параметров сохранены parameter_nodes_preserved=true
Исходные градиенты явно обнулены cleared_gradients=true
schedule_exact=true, replay_bitwise=true На схеме десять отдельных отметок: одно измерение на обучающей и одно на валидационной выборке в каждой из пяти контрольных точек. Между ними нет отрезков. Линия утверждала бы существование значений на неизмеренных шагах, поэтому точная таблица остаётся основой сравнения.
При потери равны и , а при — и
. Двойное подчёркивание валидационной отметки показывает ,
поскольку — наименьшее измеренное значение. Запись выбора также
содержит criterion=validation-only, snapshot=true и
test_partition_rejected=true.
Во всех обновлениях именно этого примера общая норма градиента до ограничения превышает , поэтому AdamW каждый раз получает норму . Это свойство конкретного запуска, а не требование к каждому обновлению реальной LLM. Общий цикл принимает и конечные градиенты, норма которых уже ниже предела.
В каждой записи об обновлении также указано nodes_preserved=true и
cleared=true. В итоговой проверке те же свойства всего запуска записаны как
parameter_nodes_preserved=true и cleared_gradients=true. Первое поле
означает, что AdamW обновил значения внутри исходных узлов параметров. Второе
подтверждает, что затем цикл обучения явно обнулил находившиеся в них исходные
градиенты.
Проверьте информационную границу и владение состоянием
- Замените роль эпохи обновлений
TrainнаTest. Предскажите ошибку предварительной проверки и объясните, почему исходные декодер и оптимизатор не меняются. - Замените контрольные точки на . Объясните, зачем после всех запланированных обновлений нужна заключительная точка.
- Возьмите валидационные потери . Определите, какую точку сохранит строгое сравнение, и свяжите ответ с определением .
- Усредните средние значения двух пакетов без весов, хотя числа токенов различаются. Сравните результат с формулой взвешивания по токенам.
- Пусть AdamW запишет новые значения в существующие узлы параметров. Объясните, почему реестр, компоненты декодера и связанная с эмбеддингом выходная проекция увидят обновление без повторной сборки декодера.
- Проведите линию между пятью валидационными отметками. Перечислите добавленные ею неизмеренные утверждения, затем удалите линию и вернитесь к отдельным измеренным значениям.
Проверьте рассуждение
Testотклоняется до прямого прохода, потому что тестовая выборка не может обновлять или выбирать модель.- Расписание контрольных точек должно включать последнее запланированное обновление, чтобы у завершённого запуска было заключительное состояние для сравнения.
- Побеждает первое значение, равное минимуму: состояние заменяется только при строгом уменьшении.
- Средние значения пакетов нужно взвесить пропорционально фактическому числу предсказываемых токенов.
- Все эти ссылки указывают на те же узлы
TensorValue. Значение меняется внутри узла, поэтому связи сохраняются, в том числе связь между эмбеддингом и выходной проекцией, а следующий прямой проход читает обновлённые значения. - Обоснованы только десять измеренных отметок и точная таблица; значения между контрольными точками неизвестны.
Зафиксируйте выбранный декодер до локальной тестовой оценки
К этому этапу декодер умеет выполнить полный план обучения с заранее заданным числом шагов и вернуть зафиксированное состояние, выбранное по валидации. В главе 34 один локальный экземпляр оценщика получит доступ к фиксированным тестовым данным после выбора и корректно сопоставит это состояние с зафиксированной базовой моделью.
После просмотра тестового результата нельзя менять расписание, предел нормы,
число обновлений или выбранную контрольную точку. Глава 33 заканчивается
завершённым выбором модели и проверенным отклонением Test на границе обучения.
Локальный оценщик главы 34 использует свой единственный доступ для оценки, а не
для выбора. Этот счётчик относится к одному экземпляру оценщика в одном запуске,
а не ко всей истории проверок фиксированного набора тестовых данных в репозитории.