32 · Версия материала 4
Соберите декодер и свяжите веса проекции на словарь
Разберитесь, как эмбеддинги токенов, повторяющиеся каузальные блоки, итоговый RMSNorm и одна общая таблица словаря создают дифференцируемые логиты следующего токена.
Предскажите оси до трассировки декодера
Соберём из выбора эмбеддингов по ID токенов, повторяющихся блоков декодера с предварительной нормализацией, итогового RMSNorm и одной действительно общей проекции на словарь дифференцируемые логиты. В примере ID токенов проходят через декодер из двух блоков. Размер словаря , ширина модели , число голов внимания равно двум, ширина сети прямого распространения , а ёмкость контекста равна . Целевые ID: .
До запуска предскажите пять структурных результатов:
- выбор эмбеддингов вернёт форму ;
- оба блока декодера и итоговый RMSNorm сохранят форму ;
- проекция на словарь вернёт форму ;
token_embedding.weightвстретится в списке параметров один раз, хотя в ленте вычислений к нему ведут два пути;- изменение только токена в позиции не сможет изменить строки логитов и .
Детерминированный пример даёт и численные значения. В позиции токена строка логитов равна
Наибольшее значение соответствует ID словаря . Среднее индексированное отрицательное логарифмическое правдоподобие по трём целевым позициям равно . Это свидетельство работы маленькой необученной модели, а не свидетельство того, что она уже выучила язык.
Повторно используйте таблицу эмбеддингов в конце стека
Полный прямой проход задаётся формулой
Эта запись относится к положительной глубине . На допустимой границе с нулём блоков пустая композиция блоков является тождественным преобразованием, поэтому формула сокращается до
Селекторы токенов удовлетворяют условию . Выбор строк из таблицы создаёт остаточный поток из . Каждый сохраняет эту форму. Итоговый также её сохраняет. Умножение на меняет только последнюю ось, поэтому
Здесь — тензор логитов, а не скалярное значение функции потерь. Для целевых ID пример вычисляет
Если компоненты не используют смещения, один блок содержит скаляров. Во всей модели с общими весами их число равно
В нашем примере получается . Отдельная матрица проекции на словарь добавила бы скаляров и увеличила итог до .
Различайте один параметр и два его применения
- — тензор ранга два с пакетами целочисленных ID токенов.
- — одна обучаемая таблица «словарь на признаки».
- выбирает из этой таблицы строки по ID токенов для входного применения.
- — -й каузальный блок декодера с предварительной нормализацией. Одинаковая конфигурация не означает, что блоки используют общие параметры.
- — глубина декодера. Эта реализация принимает ноль, один и большее число блоков.
- Итоговый имеет собственный коэффициент, отдельный от коэффициентов всех блоков.
- — дифференцируемое транспонированное представление , а не ещё один параметр.
- — размер пакета, — длина последовательности, а — размер словаря.
- — ширина остаточного потока, а — скрытая ширина каждой ветви SwiGLU.
- содержит по одному целевому ID словаря для каждой пары .
- содержит логиты словаря, а — скалярное среднее значение функции потерь.
Связывание весов — это не просто одинаковая инициализация. В модели нет ни
lm_head.weight, ни скопированной матрицы, ни шага синхронизации. Обратный
режим достигает token_embedding.weight один раз через выбранные входные
строки и ещё раз через выходную проекцию, а затем складывает оба вклада в одном
листе:
От раздельных рекуррентных компонентов к единому стеку декодера
Сначала рекуррентные языковые модели использовали раздельные входную и выходную таблицы. Затем появились общие веса словарных преобразований и повторяющиеся каузальные слои Transformer. Так выглядит ограниченный путь от раздельных рекуррентных компонентов к устройству декодерной LLM, собираемой в этой главе.
Using the Output Embedding to Improve Language Models задаёт исходную точку сравнения. Пресс и Вольф выделяют в рекуррентных нейронных языковых моделях входные эмбеддинги, промежуточное вычисление и матрицу выходных оценок, рекомендуют связать веса двух таблиц и рассматривают обновление общей матрицы как сумму вкладов от обеих ролей. В ранних рекуррентных нейронных языковых моделях входные эмбеддинги, пошаговое рекуррентное вычисление и отдельный классификатор по словарю обычно были разными компонентами. Поэтому входная и выходная таблицы слов могли иметь независимые параметры и обновления.
Attention Is All You Need меняет архитектуру последовательности. Васвани и соавторы описывают стек декодера Transformer с каузальной маской и сообщают, что эмбеддинги и линейное преобразование перед softmax используют одну матрицу. Полный декодер для перевода в этой работе также содержит внимание к выходу энкодера и постнормализацию, поэтому он не совпадает с нашим декодером только с предварительной нормализацией.
Language Models are Unsupervised Multitask Learners показывает следующий порядок компонентов на уровне модели. Рэдфорд и соавторы описывают GPT-2 как многослойную языковую модель Transformer, где нормализация стоит на входе каждого подблока, а после последнего блока применяется дополнительная нормализация. В этой главе используется только такой порядок: мы не приписываем маленькому примеру масштаб, инициализацию, токенизатор, контекст или обученные параметры GPT-2.
LLaMA даёт ограниченный пример современного семейства моделей. Туврон и соавторы используют в современном семействе языковых моделей Transformer предварительную нормализацию RMSNorm, SwiGLU, RoPE и каузальное внимание, а глубину задают как отдельный параметр архитектуры. LLM только с декодером преобразует ID токенов в логиты словаря с помощью стека повторяющихся каузальных остаточных блоков. В этом курсе используются уже изученные RMSNorm, RoPE, внимание и SwiGLU, а общие веса эмбеддингов и выходной проекции выбраны явно и не объявляются обязательными для всех моделей.
Связывание весов позволило одной матрице «словарь на признаки» выполнять обе роли, Transformer объединил слои с каузальной маской в стек, а в GPT-2 нормализация расположена на входе каждого подблока и ещё раз перед проекцией на словарь. Поэтому граница модели сменилась: вместо эмбеддинга токена, пошагово обновляемого рекуррентного состояния и отдельного классификатора появились повторяющиеся блоки с каузальной маской, итоговые скрытые состояния которых нормализуются и проецируются в логиты словаря. Связывание весов уменьшает число параметров, но требует складывать в одном листе оба градиентных вклада.
Исполняемый пример сравнивает модель с общей таблицей и модель с отдельным классификатором словаря. Модель с общими весами содержит скаляра, а такая же модель с отдельным классификатором словаря формы содержала бы . Если по очереди отсоединить каждое из двух применений, можно проверить, что полный градиент таблицы равен сумме вкладов от выбора строк и от выходной проекции. Это выделяет следствие совместного использования параметра, но не выдаёт маленький пример за рекуррентную модель-предшественницу, Transformer, GPT-2 или LLaMA.
rust/demos/ch32-decoder-model/src/lib.rs#tied-gradient-proof fn tied_role_gradient(include_lookup: bool, include_head: bool) -> Vec<f64> {
let table = NamedParameter::from_tensor(
"token_embedding.weight",
tensor(&[VOCABULARY, MODEL_WIDTH], &PROBE_TABLE),
)
.expect("probe table must be valid");
let embedding = Embedding::from_parameter(table.clone()).expect("probe table must embed");
let lookup = embedding
.forward(&[0, 1], &[1, 2])
.expect("probe lookup must be valid");
let lookup = if include_lookup {
lookup
} else {
lookup.detach()
};
let normalized = RmsNorm::new("final_norm.gain", MODEL_WIDTH, RMS_EPSILON)
.expect("probe norm must be valid")
.forward(&lookup)
.expect("probe norm forward must be valid");
let head_source = if include_head {
table.tensor().clone()
} else {
table.tensor().detach()
};
let logits = normalized
.matmul(
&head_source
.transpose(0, 1)
.expect("probe head transpose must be valid"),
)
.expect("probe tied projection must be valid");
logits
.indexed_mean_nll(2, &[1, 2])
.expect("probe indexed loss must be valid")
.backward_with_seed(&tensor(&[], &[1.0]).view(), GraphRetention::Retain)
.expect("probe backward must succeed");
table.tensor().gradient().map_or_else(
|| vec![0.0; PROBE_TABLE.len()],
|gradient| gradient.as_slice().to_vec(),
)
}
fn tied_gradient_decomposition_error() -> f64 {
let full = tied_role_gradient(true, true);
let lookup = tied_role_gradient(true, false);
let head = tied_role_gradient(false, true);
full.iter()
.zip(lookup.iter().zip(head.iter()))
.map(|(full, (lookup, head))| (full - lookup - head).abs())
.fold(0.0, f64::max)
} Явно задайте полную границу модели на Rust
DecoderModelError различает ошибки словаря, ширины, голов, сети прямого
распространения, глубины, контекста, RoPE, RMSNorm, компонентов, имён
параметров, токенов, целей и ленты вычислений. Ошибки делегированных блоков,
эмбеддингов, нормализации, проекции и индексированной функции потерь сохраняют
свой источник, а не превращаются в одну общую ошибку модели.
rust/crates/llm-from-scratch/src/models/decoder.rs#decoder-model-errors /// A model-owned taped operation that rejected a forward or loss calculation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecoderModelStage {
TiedWeightTranspose,
TiedVocabularyProjection,
IndexedMeanLoss,
}
impl fmt::Display for DecoderModelStage {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::TiedWeightTranspose => "transpose tied embedding weight",
Self::TiedVocabularyProjection => "project tied vocabulary logits",
Self::IndexedMeanLoss => "indexed mean negative log likelihood",
})
}
}
/// Which normalization inside a repeated block disagreed with model config.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DecoderModelNorm {
Attention,
FeedForward,
}
impl fmt::Display for DecoderModelNorm {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Attention => "attention",
Self::FeedForward => "feed-forward",
})
}
}
/// A rejected decoder configuration, component assembly, input, or tape stage.
#[derive(Clone, Debug, PartialEq)]
pub enum DecoderModelError {
EmptyVocabulary,
ZeroModelWidth,
ZeroHeadCount,
ModelWidthNotDivisible {
model_width: usize,
heads: usize,
},
OddHeadWidth {
head_width: usize,
},
ZeroFeedForwardWidth,
ZeroPositionCapacity,
InvalidRopeBase {
value: f64,
},
InvalidRmsEpsilon {
value: f64,
},
LayerAllocationFailed {
layers: usize,
},
ParameterAllocationFailed {
tensors: usize,
},
ParameterCountMismatch {
expected: usize,
actual: usize,
},
TargetAllocationFailed {
targets: usize,
},
Embedding(EmbeddingError),
Block {
layer: usize,
source: DecoderBlockError,
},
FinalNorm(RmsNormError),
Initialization(InitializationError),
LayerCountMismatch {
expected: usize,
actual: usize,
},
EmbeddingVocabularyMismatch {
expected: usize,
actual: usize,
},
EmbeddingWidthMismatch {
expected: usize,
actual: usize,
},
BlockModelWidthMismatch {
layer: usize,
expected: usize,
actual: usize,
},
BlockHeadCountMismatch {
layer: usize,
expected: usize,
actual: usize,
},
BlockFeedForwardWidthMismatch {
layer: usize,
expected: usize,
actual: usize,
},
BlockPositionCapacityMismatch {
layer: usize,
expected: usize,
actual: usize,
},
BlockRopeBaseMismatch {
layer: usize,
expected: f64,
actual: f64,
},
BlockRmsEpsilonMismatch {
layer: usize,
norm: DecoderModelNorm,
expected: f64,
actual: f64,
},
FinalNormWidthMismatch {
expected: usize,
actual: usize,
},
FinalNormEpsilonMismatch {
expected: f64,
actual: f64,
},
ParameterNameMismatch {
index: usize,
expected: String,
actual: String,
},
TokenRank {
rank: usize,
},
EmptyBatch,
EmptyTokens,
TokenCountOverflow {
batch: usize,
tokens: usize,
},
TokenCountMismatch {
expected: usize,
actual: usize,
},
ContextLengthExceeded {
tokens: usize,
max_positions: usize,
},
TargetCountMismatch {
expected: usize,
actual: usize,
},
TargetIdOutOfBounds {
position: usize,
id: u32,
vocabulary_size: usize,
},
Autodiff {
stage: DecoderModelStage,
source: TensorAutodiffError,
},
}
impl fmt::Display for DecoderModelError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyVocabulary => {
formatter.write_str("decoder vocabulary must contain at least one token")
}
Self::ZeroModelWidth => formatter.write_str("decoder model width must be nonzero"),
Self::ZeroHeadCount => formatter.write_str("decoder head count must be nonzero"),
Self::ModelWidthNotDivisible { model_width, heads } => write!(
formatter,
"decoder model width {model_width} must be divisible by head count {heads}"
),
Self::OddHeadWidth { head_width } => write!(
formatter,
"decoder per-head width must be even for RoPE, got {head_width}"
),
Self::ZeroFeedForwardWidth => {
formatter.write_str("decoder feed-forward width must be nonzero")
}
Self::ZeroPositionCapacity => {
formatter.write_str("decoder context capacity must be nonzero")
}
Self::InvalidRopeBase { value } => {
write!(
formatter,
"decoder RoPE base must be finite and positive, got {value:?}"
)
}
Self::InvalidRmsEpsilon { value } => write!(
formatter,
"decoder RMSNorm epsilon must be finite and nonnegative, got {value:?}"
),
Self::LayerAllocationFailed { layers } => {
write!(formatter, "could not reserve {layers} decoder blocks")
}
Self::ParameterAllocationFailed { tensors } => {
write!(
formatter,
"could not reserve {tensors} decoder parameter handles"
)
}
Self::ParameterCountMismatch { expected, actual } => write!(
formatter,
"decoder parameter count must be {expected}, got {actual}"
),
Self::TargetAllocationFailed { targets } => {
write!(
formatter,
"could not reserve {targets} decoder target indices"
)
}
Self::Embedding(source) => write!(formatter, "token embedding: {source}"),
Self::Block { layer, source } => {
write!(formatter, "decoder block {layer}: {source}")
}
Self::FinalNorm(source) => write!(formatter, "final RMSNorm: {source}"),
Self::Initialization(source) => source.fmt(formatter),
Self::LayerCountMismatch { expected, actual } => write!(
formatter,
"decoder config needs {expected} blocks, but received {actual}"
),
Self::EmbeddingVocabularyMismatch { expected, actual } => write!(
formatter,
"token embedding vocabulary must be {expected}, got {actual}"
),
Self::EmbeddingWidthMismatch { expected, actual } => write!(
formatter,
"token embedding width must be {expected}, got {actual}"
),
Self::BlockModelWidthMismatch {
layer,
expected,
actual,
} => write!(
formatter,
"decoder block {layer} model width must be {expected}, got {actual}"
),
Self::BlockHeadCountMismatch {
layer,
expected,
actual,
} => write!(
formatter,
"decoder block {layer} head count must be {expected}, got {actual}"
),
Self::BlockFeedForwardWidthMismatch {
layer,
expected,
actual,
} => write!(
formatter,
"decoder block {layer} feed-forward width must be {expected}, got {actual}"
),
Self::BlockPositionCapacityMismatch {
layer,
expected,
actual,
} => write!(
formatter,
"decoder block {layer} position capacity must be {expected}, got {actual}"
),
Self::BlockRopeBaseMismatch {
layer,
expected,
actual,
} => write!(
formatter,
"decoder block {layer} RoPE base must be {expected:?}, got {actual:?}"
),
Self::BlockRmsEpsilonMismatch {
layer,
norm,
expected,
actual,
} => write!(
formatter,
"decoder block {layer} {norm} RMSNorm epsilon must be {expected:?}, got {actual:?}"
),
Self::FinalNormWidthMismatch { expected, actual } => write!(
formatter,
"final RMSNorm width must be {expected}, got {actual}"
),
Self::FinalNormEpsilonMismatch { expected, actual } => write!(
formatter,
"final RMSNorm epsilon must be {expected:?}, got {actual:?}"
),
Self::ParameterNameMismatch {
index,
expected,
actual,
} => write!(
formatter,
"decoder parameter {index} must be named {expected:?}, got {actual:?}"
),
Self::TokenRank { rank } => write!(
formatter,
"decoder token shape must have rank two [batch, tokens], got rank {rank}"
),
Self::EmptyBatch => formatter.write_str("decoder batch must be nonempty"),
Self::EmptyTokens => formatter.write_str("decoder token sequence must be nonempty"),
Self::TokenCountOverflow { batch, tokens } => write!(
formatter,
"decoder token count overflows for batch {batch} and length {tokens}"
),
Self::TokenCountMismatch { expected, actual } => write!(
formatter,
"decoder token shape needs {expected} IDs, but received {actual}"
),
Self::ContextLengthExceeded {
tokens,
max_positions,
} => write!(
formatter,
"decoder sequence length {tokens} exceeds context capacity {max_positions}"
),
Self::TargetCountMismatch { expected, actual } => write!(
formatter,
"decoder loss needs {expected} targets, but received {actual}"
),
Self::TargetIdOutOfBounds {
position,
id,
vocabulary_size,
} => write!(
formatter,
"target token ID {id} at flat position {position} is out of bounds for vocabulary size {vocabulary_size}"
),
Self::Autodiff { stage, source } => write!(formatter, "decoder {stage}: {source}"),
}
}
}
impl Error for DecoderModelError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Embedding(source) => Some(source),
Self::Block { source, .. } => Some(source),
Self::FinalNorm(source) => Some(source),
Self::Initialization(source) => Some(source),
Self::Autodiff { source, .. } => Some(source),
_ => None,
}
}
}
impl From<InitializationError> for DecoderModelError {
fn from(source: InitializationError) -> Self {
Self::Initialization(source)
}
}
fn autodiff_error(
stage: DecoderModelStage,
) -> impl FnOnce(TensorAutodiffError) -> DecoderModelError {
move |source| DecoderModelError::Autodiff { stage, source }
} DecoderModelConfig проверяет все настройки архитектуры даже при .
Поэтому пустой стек не может скрыть неверное расположение голов, пустой
словарь, недостаточную ёмкость контекста, неконечное основание RoPE или
недопустимый эпсилон RMSNorm.
rust/crates/llm-from-scratch/src/models/decoder.rs#decoder-model-config /// Every dimension and numerical constant owned by one decoder model.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DecoderModelConfig {
vocabulary_size: usize,
model_width: usize,
heads: usize,
feed_forward_width: usize,
layers: usize,
max_positions: usize,
rope_base: f64,
rms_epsilon: f64,
}
impl DecoderModelConfig {
#[allow(clippy::too_many_arguments)]
pub const fn new(
vocabulary_size: usize,
model_width: usize,
heads: usize,
feed_forward_width: usize,
layers: usize,
max_positions: usize,
rope_base: f64,
rms_epsilon: f64,
) -> Self {
Self {
vocabulary_size,
model_width,
heads,
feed_forward_width,
layers,
max_positions,
rope_base,
rms_epsilon,
}
}
pub const fn vocabulary_size(self) -> usize {
self.vocabulary_size
}
pub const fn model_width(self) -> usize {
self.model_width
}
pub const fn heads(self) -> usize {
self.heads
}
pub const fn head_width(self) -> Option<usize> {
if self.heads == 0 || !self.model_width.is_multiple_of(self.heads) {
None
} else {
Some(self.model_width / self.heads)
}
}
pub const fn feed_forward_width(self) -> usize {
self.feed_forward_width
}
pub const fn layers(self) -> usize {
self.layers
}
pub const fn max_positions(self) -> usize {
self.max_positions
}
pub const fn rope_base(self) -> f64 {
self.rope_base
}
pub const fn rms_epsilon(self) -> f64 {
self.rms_epsilon
}
pub const fn block_config(self) -> DecoderBlockConfig {
DecoderBlockConfig::new(
self.model_width,
self.heads,
self.feed_forward_width,
self.max_positions,
self.rope_base,
self.rms_epsilon,
)
}
}
fn validate_config(config: DecoderModelConfig) -> Result<(), DecoderModelError> {
if config.vocabulary_size == 0 {
return Err(DecoderModelError::EmptyVocabulary);
}
if config.model_width == 0 {
return Err(DecoderModelError::ZeroModelWidth);
}
if config.heads == 0 {
return Err(DecoderModelError::ZeroHeadCount);
}
if !config.model_width.is_multiple_of(config.heads) {
return Err(DecoderModelError::ModelWidthNotDivisible {
model_width: config.model_width,
heads: config.heads,
});
}
let head_width = config.model_width / config.heads;
if !head_width.is_multiple_of(2) {
return Err(DecoderModelError::OddHeadWidth { head_width });
}
if config.feed_forward_width == 0 {
return Err(DecoderModelError::ZeroFeedForwardWidth);
}
if config.max_positions == 0 {
return Err(DecoderModelError::ZeroPositionCapacity);
}
if !config.rope_base.is_finite() || config.rope_base <= 0.0 {
return Err(DecoderModelError::InvalidRopeBase {
value: config.rope_base,
});
}
if !config.rms_epsilon.is_finite() || config.rms_epsilon < 0.0 {
return Err(DecoderModelError::InvalidRmsEpsilon {
value: config.rms_epsilon,
});
}
Ok(())
}
fn expected_parameter_tensors(layers: usize) -> Result<usize, DecoderModelError> {
layers
.checked_mul(BLOCK_PARAMETER_SUFFIXES.len())
.and_then(|count| count.checked_add(2))
.ok_or(DecoderModelError::ParameterAllocationFailed {
tensors: usize::MAX,
})
} При создании модель сначала получает token_embedding.weight, затем по девять
параметров в стабильном порядке для каждой записи blocks.N, а в конце —
final_norm.gain. В примере с двумя блоками есть тензоров и скаляра.
Отдельного параметра выходной проекции намеренно нет. Инициализация использует
временную копию состояния генератора псевдослучайных чисел и меняет состояние
генератора вызывающего кода только после того, как проверены все компоненты и
связи между ними.
Стабильный порядок также задаёт единую схему списка параметров декодера. Прежде
чем from_parameters свяжет готовый список параметров с компонентами модели,
validate_parameter_layout проверяет конфигурацию, точное число тензоров,
имя тензора в каждой позиции списка и форму, которую ожидает соответствующий
компонент. В позиции с индексом ноль находится единственная запись
token_embedding.weight, используемая и для эмбеддингов, и для выходной
проекции; отдельной записи для выходной проекции нет.
Для проверки достаточно по очереди временно прочитать имя и тензор каждого
параметра. Проверка не создаёт узлов параметров, не связывает с ними компоненты
и не копирует значения тензоров. Поэтому успешная проверка означает только, что
список имеет требуемую схему. Затем from_parameters передаёт эмбеддингу,
блокам и итоговому RMSNorm ссылки на проверенные листовые узлы параметров.
Только после этой привязки выбор строк и выходная проекция обращаются к одному и
тому же узлу эмбеддинга.
forward принимает только непустые тензоры ID токенов ранга два, где
. Метод выбирает строки таблицы, вызывает каждый
блок со смещением позиции ноль, применяет итоговый RMSNorm, транспонирует
исходный лист таблицы и сразу умножает результат, получая логиты. Метод loss
сначала проверяет по одной допустимой цели для каждого токена, а затем применяет
среднее индексированное отрицательное логарифмическое правдоподобие по оси
словаря .
rust/crates/llm-from-scratch/src/models/decoder.rs#decoder-model-layer /// Inspectable values from lookup through the tied vocabulary projection.
#[derive(Clone, Debug)]
pub struct DecoderModelForward {
embedding: TensorValue,
blocks: Vec<DecoderBlockForward>,
final_norm: RmsNormForward,
logits: TensorValue,
}
impl DecoderModelForward {
pub fn embedding(&self) -> &TensorValue {
&self.embedding
}
pub fn blocks(&self) -> &[DecoderBlockForward] {
&self.blocks
}
pub fn final_norm(&self) -> &RmsNormForward {
&self.final_norm
}
pub fn logits(&self) -> &TensorValue {
&self.logits
}
pub fn into_logits(self) -> TensorValue {
self.logits
}
}
/// Token lookup, repeated decoder blocks, final RMSNorm, and one tied head.
#[derive(Clone, Debug)]
pub struct DecoderModel {
config: DecoderModelConfig,
embedding: Embedding,
blocks: Vec<DecoderBlock>,
final_norm: RmsNorm,
parameters: NamedParameters,
}
impl DecoderModel {
/// Initializes the full model transactionally from one deterministic stream.
pub fn new(
config: DecoderModelConfig,
rng: &mut SplitMix64,
) -> Result<Self, DecoderModelError> {
validate_config(config)?;
let mut trial = rng.clone();
let embedding = Embedding::new(
"token_embedding.weight",
config.vocabulary_size,
config.model_width,
&mut trial,
)
.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 {
blocks.push(
DecoderBlock::new(format!("blocks.{layer}"), config.block_config(), &mut trial)
.map_err(|source| DecoderModelError::Block { layer, source })?,
);
}
let final_norm = RmsNorm::new("final_norm.gain", config.model_width, config.rms_epsilon)
.map_err(DecoderModelError::FinalNorm)?;
let model = Self::from_parts(config, embedding, blocks, final_norm)?;
*rng = trial;
Ok(model)
}
// region: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)
}
// endregion:decoder-parameter-rebuild
/// Assembles exact named components and rejects any configuration drift.
pub fn from_parts(
config: DecoderModelConfig,
embedding: Embedding,
blocks: Vec<DecoderBlock>,
final_norm: RmsNorm,
) -> Result<Self, DecoderModelError> {
validate_config(config)?;
if embedding.vocabulary_size() != config.vocabulary_size {
return Err(DecoderModelError::EmbeddingVocabularyMismatch {
expected: config.vocabulary_size,
actual: embedding.vocabulary_size(),
});
}
if embedding.embedding_width() != config.model_width {
return Err(DecoderModelError::EmbeddingWidthMismatch {
expected: config.model_width,
actual: embedding.embedding_width(),
});
}
if blocks.len() != config.layers {
return Err(DecoderModelError::LayerCountMismatch {
expected: config.layers,
actual: blocks.len(),
});
}
for (layer, block) in blocks.iter().enumerate() {
if block.model_width() != config.model_width {
return Err(DecoderModelError::BlockModelWidthMismatch {
layer,
expected: config.model_width,
actual: block.model_width(),
});
}
if block.attention().heads() != config.heads {
return Err(DecoderModelError::BlockHeadCountMismatch {
layer,
expected: config.heads,
actual: block.attention().heads(),
});
}
if block.feed_forward().hidden_width() != config.feed_forward_width {
return Err(DecoderModelError::BlockFeedForwardWidthMismatch {
layer,
expected: config.feed_forward_width,
actual: block.feed_forward().hidden_width(),
});
}
if block.attention().rope().max_positions() != config.max_positions {
return Err(DecoderModelError::BlockPositionCapacityMismatch {
layer,
expected: config.max_positions,
actual: block.attention().rope().max_positions(),
});
}
if block.attention().rope().base().to_bits() != config.rope_base.to_bits() {
return Err(DecoderModelError::BlockRopeBaseMismatch {
layer,
expected: config.rope_base,
actual: block.attention().rope().base(),
});
}
for (norm, epsilon) in [
(
DecoderModelNorm::Attention,
block.attention_norm().epsilon(),
),
(
DecoderModelNorm::FeedForward,
block.feed_forward_norm().epsilon(),
),
] {
if epsilon.to_bits() != config.rms_epsilon.to_bits() {
return Err(DecoderModelError::BlockRmsEpsilonMismatch {
layer,
norm,
expected: config.rms_epsilon,
actual: epsilon,
});
}
}
}
if final_norm.feature_width() != config.model_width {
return Err(DecoderModelError::FinalNormWidthMismatch {
expected: config.model_width,
actual: final_norm.feature_width(),
});
}
if final_norm.epsilon().to_bits() != config.rms_epsilon.to_bits() {
return Err(DecoderModelError::FinalNormEpsilonMismatch {
expected: config.rms_epsilon,
actual: final_norm.epsilon(),
});
}
let parameter_tensors = expected_parameter_tensors(config.layers)?;
let mut listed = Vec::new();
listed.try_reserve_exact(parameter_tensors).map_err(|_| {
DecoderModelError::ParameterAllocationFailed {
tensors: parameter_tensors,
}
})?;
listed.push(embedding.table().clone());
for block in &blocks {
listed.extend(block.parameters().iter().cloned());
}
listed.push(final_norm.gain().clone());
validate_parameter_names(listed.as_slice(), config.layers)?;
let parameters = NamedParameters::try_new(listed)?;
Ok(Self {
config,
embedding,
blocks,
final_norm,
parameters,
})
}
/// Runs the complete model while retaining one evidence record per layer.
pub fn forward(
&self,
token_ids: &[u32],
token_shape: &[usize],
) -> Result<DecoderModelForward, DecoderModelError> {
self.validate_tokens(token_ids, token_shape)?;
let embedding = self
.embedding
.forward(token_ids, token_shape)
.map_err(DecoderModelError::Embedding)?;
let mut current = embedding.clone();
let mut block_forwards = Vec::new();
block_forwards
.try_reserve_exact(self.blocks.len())
.map_err(|_| DecoderModelError::LayerAllocationFailed {
layers: self.blocks.len(),
})?;
for (layer, block) in self.blocks.iter().enumerate() {
let forward = block
.forward(¤t, 0)
.map_err(|source| DecoderModelError::Block { layer, source })?;
current = forward.output().clone();
block_forwards.push(forward);
}
let final_norm = self
.final_norm
.forward_with_intermediates(¤t)
.map_err(DecoderModelError::FinalNorm)?;
let tied_weight = self
.embedding
.table()
.tensor()
.transpose(0, 1)
.map_err(autodiff_error(DecoderModelStage::TiedWeightTranspose))?;
let logits = final_norm
.output()
.matmul(&tied_weight)
.map_err(autodiff_error(DecoderModelStage::TiedVocabularyProjection))?;
Ok(DecoderModelForward {
embedding,
blocks: block_forwards,
final_norm,
logits,
})
}
/// Computes one mean next-token loss over the vocabulary axis.
pub fn loss(
&self,
token_ids: &[u32],
token_shape: &[usize],
targets: &[u32],
) -> Result<TensorValue, DecoderModelError> {
let expected = self.validate_tokens(token_ids, token_shape)?;
if targets.len() != expected {
return Err(DecoderModelError::TargetCountMismatch {
expected,
actual: targets.len(),
});
}
let mut target_indices = Vec::new();
target_indices
.try_reserve_exact(expected)
.map_err(|_| DecoderModelError::TargetAllocationFailed { targets: expected })?;
for (position, &id) in targets.iter().enumerate() {
let valid = usize::try_from(id)
.ok()
.is_some_and(|index| index < self.config.vocabulary_size);
if !valid {
return Err(DecoderModelError::TargetIdOutOfBounds {
position,
id,
vocabulary_size: self.config.vocabulary_size,
});
}
target_indices
.push(usize::try_from(id).expect("validated target token ID must fit usize"));
}
self.forward(token_ids, token_shape)?
.into_logits()
.indexed_mean_nll(2, &target_indices)
.map_err(autodiff_error(DecoderModelStage::IndexedMeanLoss))
}
fn validate_tokens(
&self,
token_ids: &[u32],
token_shape: &[usize],
) -> Result<usize, DecoderModelError> {
if token_shape.len() != 2 {
return Err(DecoderModelError::TokenRank {
rank: token_shape.len(),
});
}
let batch = token_shape[0];
let tokens = token_shape[1];
if batch == 0 {
return Err(DecoderModelError::EmptyBatch);
}
if tokens == 0 {
return Err(DecoderModelError::EmptyTokens);
}
if tokens > self.config.max_positions {
return Err(DecoderModelError::ContextLengthExceeded {
tokens,
max_positions: self.config.max_positions,
});
}
let expected = batch
.checked_mul(tokens)
.ok_or(DecoderModelError::TokenCountOverflow { batch, tokens })?;
if token_ids.len() != expected {
return Err(DecoderModelError::TokenCountMismatch {
expected,
actual: token_ids.len(),
});
}
Ok(expected)
}
pub const fn config(&self) -> DecoderModelConfig {
self.config
}
pub const fn embedding(&self) -> &Embedding {
&self.embedding
}
pub fn blocks(&self) -> &[DecoderBlock] {
&self.blocks
}
pub const fn final_norm(&self) -> &RmsNorm {
&self.final_norm
}
/// The sole table used for both token lookup and vocabulary projection.
pub const fn tied_embedding(&self) -> &NamedParameter {
self.embedding.table()
}
pub fn parameters(&self) -> &[NamedParameter] {
self.parameters.as_slice()
}
pub fn parameter_count(&self) -> usize {
self.parameters
.iter()
.map(|parameter| parameter.tensor().value().len())
.sum()
}
} Численная проверка сравнивает все координат общей таблицы и все координаты итогового коэффициента с центральными конечными разностями. Её допуск равен
Полная функция потерь двухблочной модели также создаёт конечные градиенты для всех зарегистрированных тензоров параметров.
rust/demos/ch32-decoder-model/src/lib.rs#gradient-checks #[derive(Clone, Debug, PartialEq)]
pub struct GradientEvidence {
pub tied_table_checks: usize,
pub final_norm_checks: usize,
pub tolerance: f64,
pub passed: bool,
pub stack_parameter_tensors: usize,
pub stack_gradient_tensors: usize,
pub decomposition_error: f64,
}
fn gradient_evidence(model: &DecoderModel) -> Result<GradientEvidence, Box<dyn Error>> {
let probe = zero_layer_model(
tensor(&[VOCABULARY, MODEL_WIDTH], &PROBE_TABLE),
tensor(&[MODEL_WIDTH], &PROBE_GAIN),
);
probe.loss(&[0, 1], &[1, 2], &[1, 2])?.backward()?;
let table_gradient = probe
.tied_embedding()
.tensor()
.gradient()
.expect("probe tied table must receive a gradient");
let gain_gradient = probe
.final_norm()
.gain()
.tensor()
.gradient()
.expect("probe final gain must receive a gradient");
let table_report = sampled_tensor_gradient_check(
&mut tensor(&[VOCABULARY, MODEL_WIDTH], &PROBE_TABLE),
&table_gradient.view(),
STEP,
TOLERANCE,
PROBE_TABLE.len(),
|candidate| zero_layer_loss(candidate.clone(), tensor(&[MODEL_WIDTH], &PROBE_GAIN)),
)?;
let gain_report = sampled_tensor_gradient_check(
&mut tensor(&[MODEL_WIDTH], &PROBE_GAIN),
&gain_gradient.view(),
STEP,
TOLERANCE,
PROBE_GAIN.len(),
|candidate| {
zero_layer_loss(
tensor(&[VOCABULARY, MODEL_WIDTH], &PROBE_TABLE),
candidate.clone(),
)
},
)?;
model
.loss(&TOKEN_IDS, &[BATCH, TOKENS], &TARGET_IDS)?
.backward()?;
let stack_gradient_tensors = model
.parameters()
.iter()
.filter(|parameter| {
parameter
.tensor()
.gradient()
.is_some_and(|gradient| gradient.as_slice().iter().all(|value| value.is_finite()))
})
.count();
Ok(GradientEvidence {
tied_table_checks: table_report.checks.len(),
final_norm_checks: gain_report.checks.len(),
tolerance: TOLERANCE,
passed: table_report.passed
&& gain_report.passed
&& stack_gradient_tensors == model.parameters().len(),
stack_parameter_tensors: model.parameters().len(),
stack_gradient_tensors,
decomposition_error: tied_gradient_decomposition_error(),
})
} Детерминированный пример собирает точные значения этапов, логиты, среднюю потерю, сведения о владении параметрами, проверки глубины, именованные ошибки входов, каузальность, проверки градиентов и побитовую воспроизводимость.
rust/demos/ch32-decoder-model/src/lib.rs#learner-evidence pub fn learner_evidence() -> Result<LearnerEvidence, Box<dyn Error>> {
let model = initialized_model(LAYERS)?;
let forward = model.forward(&TOKEN_IDS, &[BATCH, TOKENS])?;
let mut stages = Vec::with_capacity(LAYERS + 2);
stages.push(stage("embedding", forward.embedding()));
for (layer, block) in forward.blocks().iter().enumerate() {
stages.push(stage(format!("block-{layer}"), block.output()));
}
stages.push(stage("final-norm", forward.final_norm().output()));
let logits = forward.logits().value_snapshot();
let loss = model
.loss(&TOKEN_IDS, &[BATCH, TOKENS], &TARGET_IDS)?
.value()
.as_slice()[0];
let replay = initialized_model(LAYERS)?
.forward(&TOKEN_IDS, &[BATCH, TOKENS])?
.logits()
.value_snapshot();
let expected_names = model
.parameters()
.iter()
.map(|parameter| parameter.name().to_owned())
.collect::<Vec<_>>();
let untied_parameter_scalars = model.parameter_count() + VOCABULARY * MODEL_WIDTH;
Ok(LearnerEvidence {
stages,
predictions: predictions(&logits),
logits: logits.clone(),
loss,
parameter_names: expected_names.clone(),
parameter_scalars: model.parameter_count(),
untied_parameter_scalars,
tied_parameter_name: model.tied_embedding().name().to_owned(),
tied_lookup_and_head: model
.embedding()
.table()
.tensor()
.is_same_node(model.tied_embedding().tensor()),
bias_free: expected_names.iter().all(|name| !name.contains("bias")),
stable_order: expected_names
.first()
.is_some_and(|name| name == "token_embedding.weight")
&& expected_names
.last()
.is_some_and(|name| name == "final_norm.gain"),
depths_valid: shape_evidence()?,
errors: error_evidence(),
causality: causality_evidence(&model)?,
gradients: gradient_evidence(&model)?,
replay_bitwise: logits == replay,
})
} rust/demos/ch32-decoder-model/src/lib.rs#learner-report pub fn render_report(evidence: &LearnerEvidence) -> String {
let token_one = &evidence.logits.as_slice()[VOCABULARY..2 * VOCABULARY];
format!(
concat!(
"chapter=32-decoder-model\n",
"config=batch:{batch} tokens:{tokens} vocabulary:{vocabulary} model_width:{model_width} layers:{layers} heads:{heads} head_width:{head_width} feed_forward_width:{feed_forward_width} context:{max_positions}\n",
"shape=embedding:{embedding:?} block_0:{block_0:?} block_1:{block_1:?} final_norm:{final_norm:?} logits:{logits:?}\n",
"token_1_logits={token_one}\n",
"targets=[1,2,3] mean_loss:{loss:.6}\n",
"prediction=token_0:{prediction_0} token_1:{prediction_1} token_2:{prediction_2}\n",
"tying=name:{tied_name} lookup_and_head:{tied} gradient_roles:lookup+output decomposition_error:{decomposition_error:.12}\n",
"parameters=tensors:{parameter_tensors} scalars:{parameter_scalars} untied_scalars:{untied_scalars} saved:{saved} bias_free:{bias_free} stable_order:{stable_order}\n",
"depths=zero_one_two:{depths} configuration_errors:{configuration} context_limit:{context} vocabulary_errors:{vocabulary_error} target_errors:{target}\n",
"causality=prefix_0_bitwise:{prefix_0} prefix_1_bitwise:{prefix_1} suffix_changed:{suffix}\n",
"gradcheck=tied_table:{table_checks} final_norm:{gain_checks} total:{total_checks} tolerance:{tolerance:.6} passed:{passed} stack_gradients:{stack_gradients}/{stack_parameters}\n",
"replay=bitwise:{replay}\n",
"next=train this decoder and select a state with validation loss only\n",
),
batch = BATCH,
tokens = TOKENS,
vocabulary = VOCABULARY,
model_width = MODEL_WIDTH,
layers = LAYERS,
heads = HEADS,
feed_forward_width = FEED_FORWARD_WIDTH,
max_positions = MAX_POSITIONS,
head_width = MODEL_WIDTH / HEADS,
embedding = evidence.stages[0].shape,
block_0 = evidence.stages[1].shape,
block_1 = evidence.stages[2].shape,
final_norm = evidence.stages[3].shape,
logits = evidence.logits.shape(),
token_one = values_text(token_one),
loss = evidence.loss,
prediction_0 = evidence.predictions[0],
prediction_1 = evidence.predictions[1],
prediction_2 = evidence.predictions[2],
tied_name = evidence.tied_parameter_name,
tied = evidence.tied_lookup_and_head,
decomposition_error = evidence.gradients.decomposition_error,
parameter_tensors = evidence.parameter_names.len(),
parameter_scalars = evidence.parameter_scalars,
untied_scalars = evidence.untied_parameter_scalars,
saved = evidence.untied_parameter_scalars - evidence.parameter_scalars,
bias_free = evidence.bias_free,
stable_order = evidence.stable_order,
depths = evidence.depths_valid,
configuration = evidence.errors.configuration,
context = evidence.errors.context,
vocabulary_error = evidence.errors.vocabulary,
target = evidence.errors.target,
prefix_0 = evidence.causality.prefix_0_bitwise,
prefix_1 = evidence.causality.prefix_1_bitwise,
suffix = evidence.causality.suffix_changed,
table_checks = evidence.gradients.tied_table_checks,
gain_checks = evidence.gradients.final_norm_checks,
total_checks = evidence.gradients.tied_table_checks + evidence.gradients.final_norm_checks,
tolerance = evidence.gradients.tolerance,
passed = evidence.gradients.passed,
stack_gradients = evidence.gradients.stack_gradient_tensors,
stack_parameters = evidence.gradients.stack_parameter_tensors,
replay = evidence.replay_bitwise,
)
} Исполняемый файл печатает только зафиксированный отчёт:
rust/demos/ch32-decoder-model/src/main.rs fn main() -> Result<(), Box<dyn std::error::Error>> {
let evidence = ch32_decoder_model::learner_evidence()?;
print!("{}", ch32_decoder_model::render_report(&evidence));
Ok(())
} Запустите cargo run --quiet --locked -p ch32-decoder-model. Стандартный вывод
побайтно совпадает с rust/demos/ch32-decoder-model/expected.txt, включая
последний перевод строки.
Проследите одну таблицу в начале и конце прямого прохода
Детерминированный запуск записывает конфигурацию модели, ID токенов, каждую строку признаков после выбора эмбеддингов и каждого этапа стека, все логиты словаря, цели, предсказания, среднюю потерю, свидетельство общих весов, числа параметров, границы входов, каузальность, проверки градиентов и воспроизводимость.
rust/demos/ch32-decoder-model/src/diagram_trace.rs //! Locale-neutral static trace consumed by the Chapter 32 diagram parser.
use std::fmt::Write;
use crate::{
BATCH, FEED_FORWARD_WIDTH, HEADS, LAYERS, LearnerEvidence, MAX_POSITIONS, MODEL_WIDTH,
TARGET_IDS, TOKEN_IDS, TOKENS, VOCABULARY,
};
fn values(values: &[f64]) -> String {
format!(
"[{}]",
values
.iter()
.map(|value| format!("{value:.6}"))
.collect::<Vec<_>>()
.join(",")
)
}
fn shape(shape: &[usize]) -> String {
format!(
"[{}]",
shape
.iter()
.map(usize::to_string)
.collect::<Vec<_>>()
.join(",")
)
}
pub fn render_trace(evidence: &LearnerEvidence) -> String {
let mut output = String::new();
writeln!(output, "DECODER_MODEL_TRACE_V1").unwrap();
writeln!(
output,
"config batch={BATCH} tokens={TOKENS} vocabulary={VOCABULARY} model_width={MODEL_WIDTH} layers={LAYERS} heads={HEADS} feed_forward_width={FEED_FORWARD_WIDTH} context={MAX_POSITIONS}"
)
.unwrap();
writeln!(
output,
"tokens shape=[{BATCH},{TOKENS}] values=[{},{},{}]",
TOKEN_IDS[0], TOKEN_IDS[1], TOKEN_IDS[2]
)
.unwrap();
writeln!(
output,
"targets values=[{},{},{}]",
TARGET_IDS[0], TARGET_IDS[1], TARGET_IDS[2]
)
.unwrap();
for stage in &evidence.stages {
for token in 0..TOKENS {
let start = token * MODEL_WIDTH;
writeln!(
output,
"stage name={} shape={} token={} values={}",
stage.name,
shape(&stage.shape),
token,
values(&stage.values.as_slice()[start..start + MODEL_WIDTH])
)
.unwrap();
}
}
for token in 0..TOKENS {
let start = token * VOCABULARY;
writeln!(
output,
"logits token={} values={}",
token,
values(&evidence.logits.as_slice()[start..start + VOCABULARY])
)
.unwrap();
}
writeln!(
output,
"predictions values=[{},{},{}]",
evidence.predictions[0], evidence.predictions[1], evidence.predictions[2]
)
.unwrap();
writeln!(output, "loss mean={:.6}", evidence.loss).unwrap();
writeln!(
output,
"tying name={} lookup_and_head={} gradient_roles=lookup+output decomposition_error={:.12}",
evidence.tied_parameter_name,
evidence.tied_lookup_and_head,
evidence.gradients.decomposition_error
)
.unwrap();
writeln!(
output,
"parameters tensors={} scalars={} untied_scalars={} saved={} bias_free={} stable_order={}",
evidence.parameter_names.len(),
evidence.parameter_scalars,
evidence.untied_parameter_scalars,
evidence.untied_parameter_scalars - evidence.parameter_scalars,
evidence.bias_free,
evidence.stable_order
)
.unwrap();
writeln!(
output,
"depths zero_one_two={} configuration_errors={} context_limit={} vocabulary_errors={} target_errors={}",
evidence.depths_valid,
evidence.errors.configuration,
evidence.errors.context,
evidence.errors.vocabulary,
evidence.errors.target
)
.unwrap();
writeln!(
output,
"causality prefix_0_bitwise={} prefix_1_bitwise={} suffix_changed={}",
evidence.causality.prefix_0_bitwise,
evidence.causality.prefix_1_bitwise,
evidence.causality.suffix_changed
)
.unwrap();
writeln!(
output,
"gradcheck tied_table={} final_norm={} total={} tolerance={:.6} passed={} stack_gradients={}/{}",
evidence.gradients.tied_table_checks,
evidence.gradients.final_norm_checks,
evidence.gradients.tied_table_checks + evidence.gradients.final_norm_checks,
evidence.gradients.tolerance,
evidence.gradients.passed,
evidence.gradients.stack_gradient_tensors,
evidence.gradients.stack_parameter_tensors
)
.unwrap();
writeln!(output, "replay bitwise={}", evidence.replay_bitwise).unwrap();
writeln!(output, "END_DECODER_MODEL_TRACE").unwrap();
output
} Проследите общую таблицу словаря через весь декодер
Проследите точные строки признаков для токенов из вычислений Rust через выбор эмбеддингов, два разных каузальных блока, итоговый RMSNorm и транспонированную проекцию обратно в пять логитов словаря.
- Двойная граница: разные веса повторяющихся блоков
- Пунктирная граница: две роли одного параметра
- Двойное подчёркивание: проверено вычислениями Rust
Один путь декодера и две роли одной таблицы
Ширина остаточного потока остаётся равной четырём после выбора эмбеддингов, обоих блоков и итоговой нормализации. Только общая проекция меняет размер последней оси на пять — размер словаря.
ID токенов
Выбор эмбеддингов
Два разных блока декодера
blocks.0, blocks.1 Итоговый RMSNorm
Логиты словаря
Один параметр используется дважды
token_embedding.weight Выбор строк по ID: Роль транспонированной проекции:
Проверьте строки скрытых признаков и все логиты
Все строки этапов и логиты получены в одном детерминированном запуске, поэтому таблицы сохраняют единый путь от выбора эмбеддингов до функции потерь.
| Этап | Позиция токена | Форма | Точная строка признаков |
|---|---|---|---|
| Выбор эмбеддинга | |||
| После блока декодера 1 | |||
| После блока декодера 2 | |||
| После итогового RMSNorm |
| Позиция токена | Предсказание | Цель | |||||
|---|---|---|---|---|---|---|---|
Среднее значение индексированной функции потерь:
Проверьте владение, общие градиенты, каузальность и границы глубины
Число параметров, разложение по двум ролям, неизменность префикса, допустимые глубины и именованные ошибки входов получены на одном детерминированном примере.
Единый стабильный список параметров
Сэкономлено скаляров:
tensors=20, bias_free=true Градиентные вклады складываются в одном листовом узле
checked=24, stack=20/20 Изменение только суффикса
: Побитово без изменений
: Побитово без изменений
: Численно изменилось
Проверки глубины и границ
: Двойное подчёркивание: проверено вычислениями Rust
configuration=true context=true vocabulary=true targets=true На схеме есть одна карточка token_embedding.weight. В начале пути отношение
выбора строк использует , а после итоговой нормализации отношение проекции
использует . Одна карточка здесь принципиальна: две отдельные карточки
матриц ошибочно означали бы два скопированных параметра. Двойные и пунктирные
границы сохраняют различие между повторением блоков и общими весами без опоры
на цвет.
Проследите в таблице позицию токена . Сначала выбирается строка
Два разных блока последовательно дают
и
После итогового RMSNorm получается
а транспонированное представление общей таблицы переводит эту строку в пять логитов, показанных выше. Эти строки, предсказания, средняя потеря и проверка общего градиента относятся к одному прямому и обратному проходу; ни одно из значений не было оценено отдельно ради иллюстрации.
Проверьте оси, владение, каузальность и ошибки
- Для , , и укажите форму после выбора эмбеддингов, каждого блока, итогового RMSNorm и общей проекции.
- Для , , и вычислите число параметров модели с общими весами. Сколько скаляров добавит отдельная выходная проекция на словарь?
- Назовите первое, десятое и последнее имена параметров двухблочной модели.
Почему среди них нет
lm_head.weight? - Нарисуйте оба пути ленты вычислений к
token_embedding.weight. Какие строки может обновить применение для выбора строк, а какие — роль классификатора? - Измените только последний токен во входе длины три. Какие логиты должны остаться побитово неизменными?
- Распределите ошибки по категориям: ноль голов, длина токенов больше ёмкости, ID токена и одна отсутствующая цель.
Проверить структурные ответы
- После выбора эмбеддингов, каждого блока и итогового RMSNorm форма равна . После общей проекции форма равна .
- Число параметров модели с общими весами равно . Отдельная проекция добавит скаляров.
- Первое имя —
token_embedding.weight, десятое —blocks.0.ffn.down.weight, последнее —final_norm.gain. Транспонированное представление первого листа служит выходной проекцией, поэтому отдельного параметра для неё нет. - Выбор эмбеддингов напрямую затрагивает строки, заданные входными ID. Выходная роль может внести вклад в каждую строку словаря, потому что каждая строка получает логит. Обратный режим складывает оба вклада в одной таблице.
- Строки логитов и останутся побитово неизменными, а строка может измениться. Каузальная маска в каждом повторяющемся блоке сохраняет эту границу префикса.
- Ноль голов — ошибка конфигурации, слишком длинная последовательность — ошибка контекста, ID — ошибка входного словаря, а отсутствующая цель — ошибка формы целей.
Заблуждение: связывание весов копирует матрицу эмбеддингов в выходную проекцию. Исправление: существует один лист таблицы и два его дифференцируемых применения. Отдельная матрица с теми же начальными значениями имела бы собственную идентичность, принадлежность параметров и градиенты.
На следующем шаге обучите именно эту модель
Накопленная реализация теперь получает из ID токенов дифференцируемые логиты следующего токена и среднее значение индексированной функции потерь. В главе 33 эта же модель будет обучена в ограниченном детерминированном цикле, а её состояние будет выбрано только по потерям на валидационной выборке.
Эта глава заканчивается до оптимизации. Она охватывает архитектуру, идентичность параметров, логиты, индексированную функцию потерь, каузальность и градиенты, но не ограничивает градиенты, не применяет AdamW, не обнуляет градиенты между шагами, не оценивает валидационные данные, не выбирает контрольную точку, не просматривает тестовые данные, не генерирует токены и не управляет KV-кэшем. Эти обязанности останутся явными, когда в главе 33 появится цикл обучения.