← Все главы

32 · Версия материала 4

Соберите декодер и свяжите веса проекции на словарь

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

Предскажите оси до трассировки декодера

Соберём из выбора эмбеддингов по ID токенов, повторяющихся блоков декодера с предварительной нормализацией, итогового RMSNorm и одной действительно общей проекции на словарь дифференцируемые логиты. В примере ID токенов z=[0,1,2]z=[0,1,2] проходят через декодер из двух блоков. Размер словаря V=5V=5, ширина модели dmodel=4d_{\mathrm{model}}=4, число голов внимания равно двум, ширина сети прямого распространения dff=4d_{\mathrm{ff}}=4, а ёмкость контекста равна 44. Целевые ID: y=[1,2,3]y=[1,2,3].

До запуска предскажите пять структурных результатов:

  1. выбор эмбеддингов вернёт форму [1,3,4][1,3,4];
  2. оба блока декодера и итоговый RMSNorm сохранят форму [1,3,4][1,3,4];
  3. проекция на словарь вернёт форму [1,3,5][1,3,5];
  4. token_embedding.weight встретится в списке параметров один раз, хотя в ленте вычислений к нему ведут два пути;
  5. изменение только токена в позиции 22 не сможет изменить строки логитов 00 и 11.

Детерминированный пример даёт и численные значения. В позиции токена 11 строка логитов равна

[0.862249, 0.967613, 0.991545, 0.446363, 1.234533].[-0.862249,\ 0.967613,\ -0.991545,\ -0.446363,\ 1.234533].

Наибольшее значение соответствует ID словаря 44. Среднее индексированное отрицательное логарифмическое правдоподобие по трём целевым позициям равно 2.0455352.045535. Это свидетельство работы маленькой необученной модели, а не свидетельство того, что она уже выучила язык.

Повторно используйте таблицу эмбеддингов в конце стека

Полный прямой проход задаётся формулой

=RMSNorm(BN(B1(E[z])))E\ell=\operatorname{RMSNorm}(B_N(\cdots B_1(E[z])\cdots))E^\top

Эта запись относится к положительной глубине N1N\geq 1. На допустимой границе с нулём блоков пустая композиция блоков является тождественным преобразованием, поэтому формула сокращается до

=RMSNorm(E[z])E.\ell=\operatorname{RMSNorm}(E[z])E^\top.

Селекторы токенов удовлетворяют условию z{0,,V1}B×Tz\in\{0,\ldots,V-1\}^{B\times T}. Выбор строк из таблицы EV×dmodelE\in\mathbb{R}^{V\times d_{\mathrm{model}}} создаёт остаточный поток из B×T×dmodel\mathbb{R}^{B\times T\times d_{\mathrm{model}}}. Каждый BiB_i сохраняет эту форму. Итоговый RMSNorm\operatorname{RMSNorm} также её сохраняет. Умножение на EE^\top меняет только последнюю ось, поэтому

B×T×V.\ell\in\mathbb{R}^{B\times T\times V}.

Здесь \ell — тензор логитов, а не скалярное значение функции потерь. Для целевых ID yy пример вычисляет

=1BTb=1Bt=1Tlogsoftmax(b,t,:)yb,t.\mathcal{L}=-\frac{1}{BT}\sum_{b=1}^{B}\sum_{t=1}^{T} \log\operatorname{softmax}(\ell_{b,t,:})_{y_{b,t}}.

Если компоненты не используют смещения, один блок содержит 4dmodel2+3dmodeldff+2dmodel4d_{\mathrm{model}}^2+3d_{\mathrm{model}}d_{\mathrm{ff}}+2d_{\mathrm{model}} скаляров. Во всей модели с общими весами их число равно

Vdmodel+N(4dmodel2+3dmodeldff+2dmodel)+dmodel.Vd_{\mathrm{model}}+N\left(4d_{\mathrm{model}}^2 +3d_{\mathrm{model}}d_{\mathrm{ff}}+2d_{\mathrm{model}}\right) +d_{\mathrm{model}}.

В нашем примере получается 20+2(64+48+8)+4=26420+2(64+48+8)+4=264. Отдельная матрица проекции на словарь добавила бы Vdmodel=20Vd_{\mathrm{model}}=20 скаляров и увеличила итог до 284284.

Различайте один параметр и два его применения

  • zz — тензор ранга два с пакетами целочисленных ID токенов.
  • EE — одна обучаемая таблица «словарь на признаки».
  • E[z]E[z] выбирает из этой таблицы строки по ID токенов для входного применения.
  • BiB_iii-й каузальный блок декодера с предварительной нормализацией. Одинаковая конфигурация не означает, что блоки используют общие параметры.
  • NN — глубина декодера. Эта реализация принимает ноль, один и большее число блоков.
  • Итоговый RMSNorm\operatorname{RMSNorm} имеет собственный коэффициент, отдельный от коэффициентов всех блоков.
  • EE^\top — дифференцируемое транспонированное представление EE, а не ещё один параметр.
  • BB — размер пакета, TT — длина последовательности, а VV — размер словаря.
  • dmodeld_{\mathrm{model}} — ширина остаточного потока, а dffd_{\mathrm{ff}} — скрытая ширина каждой ветви SwiGLU.
  • yy содержит по одному целевому ID словаря для каждой пары (b,t)(b,t).
  • \ell содержит логиты словаря, а \mathcal{L} — скалярное среднее значение функции потерь.

Связывание весов — это не просто одинаковая инициализация. В модели нет ни lm_head.weight, ни скопированной матрицы, ни шага синхронизации. Обратный режим достигает token_embedding.weight один раз через выбранные входные строки и ещё раз через выходную проекцию, а затем складывает оба вклада в одном листе:

Eˉ=Eˉlookup+Eˉoutput.\bar E=\bar E_{\mathrm{lookup}}+\bar E_{\mathrm{output}}.

От раздельных рекуррентных компонентов к единому стеку декодера

Сначала рекуррентные языковые модели использовали раздельные входную и выходную таблицы. Затем появились общие веса словарных преобразований и повторяющиеся каузальные слои 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 нормализация расположена на входе каждого подблока и ещё раз перед проекцией на словарь. Поэтому граница модели сменилась: вместо эмбеддинга токена, пошагово обновляемого рекуррентного состояния и отдельного классификатора появились повторяющиеся блоки с каузальной маской, итоговые скрытые состояния которых нормализуются и проецируются в логиты словаря. Связывание весов уменьшает число параметров, но требует складывать в одном листе оба градиентных вклада.

Исполняемый пример сравнивает модель с общей таблицей и модель с отдельным классификатором словаря. Модель с общими весами содержит 264264 скаляра, а такая же модель с отдельным классификатором словаря формы [5,4][5,4] содержала бы 284284. Если по очереди отсоединить каждое из двух применений, можно проверить, что полный градиент таблицы равен сумме вкладов от выбора строк и от выходной проекции. Это выделяет следствие совместного использования параметра, но не выдаёт маленький пример за рекуррентную модель-предшественницу, 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 проверяет все настройки архитектуры даже при N=0N=0. Поэтому пустой стек не может скрыть неверное расположение голов, пустой словарь, недостаточную ёмкость контекста, неконечное основание RoPE или недопустимый эпсилон RMSNorm.

Проверить словарь, размерности, глубину, контекст, RoPE и нормализацию до сборки модели 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. В примере с двумя блоками есть 2020 тензоров и 264264 скаляра. Отдельного параметра выходной проекции намеренно нет. Инициализация использует временную копию состояния генератора псевдослучайных чисел и меняет состояние генератора вызывающего кода только после того, как проверены все компоненты и связи между ними.

Стабильный порядок также задаёт единую схему списка параметров декодера. Прежде чем from_parameters свяжет готовый список параметров с компонентами модели, validate_parameter_layout проверяет конфигурацию, точное число тензоров, имя тензора в каждой позиции списка и форму, которую ожидает соответствующий компонент. В позиции с индексом ноль находится единственная запись token_embedding.weight, используемая и для эмбеддингов, и для выходной проекции; отдельной записи для выходной проекции нет.

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

forward принимает только непустые тензоры ID токенов ранга два, где Tmax_positionsT\leq\mathrm{max\_positions}. Метод выбирает строки таблицы, вызывает каждый блок со смещением позиции ноль, применяет итоговый RMSNorm, транспонирует исходный лист таблицы и сразу умножает результат, получая логиты. Метод loss сначала проверяет по одной допустимой цели для каждого токена, а затем применяет среднее индексированное отрицательное логарифмическое правдоподобие по оси словаря 22.

Проверить стабильные имена и формы, затем собрать выбор эмбеддингов, блоки, итоговый RMSNorm и общую выходную проекцию 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(&current, 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(&current)
            .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()
    }
}

Численная проверка сравнивает все 2020 координат общей таблицы и все 44 координаты итогового коэффициента с центральными конечными разностями. Её допуск равен

τ=2×105.\tau=2\times10^{-5}.

Полная функция потерь двухблочной модели также создаёт конечные градиенты для всех 2020 зарегистрированных тензоров параметров.

Численно проверить общую таблицу и итоговый коэффициент, затем просмотреть градиенты всех параметров стека 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,
    })
}
Сформировать точный отчёт модели-декодера, оставив оптимизацию для главы 33 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,
    )
}

Исполняемый файл печатает только зафиксированный отчёт:

Напечатать зафиксированный учебный отчёт модели-декодера из главы 32 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 токенов
z=[0,1,2]z=[0,1,2] [1,3][1,3]
Выбор эмбеддингов
E[z]E[z] [1,3,4][1,3,4]
Два разных блока декодера

B1B_1 B2B_2

blocks.0, blocks.1 [1,3,4][1,3,4]
Итоговый RMSNorm
RMSNorm\operatorname{RMSNorm} [1,3,4][1,3,4]
Общая проекция EE^\top
Логиты словаря
\ell [1,3,5][1,3,5]
Один параметр используется дважды
token_embedding.weight

Выбор строк по ID: E[z]E[z] Роль транспонированной проекции: EE^\top

Проверьте строки скрытых признаков и все логиты

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

Прокручиваемая таблица строк признаков на этапах декодера
Этап Позиция токена Форма Точная строка признаков
Выбор эмбеддинга t=1t=1 [1,3,4][1,3,4] [0.104430,[0.104430,0.699156,0.699156,0.051971,0.051971,0.160217]-0.160217]
После блока декодера 1 t=1t=1 [1,3,4][1,3,4] [0.772509,[-0.772509,0.841246,0.841246,1.816030,1.816030,0.156071]0.156071]
После блока декодера 2 t=1t=1 [1,3,4][1,3,4] [0.183854,[-0.183854,0.605829,0.605829,0.709748,0.709748,0.521934]-0.521934]
После итогового RMSNorm t=1t=1 [1,3,4][1,3,4] [0.338936,[-0.338936,1.116846,1.116846,1.308420,1.308420,0.962186]-0.962186]
Прокручиваемая таблица логитов словаря
Позиция токена v=0v=0v=1v=1v=2v=2v=3v=3v=4v=4 Предсказание Цель
t=0t=0 0.3861150.3861150.2760910.2760910.3012660.3012660.460642-0.4606420.735173-0.735173 argmax=0\operatorname*{argmax}=0 y=1y=1
t=1t=1 0.862249-0.8622490.9676130.9676130.991545-0.9915450.446363-0.4463631.2345331.234533 argmax=4\operatorname*{argmax}=4 y=2y=2
t=2t=2 0.220241-0.2202410.3329210.3329210.193218-0.1932180.267554-0.2675540.500487-0.500487 argmax=1\operatorname*{argmax}=1 y=3y=3

Среднее значение индексированной функции потерь: =2.045535\mathcal{L}=2.045535

Проверьте владение, общие градиенты, каузальность и границы глубины

Число параметров, разложение по двум ролям, неизменность префикса, допустимые глубины и именованные ошибки входов получены на одном детерминированном примере.

Единый стабильный список параметров

Nθ=264N_\theta=264

Сэкономлено скаляров: 284264=20284-264=20

tensors=20, bias_free=true
Градиентные вклады складываются в одном листовом узле

Eˉ=Eˉlookup+Eˉoutput\bar E=\bar E_{\mathrm{lookup}}+\bar E_{\mathrm{output}}

Δ=0.000000000000\Delta=0.000000000000

checked=24, stack=20/20
Изменение только суффикса

t=0t=0: Побитово без изменений

t=1t=1: Побитово без изменений

t=2t=2: Численно изменилось

Проверки глубины и границ

N{0,1,2}N\in\{0,1,2\}: Двойное подчёркивание: проверено вычислениями Rust

configuration=true context=true vocabulary=true targets=true

На схеме есть одна карточка token_embedding.weight. В начале пути отношение выбора строк использует E[z]E[z], а после итоговой нормализации отношение проекции использует EE^\top. Одна карточка здесь принципиальна: две отдельные карточки матриц ошибочно означали бы два скопированных параметра. Двойные и пунктирные границы сохраняют различие между повторением блоков и общими весами без опоры на цвет.

Проследите в таблице позицию токена 11. Сначала выбирается строка

[0.104430, 0.699156, 0.051971, 0.160217].[0.104430,\ 0.699156,\ 0.051971,\ -0.160217].

Два разных блока последовательно дают

[0.772509, 0.841246, 1.816030, 0.156071][-0.772509,\ 0.841246,\ 1.816030,\ 0.156071]

и

[0.183854, 0.605829, 0.709748, 0.521934].[-0.183854,\ 0.605829,\ 0.709748,\ -0.521934].

После итогового RMSNorm получается

[0.338936, 1.116846, 1.308420, 0.962186],[-0.338936,\ 1.116846,\ 1.308420,\ -0.962186],

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

Проверьте оси, владение, каузальность и ошибки

  1. Для B=2B=2, T=6T=6, dmodel=12d_{\mathrm{model}}=12 и V=40V=40 укажите форму после выбора эмбеддингов, каждого блока, итогового RMSNorm и общей проекции.
  2. Для V=40V=40, dmodel=12d_{\mathrm{model}}=12, dff=32d_{\mathrm{ff}}=32 и N=3N=3 вычислите число параметров модели с общими весами. Сколько скаляров добавит отдельная выходная проекция на словарь?
  3. Назовите первое, десятое и последнее имена параметров двухблочной модели. Почему среди них нет lm_head.weight?
  4. Нарисуйте оба пути ленты вычислений к token_embedding.weight. Какие строки может обновить применение для выбора строк, а какие — роль классификатора?
  5. Измените только последний токен во входе длины три. Какие логиты должны остаться побитово неизменными?
  6. Распределите ошибки по категориям: ноль голов, длина токенов больше ёмкости, ID токена VV и одна отсутствующая цель.
Проверить структурные ответы
  1. После выбора эмбеддингов, каждого блока и итогового RMSNorm форма равна [2,6,12][2,6,12]. После общей проекции форма равна [2,6,40][2,6,40].
  2. Число параметров модели с общими весами равно 40(12)+3(4(12)2+3(12)(32)+2(12))+12=574840(12)+3(4(12)^2+3(12)(32)+2(12))+12=5748. Отдельная проекция добавит 40(12)=48040(12)=480 скаляров.
  3. Первое имя — token_embedding.weight, десятое — blocks.0.ffn.down.weight, последнее — final_norm.gain. Транспонированное представление первого листа служит выходной проекцией, поэтому отдельного параметра для неё нет.
  4. Выбор эмбеддингов напрямую затрагивает строки, заданные входными ID. Выходная роль может внести вклад в каждую строку словаря, потому что каждая строка получает логит. Обратный режим складывает оба вклада в одной таблице.
  5. Строки логитов 00 и 11 останутся побитово неизменными, а строка 22 может измениться. Каузальная маска в каждом повторяющемся блоке сохраняет эту границу префикса.
  6. Ноль голов — ошибка конфигурации, слишком длинная последовательность — ошибка контекста, ID VV — ошибка входного словаря, а отсутствующая цель — ошибка формы целей.

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

На следующем шаге обучите именно эту модель

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

Эта глава заканчивается до оптимизации. Она охватывает архитектуру, идентичность параметров, логиты, индексированную функцию потерь, каузальность и градиенты, но не ограничивает градиенты, не применяет AdamW, не обнуляет градиенты между шагами, не оценивает валидационные данные, не выбирает контрольную точку, не просматривает тестовые данные, не генерирует токены и не управляет KV-кэшем. Эти обязанности останутся явными, когда в главе 33 появится цикл обучения.