Chapter 06 · Content revision 4
Initialize residual projection weights for decoder depth
Scale each block's two output projection weight matrices once at initialization, then inspect residual activation RMS and completed mean-loss adjoints on fixed CPU decoders.
The problem: depth adds more residual contributions
Each complete decoder block adds an attention output to the residual state, then adds a feed-forward output. Increasing the block count repeats these additions. The existing Xavier initializer uses a matrix’s input and output widths to choose its scale; those widths can stay fixed while the number of residual additions grows. A suitable scale for one matrix therefore does not settle how activations and loss sensitivity behave throughout the stack.
We will reduce the initial coefficients of the two projections that write branch outputs into the residual state. This is a local initialization heuristic. We will also observe the resulting decoder before training, using the same occurrence-local contexts and valid-target mean loss as Chapter 5.
Start with four selected coefficients
Take an illustrative output-projection vector . For one complete block, keep it unchanged. For two blocks, multiply each coefficient once by . In their original order, the stored coefficients are approximately , , and . For eight blocks, the factor is , giving approximately , , and .
These decimal displays are rounded to six places; the Rust demo emits the
actual f64 results. The vector illustrates the transformation, rather than
estimating a distribution from four values. An unselected query-projection
vector remains unchanged at all three depths.
The selected matrices are blocks.{b}.attention.output.weight and
blocks.{b}.ffn.down.weight, where {b} is the zero-based block index. The
attention output projection writes the combined attention features back to
model width; the feed-forward down projection writes the SwiGLU features back
to that width. Query, key and value projections, gate and up projections,
embeddings and their tied vocabulary head, and all RMSNorm gains retain their
base values.
At fixed branch inputs, each selected projection is linear in its weights, so multiplying those weights by the factor multiplies that branch’s output by the factor. In a whole decoder, later branch inputs also change. The final decoder output is therefore not simply the unscaled output times one factor.
Apply the factor once, using complete blocks
Use a positive number of complete decoder blocks, , for the initialization rule:
Apply this rule to each of the two selected matrices in every block. A block has two residual additions, but the depth divisor remains the complete-block count . Eight blocks have additions and use , rather than .
The transformation happens before forward execution. Forward uses directly and adds no further factor. After initialization, these stored coefficients may evolve normally when a later chapter trains the model; they are not permanently constrained to remain a scaled base vector.
One block has factor . The implementation returns its base decoder directly, bypassing both transformation and reconstruction. It preserves parameter bits, names, shapes and ordering, random-number consumption, forward values, loss and parameter adjoints relative to the existing one-block constructor. The old constructor still admits zero-block models; this new policy requires positive depth because its factor is undefined at zero.
What the variance intuition assumes
Consider a toy sum of independent, zero-mean increments, each with variance . Multiplying each increment by gives
Here is one toy increment. Independence removes covariance terms, and equal variance gives the same contribution for each increment. This calculation motivates the depth factor. Real attention and feed-forward branches depend on shared residual states, and their Jacobians can be correlated. They do not satisfy this toy model in general, so the calculation is no proof of stable activations, gradients or training for the decoder.
Differentiate the coefficient actually stored
For a scalar product with input , write . Its derivative is
If instead we keep a base coordinate in the graph and define , the chain rule gives
Our decoder stores the initialized coefficient as the parameter. Its gradient is with respect to that stored coefficient, so it receives no second automatic depth factor. The two coordinate choices do not imply equal parameter updates under an optimizer, nor do these derivatives imply convergence.
What the initialization symbols mean
- counts complete decoder blocks and must be positive. Each block contains an attention addition followed by a feed-forward addition.
- is the dimensionless initialization factor .
- is one selected output-projection matrix produced by the existing Xavier initializer before this transformation.
- is that matrix after initialization. Forward and parameter differentiation use its stored values directly.
Construct fresh parameters and observe the existing decoder
DepthScalePolicy::new requires a positive complete-block count and computes
the factor. Role selection is applied only after the existing decoder validator
has checked the complete parameter names, order and shapes. This prevents a
plausible suffix match from standing in for a valid decoder layout.
rust/crates/llm-from-scratch-practical/src/nn/residual_scale.rs#depth-initialization-factor /// A checked factor for a positive number of complete decoder blocks.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DepthScalePolicy {
depth: usize,
factor: f64,
}
impl DepthScalePolicy {
pub fn new(depth: usize) -> Result<Self, DepthScaleError> {
if depth == 0 {
return Err(DepthScaleError::ZeroDepth);
}
let factor = 1.0 / (depth as f64).sqrt();
if !factor.is_finite() || factor <= 0.0 {
return Err(DepthScaleError::InvalidFactor { depth, factor });
}
Ok(Self { depth, factor })
}
pub const fn depth(self) -> usize {
self.depth
}
pub const fn factor(self) -> f64 {
self.factor
}
/// Called only after the decoder owner validates the entire parameter layout.
pub(crate) fn selects(self, name: &str) -> bool {
name.ends_with(".attention.output.weight") || name.ends_with(".ffn.down.weight")
}
} FreshDepthInitialization::prepare checks the configuration’s parameter count
and position capacity against the caller’s DepthInitializationBudget before
allocating model arrays. It clones the caller’s SplitMix64 state and calls the
existing DecoderModel::new once using that clone. Dropping the preparation
leaves the caller’s generator unchanged.
complete first refuses parameter updates or nonzero accumulated gradients.
This refusal precedes any weight transformation or RNG commit. Zero gradients
from construction, or from zero_grad when parameters have not been updated,
are allowed.
complete consumes this fresh state. For depths greater than one, it stages
the selected stored values and rebuilds through DecoderModel::from_parameters.
Only after successful assembly does it commit the trial random-generator state
to the caller. Refusal leaves the caller’s state unchanged. No separate decoder,
Xavier generator or forward multiplier is needed.
rust/crates/llm-from-scratch-practical/src/nn/depth_init.rs#selective-fresh-depth-initialization pub fn complete(self) -> Result<DepthInitializedDecoder, DepthInitError> {
for (parameter, &version) in self.base.parameters().iter().zip(&self.base_versions) {
if parameter.tensor().value_revision() != version
|| parameter
.tensor()
.gradient()
.is_some_and(|gradient| gradient.as_slice().iter().any(|&x| x != 0.0))
{
return Err(DepthInitError::ModifiedFreshParameter {
name: parameter.name().to_owned(),
});
}
}
let config = self.base.config();
let transformed_tensors = if self.policy.depth() == 1 {
0
} else {
2 * self.policy.depth()
};
let model = if self.policy.depth() == 1 {
// Preserve exact leaves, values and RNG consumption for the scalar case.
self.base
} else {
let staged = scaled_parameters(config, self.base.parameters(), self.policy)?;
DecoderModel::from_parameters(config, staged)?
};
let record = DepthInitializationRecord {
config,
factor: self.policy.factor(),
initial_rng_state: self.initial_rng_state,
final_rng_state: self.trial_rng.state(),
transformed_tensors,
};
*self.caller_rng = self.trial_rng;
Ok(DepthInitializedDecoder { model, record })
} The stored parameters continue through the existing block’s RMSNorm, attention, SwiGLU and two residual additions.
rust/crates/llm-from-scratch-practical/src/models/decoder_block.rs#decoder-block-layer /// Every inspectable value produced by the two pre-normalized residual paths.
#[derive(Clone, Debug)]
pub struct DecoderBlockForward {
attention_norm: RmsNormForward,
attention: MultiHeadAttentionForward,
after_attention: TensorValue,
feed_forward_norm: RmsNormForward,
feed_forward: SwiGluForward,
output: TensorValue,
}
impl DecoderBlockForward {
pub fn attention_norm(&self) -> &RmsNormForward {
&self.attention_norm
}
pub fn attention(&self) -> &MultiHeadAttentionForward {
&self.attention
}
pub fn attention_weights(&self) -> &TensorValue {
self.attention.attention_weights()
}
pub fn after_attention(&self) -> &TensorValue {
&self.after_attention
}
pub fn feed_forward_norm(&self) -> &RmsNormForward {
&self.feed_forward_norm
}
pub fn feed_forward(&self) -> &SwiGluForward {
&self.feed_forward
}
pub fn output(&self) -> &TensorValue {
&self.output
}
pub fn into_output(self) -> TensorValue {
self.output
}
}
/// RMSNorm → causal MHA → residual, then RMSNorm → SwiGLU → residual.
#[derive(Clone, Debug)]
pub struct DecoderBlock {
attention_norm: RmsNorm,
attention: MultiHeadAttention,
feed_forward_norm: RmsNorm,
feed_forward: SwiGlu,
parameters: NamedParameters,
model_width: usize,
}
impl DecoderBlock {
/// Initializes every matrix transactionally from one deterministic stream.
pub fn new(
parameter_prefix: impl Into<String>,
config: DecoderBlockConfig,
rng: &mut SplitMix64,
) -> Result<Self, DecoderBlockError> {
let parameter_prefix = parameter_prefix.into();
let attention_norm = RmsNorm::new(
format!("{parameter_prefix}.attention_norm.gain"),
config.model_width,
config.rms_epsilon,
)
.map_err(DecoderBlockError::AttentionNorm)?;
let feed_forward_norm = RmsNorm::new(
format!("{parameter_prefix}.ffn_norm.gain"),
config.model_width,
config.rms_epsilon,
)
.map_err(DecoderBlockError::FeedForwardNorm)?;
let mut trial = rng.clone();
let attention = MultiHeadAttention::new_with_attention(
format!("{parameter_prefix}.attention"),
config.model_width,
config.heads,
config.kv_heads,
config.max_positions,
config.rope_base,
config.context_window,
&mut trial,
)
.map_err(DecoderBlockError::Attention)?;
let feed_forward = SwiGlu::new(
format!("{parameter_prefix}.ffn"),
config.model_width,
config.feed_forward_width,
config.model_width,
&mut trial,
)
.map_err(DecoderBlockError::FeedForward)?;
let block = Self::from_parts(attention_norm, attention, feed_forward_norm, feed_forward)?;
*rng = trial;
Ok(block)
}
/// Assembles already named deterministic components after cross-width checks.
pub fn from_parts(
attention_norm: RmsNorm,
attention: MultiHeadAttention,
feed_forward_norm: RmsNorm,
feed_forward: SwiGlu,
) -> Result<Self, DecoderBlockError> {
let model_width = attention.model_width();
for (component, actual) in [
(
DecoderBlockComponent::AttentionNorm,
attention_norm.feature_width(),
),
(
DecoderBlockComponent::FeedForwardNorm,
feed_forward_norm.feature_width(),
),
(
DecoderBlockComponent::FeedForwardInput,
feed_forward.input_width(),
),
(
DecoderBlockComponent::FeedForwardOutput,
feed_forward.output_width(),
),
] {
if actual != model_width {
return Err(DecoderBlockError::ComponentWidthMismatch {
component,
expected: model_width,
actual,
});
}
}
let mut listed = Vec::with_capacity(9);
listed.extend(attention_norm.parameters().iter().cloned());
listed.extend(attention.parameters().iter().cloned());
listed.extend(feed_forward_norm.parameters().iter().cloned());
listed.extend(feed_forward.parameters().iter().cloned());
let parameters = NamedParameters::try_new(listed)?;
Ok(Self {
attention_norm,
attention,
feed_forward_norm,
feed_forward,
parameters,
model_width,
})
}
/// Runs the two transformation branches in exact pre-normalized order.
pub fn forward(
&self,
input: &TensorValue,
position_offset: usize,
) -> Result<DecoderBlockForward, DecoderBlockError> {
self.forward_with_context(AutogradContext::recording(), input, position_offset)
}
/// Runs both pre-normalized branches under an explicit recording policy.
pub fn forward_with_context(
&self,
context: AutogradContext,
input: &TensorValue,
position_offset: usize,
) -> Result<DecoderBlockForward, DecoderBlockError> {
self.forward_with_optional_lengths(context, input, position_offset, None)
}
pub(crate) fn forward_with_optional_lengths(
&self,
context: AutogradContext,
input: &TensorValue,
position_offset: usize,
lengths: Option<&[usize]>,
) -> Result<DecoderBlockForward, DecoderBlockError> {
self.forward_with_layout(context, input, position_offset, lengths, None, None)
}
pub(crate) fn forward_with_layout(
&self,
context: AutogradContext,
input: &TensorValue,
position_offset: usize,
lengths: Option<&[usize]>,
starts: Option<&[usize]>,
positions: Option<&[usize]>,
) -> Result<DecoderBlockForward, DecoderBlockError> {
self.forward_with_layout_and_dropout(
context,
input,
position_offset,
lengths,
starts,
positions,
0,
&mut DropoutExecution::Identity,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn forward_with_layout_and_dropout(
&self,
context: AutogradContext,
input: &TensorValue,
position_offset: usize,
lengths: Option<&[usize]>,
starts: Option<&[usize]>,
positions: Option<&[usize]>,
layer: usize,
dropout: &mut DropoutExecution<'_>,
) -> Result<DecoderBlockForward, DecoderBlockError> {
let attention_norm = self
.attention_norm
.forward_with_intermediates_and_context(context, input)
.map_err(DecoderBlockError::AttentionNorm)?;
let attention = self
.attention
.forward_with_layout(
context,
attention_norm.output(),
position_offset,
lengths,
starts,
positions,
)
.map_err(DecoderBlockError::Attention)?;
let (after_attention, feed_forward_norm, feed_forward, output) =
self.finish_attention_residual(context, input, attention.output(), layer, dropout)?;
Ok(DecoderBlockForward {
attention_norm,
attention,
after_attention,
feed_forward_norm,
feed_forward,
output,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn forward_output_with_layout_and_dropout(
&self,
context: AutogradContext,
input: &TensorValue,
position_offset: usize,
lengths: Option<&[usize]>,
starts: Option<&[usize]>,
positions: Option<&[usize]>,
layer: usize,
dropout: &mut DropoutExecution<'_>,
) -> Result<TensorValue, DecoderBlockError> {
if context.execution().attention_tiles().is_none() {
return self
.forward_with_layout_and_dropout(
context,
input,
position_offset,
lengths,
starts,
positions,
layer,
dropout,
)
.map(DecoderBlockForward::into_output);
}
let normalized = self
.attention_norm
.forward_with_context(context, input)
.map_err(DecoderBlockError::AttentionNorm)?;
let attended = self
.attention
.forward_output_with_layout(
context,
&normalized,
position_offset,
lengths,
starts,
positions,
)
.map_err(DecoderBlockError::Attention)?;
self.finish_attention_residual(context, input, &attended, layer, dropout)
.map(|(_, _, _, output)| output)
}
fn finish_attention_residual(
&self,
context: AutogradContext,
input: &TensorValue,
attention_output: &TensorValue,
layer: usize,
dropout: &mut DropoutExecution<'_>,
) -> Result<(TensorValue, RmsNormForward, SwiGluForward, TensorValue), DecoderBlockError> {
let branch = dropout
.apply(
context,
attention_output,
DropoutSiteId {
layer,
kind: DropoutSiteKind::AttentionOutput,
},
)
.map_err(DecoderBlockError::Dropout)?;
let after_attention = residual_add_with_context(context, input, &branch)
.map_err(DecoderBlockError::AttentionResidual)?;
let (feed_forward_norm, feed_forward, output) =
self.forward_feed_forward_path_and_dropout(context, &after_attention, layer, dropout)?;
Ok((after_attention, feed_forward_norm, feed_forward, output))
}
/// One FFN residual path, reused by full blocks and residual-site diagnostics.
pub(crate) fn forward_feed_forward_path(
&self,
context: AutogradContext,
after_attention: &TensorValue,
) -> Result<(RmsNormForward, SwiGluForward, TensorValue), DecoderBlockError> {
self.forward_feed_forward_path_and_dropout(
context,
after_attention,
0,
&mut DropoutExecution::Identity,
)
}
pub(crate) fn forward_feed_forward_path_and_dropout(
&self,
context: AutogradContext,
after_attention: &TensorValue,
layer: usize,
dropout: &mut DropoutExecution<'_>,
) -> Result<(RmsNormForward, SwiGluForward, TensorValue), DecoderBlockError> {
let feed_forward_norm = self
.feed_forward_norm
.forward_with_intermediates_and_context(context, after_attention)
.map_err(DecoderBlockError::FeedForwardNorm)?;
let feed_forward = self
.feed_forward
.forward_with_intermediates_and_context(context, feed_forward_norm.output())
.map_err(DecoderBlockError::FeedForward)?;
let branch = dropout
.apply(
context,
feed_forward.output(),
DropoutSiteId {
layer,
kind: DropoutSiteKind::FeedForwardOutput,
},
)
.map_err(DecoderBlockError::Dropout)?;
let output = residual_add_with_context(context, after_attention, &branch)
.map_err(DecoderBlockError::FeedForwardResidual)?;
Ok((feed_forward_norm, feed_forward, output))
}
pub fn attention_norm(&self) -> &RmsNorm {
&self.attention_norm
}
pub fn attention(&self) -> &MultiHeadAttention {
&self.attention
}
pub fn feed_forward_norm(&self) -> &RmsNorm {
&self.feed_forward_norm
}
pub fn feed_forward(&self) -> &SwiGlu {
&self.feed_forward
}
pub fn parameters(&self) -> &[NamedParameter] {
self.parameters.as_slice()
}
pub const fn model_width(&self) -> usize {
self.model_width
}
pub fn parameter_count(&self) -> usize {
self.parameters
.as_slice()
.iter()
.map(|parameter| parameter.tensor().value().len())
.sum()
}
} initialize_depth_scaled distinguishes Fresh, Initialized and Restored
sources. Only Fresh is accepted; the other states are refused before random
draws or mutation. Applying initialization again would change coefficients that
already carry their initialization or training history.
Restoration uses DepthInitializedDecoder::from_initialized_parameters with
the typed construction record and the actual stored parameters. It restores
those supplied values exactly and never rescales them. The record identifies
the configuration, policy, base initializer and construction RNG states. It
does not supply a trained-checkpoint wire format or optimizer state.
rust/crates/llm-from-scratch-practical/src/models/decoder.rs#decoder-parameter-rebuild /// Rebuilds every component handle from one exact stable-order parameter set.
///
/// State restoration uses this construction boundary to create an isolated
/// decoder. Ordinary optimizer steps instead update the existing leaves, so
/// the registry, components, and tied embedding keep their live aliases.
pub fn from_parameters(
config: DecoderModelConfig,
parameters: Vec<NamedParameter>,
) -> Result<Self, DecoderModelError> {
validate_parameter_layout(config, parameters.as_slice())?;
let embedding = Embedding::from_parameter(parameters[0].clone())
.map_err(DecoderModelError::Embedding)?;
let (blocks, final_norm) =
assemble_backbone(config.block_config(), config.layers, ¶meters[1..])?;
Self::from_parts(config, embedding, blocks, final_norm)
} Measure residual magnitude and completed loss sensitivity
For residual values at valid token positions with features each, define root mean square as
is the set of valid positions and has size ; indexes features. RMS measures magnitude about zero. It does not subtract a mean and is not centered variance. RMSNorm also does not center activations or establish unit variance.
Record the input residual as site , then the state after each attention addition and each feed-forward addition in forward order. Adjacent blocks share a boundary, which is counted once. There are sites, ending at site before final model RMSNorm and the tied vocabulary head.
Let be activation RMS at site . Let be RMS of the fully accumulated derivative of the existing valid-target mean cross entropy with respect to that site’s residual features. The mean loss uses natural logarithms and is reported in nats per valid target. Its derivatives describe sensitivity of that same objective; they are not parameter-gradient norms or partial contributions from one backward edge.
Use two different normalizers:
and are dimensionless ratios. Activations are compared with the input magnitude. Adjoint magnitudes are compared with the terminal residual adjoint, before the final RMSNorm and tied head; those later operations still contribute to that terminal derivative. The normalization fixes and . It does not fix the absolute magnitudes of either normalizer.
Both normalizers must be positive and finite. Zero or nonfinite values are errors; the observer does not add an epsilon to make a ratio reportable. It includes only valid-position features, skipping padding before arithmetic, and uses an overflow-safe RMS reduction. Multiplying a padded NaN by zero would not exclude it safely. Empty validity, nonfinite valid values and missing completed adjoints are also errors.
The observer selects residual handles by exact node identity after the existing reverse pass has accumulated all their contributions. RMS calculations take place outside the differentiation graph. Observation adds no graph operations, random draws or changes to weight values; the usual mean-loss reverse pass still produces the decoder’s parameter gradients.
rust/crates/llm-from-scratch-practical/src/autograd/tensor_core.rs#completed-selected-adjoints /// Uses the existing reverse pass and copies completed adjoints in request order.
///
/// Missing or untracked nodes yield `None`. Node identity, rather than a
/// matching shape or operation name, binds each observation. Parameter
/// gradients have exactly the ordinary reverse pass's accumulation semantics.
pub fn backward_with_selected_adjoints(
&self,
selected: &[TensorValue],
) -> Result<Vec<Option<Tensor>>, TensorAutodiffError> {
self.backward_scalar(RecordSelectedAdjoints { selected })
} rust/crates/llm-from-scratch-practical/src/nn/depth_health.rs#completed-residual-health pub fn observe_residual_health(
model: &DecoderModel,
forward: &DecoderModelForward,
mean_loss: &TensorValue,
valid: &[bool],
) -> Result<DepthHealthReport, DepthHealthError> {
let sites = residual_sites(forward);
if forward.blocks().len() != model.config().layers()
|| forward.embedding().shape().last().copied() != Some(model.config().model_width())
{
return Err(DepthHealthError::ModelForwardMismatch);
}
let activations = sites
.iter()
.map(|site| valid_feature_rms(&site.value(), valid))
.collect::<Result<Vec<_>, _>>()?;
let adjoints = mean_loss.backward_with_selected_adjoints(&sites)?;
let gradients = adjoints
.iter()
.enumerate()
.map(|(site, adjoint)| {
let adjoint = adjoint
.as_ref()
.ok_or(DepthHealthError::MissingAdjoint { site })?;
if adjoint.shape() != sites[site].shape() {
return Err(DepthHealthError::InvalidGeometry);
}
valid_feature_rms(adjoint, valid)
})
.collect::<Result<Vec<_>, _>>()?;
let input_activation_normalizer = activations[0];
let terminal_adjoint_normalizer = *gradients.last().ok_or(DepthHealthError::InvalidGeometry)?;
if input_activation_normalizer <= 0.0
|| terminal_adjoint_normalizer <= 0.0
|| !input_activation_normalizer.is_finite()
|| !terminal_adjoint_normalizer.is_finite()
{
return Err(DepthHealthError::InvalidNormalizer);
}
let rows = activations
.iter()
.zip(&gradients)
.enumerate()
.map(|(site, (&a, &g))| {
let activation_ratio = a / input_activation_normalizer;
let adjoint_ratio = g / terminal_adjoint_normalizer;
let label = if site == 0 {
"input".to_owned()
} else if site % 2 == 1 {
format!("block.{}.after_attention", site / 2)
} else {
format!("block.{}.after_feed_forward", site / 2 - 1)
};
Ok(ResidualHealthRow {
site,
label,
activation_rms: a,
adjoint_rms: g,
activation_ratio,
adjoint_ratio,
activation_status: classify_ratio(activation_ratio)?,
adjoint_status: classify_ratio(adjoint_ratio)?,
})
})
.collect::<Result<Vec<_>, DepthHealthError>>()?;
let mut branches = Vec::with_capacity(forward.blocks().len());
for (block, (values, owner)) in forward.blocks().iter().zip(model.blocks()).enumerate() {
branches.push(BranchHealthRow {
block,
attention_output_rms: valid_feature_rms(&values.attention().output().value(), valid)?,
feed_forward_output_rms: valid_feature_rms(
&values.feed_forward().output().value(),
valid,
)?,
attention_norm_gain: owner
.attention_norm()
.gain()
.tensor()
.value()
.as_slice()
.to_vec(),
feed_forward_norm_gain: owner
.feed_forward_norm()
.gain()
.tensor()
.value()
.as_slice()
.to_vec(),
});
}
Ok(DepthHealthReport {
valid_positions: valid.iter().filter(|&&v| v).count(),
width: model.config().model_width(),
input_activation_normalizer,
terminal_adjoint_normalizer,
sites: rows,
branches,
final_norm_gain: model
.final_norm()
.gain()
.tensor()
.value()
.as_slice()
.to_vec(),
})
} Classify each finite, unrounded ratio in the inclusive band . This course-local band catches large relative changes in either direction. It does not establish convergence or adequate absolute gradients. Branch-output RMS and norm-gain vectors are reported separately; they are outside this residual-site classification.
Run the fixed CPU example
With Docker running and the repository’s Rust workspace setup available, run from the repository root:
COURSE_CORPUS=false ./course run cargo run --offline --locked -p practical-ch06-depth-stable-decoder
The example uses supplied IDs and needs no corpus or model download. Seed
4701 initializes each fixture. Source [0,99,100,101,1] gives inputs
[0,99,100,101] and targets [99,100,101,1], with positions [0,1,2,3],
all valid in one segment. In layout-v1, 0 is BOS and 1 is EOS. These are
controlled valid-ID examples, not a claim about trained tokenizer segmentation
or a real corpus. RoPE base is and RMSNorm epsilon is .
The JSON output contains worked_values, four fixtures,
synthetic_classifiers and a symbolic laptop_plan. Each fixture reports
initialized and same_base_unscaled_control, their residual sites and
within_band: true. It measures before any optimizer update.
rust/demos/practical-ch06-depth-stable-decoder/src/main.rs#depth-stable-example fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(
"{}",
serde_json::to_string_pretty(&practical_ch06_depth_stable_decoder::demo_trace()?)?
);
Ok(())
} History: fan-based scale and depth-aware residual methods
Fan-in and fan-out reasoning sets a matrix’s initialization scale under simplified assumptions; it does not by itself account for repeated additions in this residual decoder.
Glorot and Bengio’s 2010 paper studies activation and backward-gradient variance near initialization.
Section 4.2 derives fan-in and fan-out variance conditions in an approximately linear regime with independently initialized weights and equal input-feature variances, and gives a compromise normalized initialization.
Fan-in counts the inputs to a matrix and fan-out counts its outputs. Their compromise initialization targets ideal weight variance .
For a matrix, the existing Xavier uniform initializer therefore has ideal half-bound and variance . Multiplying a selected matrix by multiplies its half-bound by and its ideal variance by .
| Complete blocks | Uniform half-bound | Weight variance |
|---|---|---|
These are ideal distribution statements. The empirical variance of a finite matrix need not equal them, and the four illustrative coefficients earlier are not an empirical check of these variances. The paper’s activation setting and simplified assumptions also differ from our residual RMSNorm/SwiGLU stack.
rust/crates/llm-from-scratch-practical/src/nn/init.rs#xavier-initialization /// The formula-derived scale for one [fan-in, fan-out] matrix.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct XavierScale {
fan_in: usize,
fan_out: usize,
target_variance: f64,
standard_deviation: f64,
uniform_limit: f64,
}
impl XavierScale {
pub const fn fan_in(self) -> usize {
self.fan_in
}
pub const fn fan_out(self) -> usize {
self.fan_out
}
pub const fn target_variance(self) -> f64 {
self.target_variance
}
pub const fn standard_deviation(self) -> f64 {
self.standard_deviation
}
pub const fn uniform_limit(self) -> f64 {
self.uniform_limit
}
}
/// Calculates the Xavier variance, standard deviation, and uniform bound.
pub fn xavier_scale(fan_in: usize, fan_out: usize) -> Result<XavierScale, InitializationError> {
if fan_in == 0 {
return Err(InitializationError::ZeroFanIn);
}
if fan_out == 0 {
return Err(InitializationError::ZeroFanOut);
}
let fan_sum = fan_in
.checked_add(fan_out)
.ok_or(InitializationError::FanSumOverflow { fan_in, fan_out })?;
let target_variance = 2.0 / fan_sum as f64;
Ok(XavierScale {
fan_in,
fan_out,
target_variance,
standard_deviation: target_variance.sqrt(),
uniform_limit: (6.0 / fan_sum as f64).sqrt(),
})
}
fn checked_element_count(fan_in: usize, fan_out: usize) -> Result<usize, InitializationError> {
fan_in
.checked_mul(fan_out)
.ok_or(InitializationError::ShapeProductOverflow { fan_in, fan_out })
}
pub(crate) fn validate_name(name: &str) -> Result<(), InitializationError> {
if name.is_empty() {
return Err(InitializationError::EmptyName);
}
let mut previous_was_dot = true;
for (index, byte) in name.bytes().enumerate() {
if byte == b'.' {
if previous_was_dot {
return Err(InitializationError::EmptyNameSegment { index });
}
previous_was_dot = true;
continue;
}
if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') {
return Err(InitializationError::InvalidNameCharacter { index, byte });
}
previous_was_dot = false;
}
if previous_was_dot {
return Err(InitializationError::EmptyNameSegment { index: name.len() });
}
Ok(())
}
fn initialized_values(
rng: &mut SplitMix64,
scale: XavierScale,
) -> Result<Vec<f64>, InitializationError> {
let elements = checked_element_count(scale.fan_in, scale.fan_out)?;
let mut values = Vec::new();
values
.try_reserve_exact(elements)
.map_err(|_| InitializationError::AllocationFailed { elements })?;
for _ in 0..elements {
let centered = 2.0 * rng.next_unit_f64() - 1.0;
values.push(scale.uniform_limit * centered);
}
Ok(values)
} Later work joined residual design and initialization choices. DeepNet (2022)
DeepNet introduces DeepNorm, which modifies the residual connection and accompanies it with theoretically derived initialization. The course’s selective initialization factor does not reproduce that method, its constants or its results.
Its method depends on that Transformer setting. Our selected factor also carries no DeepNorm stability theorem.
The runnable local contrast retains one actual Xavier-initialized base set. It copies that set into an unscaled control, then completes selective initialization from the same preparation. The comparison changes the chosen initialization policy without adding a second random sample. Neither model is a DeepNorm implementation.
rust/demos/practical-ch06-depth-stable-decoder/src/lib.rs#depth-same-base-control fn fixture(id: &str, profile: &str, config: DecoderModelConfig) -> Result<Value, Box<dyn Error>> {
let mut rng = SplitMix64::from_seed(SEED);
let fresh = FreshDepthInitialization::prepare(
config,
DepthInitializationBudget {
max_parameters: if profile == "reference-ci" {
1188
} else {
8304
},
max_positions: if profile == "reference-ci" { 4 } else { 16 },
},
&mut rng,
)?;
// Copy the one actual base set; no second initialization or random replay.
let base_parameters = fresh
.base_model()
.parameters()
.iter()
.map(|p| NamedParameter::from_tensor(p.name(), p.tensor().value_snapshot()))
.collect::<Result<Vec<_>, _>>()?;
let control = DecoderModel::from_parameters(config, base_parameters)?;
let initialized = fresh.complete()?;
let scaled = initialized.model();
let alpha = initialized.record().factor();
let mut selected = Vec::new();
let mut unchanged = 0;
for (base, actual) in control.parameters().iter().zip(scaled.parameters()) {
if base.name() != actual.name() || base.tensor().shape() != actual.tensor().shape() {
return Err("depth transform changed the parameter layout".into());
}
let is_selected = base.name().ends_with(".attention.output.weight")
|| base.name().ends_with(".ffn.down.weight");
for (&before, &after) in base
.tensor()
.value()
.as_slice()
.iter()
.zip(actual.tensor().value().as_slice())
{
let expected = if is_selected && config.layers() != 1 {
before * alpha
} else {
before
};
if expected.to_bits() != after.to_bits() {
return Err("selective stored-weight transform differed".into());
}
}
if is_selected {
selected.push(base.name().to_owned());
} else {
unchanged += 1;
}
}
let (scaled_loss, scaled_health) = health(scaled)?;
let (base_loss, base_health) = health(&control)?;
if !scaled_health.within_band() {
return Err(format!("{id}: fixed initialized fixture left the frozen health band").into());
}
Ok(
json!({"id": id, "profile": profile, "vocabulary": config.vocabulary_size(),
"width": config.model_width(), "heads": config.heads(), "ff_width": config.feed_forward_width(),
"layers": config.layers(), "max_positions": config.max_positions(),
"parameters": scaled.parameter_count(), "parameter_tensors": scaled.parameters().len(),
"factor": alpha, "policy_id": initialized.record().policy_id(),
"base_initializer_id": initialized.record().base_initializer_id(),
"initial_rng_state": initialized.record().initial_rng_state().to_string(),
"final_rng_state": initialized.record().final_rng_state().to_string(),
"transformed_tensors": initialized.record().transformed_tensors(),
"selected_names": selected, "unselected_tensors_unchanged": unchanged,
"initialized": report_json(scaled_loss, &scaled_health),
"same_base_unscaled_control": report_json(base_loss, &base_health)}),
)
} A decoder needs an explicit initialization policy as its block count grows, together with observations that distinguish residual magnitude from loss sensitivity.
Read the observations in residual-site order
The narrow fixtures all have width , one attention head, feed-forward width and position capacity . They use one, two or eight complete blocks. The CPU bridge fixture has width , heads, feed-forward width , blocks and position capacity . All use vocabulary size and the four valid positions given above. Query and key/value head counts are equal in these executed decoders.
| Decoder fixture | Parameter coordinates | Residual sites |
|---|---|---|
| Narrow, one block | ||
| Narrow, two blocks | ||
| Narrow, eight blocks | ||
| CPU bridge, two blocks |
Read site as the input, each odd site as the state after an attention addition, and each following even site as the state after a feed-forward addition. Activation ratios use the input activation RMS; adjoint ratios use that model’s terminal residual-adjoint RMS. Each model has its own normalizers, so comparing ratios is not a comparison of absolute derivative magnitudes.
The full trace places every initialized activation and adjoint ratio inside for these four fixtures. Every same-base unscaled control also lies inside the band. The tables show the maximum of each ratio over that model’s sites, with displays rounded to six decimal places. Classification uses the unrounded values.
| Decoder fixture | Selectively initialized | Same-base unscaled |
|---|---|---|
| Narrow, one block | ||
| Narrow, two blocks | ||
| Narrow, eight blocks | ||
| CPU bridge, two blocks |
| Decoder fixture | Selectively initialized | Same-base unscaled |
|---|---|---|
| Narrow, one block | ||
| Narrow, two blocks | ||
| Narrow, eight blocks | ||
| CPU bridge, two blocks |
In these fixed deeper fixtures, selective initialization lowers the displayed maximum activation and adjoint ratios compared with their same-base controls. One block follows the unchanged path. These observations say only what happened for these untrained models, supplied IDs and seed. Both policies pass this finite band; the results establish neither convergence nor absolute gradient quality, nor behavior at other seeds, depths, numeric types or backends.
Keep synthetic threshold examples separate
The example also passes synthetic arrays directly to classify_ratio.
Activation ratios have an upper failure at forward site .
Gradient ratios have a lower failure at site .
The boundary array is entirely within the inclusive band.
These arrays explain the classifier; they are not measurements from an
initialized decoder.
Initialized projection weights and observed residual states
The factor initializes only stored attention output and feed-forward down weights. The residual skip route enters each addition without a depth multiplier. Each fixture lists the input, then the state after every attention and feed-forward addition. Between neighboring blocks, the state after one block's feed-forward addition is reused as the next block's input; that shared residual state is listed once. Activation RMS divides by input activation RMS. Fully accumulated mean-loss adjoint RMS divides by terminal residual-adjoint RMS before final RMSNorm and the tied head. The inclusive band classifies unrounded ratios. All fixed initialized fixtures and their same-base unscaled controls are within the band. The separate synthetic arrays illustrate classifier thresholds; they are not decoder measurements.
Within each complete decoder block
The complete-block count of the whole decoder sets the factor. The base matrix comes from Xavier initialization; its transformed values are stored once before forward. The topology shows one block of the stack.
Zero-based block index:
-
Block input residual state
The residual skip route contributes its current state directly to each addition. The initialization policy applies no multiplier to that route.
-
Attention residual addition
The attention branch reads the normalized block input.
Attention branch output:
Initialize the stored attention output projection weights.
-
Feed-forward residual addition
The feed-forward branch reads the normalized state after the attention addition.
Feed-forward branch output:
Initialize the stored feed-forward down projection weights.
Query, key, value, gate and up weights, embeddings and their tied vocabulary head, and norm gains retain their base values. Forward uses the initialized stored weights directly, without a second factor.
Observed initialized decoder residual sites
These untrained decoder measurements use the four supplied valid positions and the existing valid-target mean cross entropy, in nats per valid target. Query and key/value head counts are equal in the executed models.
Initialization seed: 4701
Inclusive band for dimensionless residual ratios:
Activation RMS measures magnitude about zero over valid residual features. Each activation ratio divides by that model's input activation RMS and is dimensionless.
Adjoint RMS uses completed derivatives of the mean loss. Each adjoint ratio divides by that model's terminal residual-adjoint RMS before final RMSNorm and the tied head, whose downstream operations contribute to that derivative. Both normalizers must be positive and finite, without epsilon repair.
RMS, factor and ratio displays are rounded to six decimal places. Classification uses unrounded values, including both band endpoints.
Narrow decoder, one complete block
- Complete decoder blocks
- Residual features per position
- Equal query and key/value head count
- Feed-forward features
- Vocabulary entries
- Position capacity
- Stored parameter coordinates
- Initialization factor for stored weights
- Input activation RMS normalizer
- Terminal residual-adjoint RMS normalizer
All site ratios within band
| Residual site and stage | Absolute activation RMS | Activation ratio and band classification | Absolute completed mean-loss adjoint RMS | Adjoint ratio and band classification |
|---|---|---|---|---|
| Input residual | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band |
Narrow decoder, two complete blocks
- Complete decoder blocks
- Residual features per position
- Equal query and key/value head count
- Feed-forward features
- Vocabulary entries
- Position capacity
- Stored parameter coordinates
- Initialization factor for stored weights
- Input activation RMS normalizer
- Terminal residual-adjoint RMS normalizer
All site ratios within band
| Residual site and stage | Absolute activation RMS | Activation ratio and band classification | Absolute completed mean-loss adjoint RMS | Adjoint ratio and band classification |
|---|---|---|---|---|
| Input residual | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band |
Narrow decoder, eight complete blocks
- Complete decoder blocks
- Residual features per position
- Equal query and key/value head count
- Feed-forward features
- Vocabulary entries
- Position capacity
- Stored parameter coordinates
- Initialization factor for stored weights
- Input activation RMS normalizer
- Terminal residual-adjoint RMS normalizer
All site ratios within band
| Residual site and stage | Absolute activation RMS | Activation ratio and band classification | Absolute completed mean-loss adjoint RMS | Adjoint ratio and band classification |
|---|---|---|---|---|
| Input residual | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band |
CPU bridge decoder, two complete blocks
- Complete decoder blocks
- Residual features per position
- Equal query and key/value head count
- Feed-forward features
- Vocabulary entries
- Position capacity
- Stored parameter coordinates
- Initialization factor for stored weights
- Input activation RMS normalizer
- Terminal residual-adjoint RMS normalizer
All site ratios within band
| Residual site and stage | Absolute activation RMS | Activation ratio and band classification | Absolute completed mean-loss adjoint RMS | Adjoint ratio and band classification |
|---|---|---|---|---|
| Input residual | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band | ||
| Zero-based block index After attention addition | Within band | Within band | ||
| Zero-based block index After feed-forward addition | Within band | Within band |
The JSON also reports branch-output RMS and norm-gain vectors separately. Those diagnostics are outside the residual-site ratio classification.
Separate same-base unscaled controls
For each fixture, the unscaled control copies the one actual base parameter set before selected weights are transformed. No model is resampled. Each control uses its own normalizers. Every fixed control also lies within the residual ratio band; comparing ratios does not compare absolute adjoint magnitudes.
| Decoder fixture | Input activation RMS normalizer | Terminal residual-adjoint RMS normalizer | Activation ratio range | Adjoint ratio range | Ratio-band classification |
|---|---|---|---|---|---|
| Narrow decoder, one complete block | Minimum over sites: Maximum over sites: | Minimum over sites: Maximum over sites: | All site ratios within band | ||
| Narrow decoder, two complete blocks | Minimum over sites: Maximum over sites: | Minimum over sites: Maximum over sites: | All site ratios within band | ||
| Narrow decoder, eight complete blocks | Minimum over sites: Maximum over sites: | Minimum over sites: Maximum over sites: | All site ratios within band | ||
| CPU bridge decoder, two complete blocks | Minimum over sites: Maximum over sites: | Minimum over sites: Maximum over sites: | All site ratios within band |
Separate synthetic threshold examples
These arrays are supplied directly to the ratio classifier. They illustrate threshold decisions and are separate from initialized decoder observations. The band includes both endpoints; classification uses unrounded values.
| Synthetic forward-site index | Supplied dimensionless ratio | Ratio-band classification |
|---|---|---|
| Within band | ||
| Within band | ||
| Within band | ||
| Above band |
| Synthetic forward-site index | Supplied dimensionless ratio | Ratio-band classification |
|---|---|---|
| Below band | ||
| Within band | ||
| Within band | ||
| Within band |
| Synthetic forward-site index | Supplied dimensionless ratio | Ratio-band classification |
|---|---|---|
| Within band | ||
| Within band | ||
| Within band |
The laptop_plan is also separate from the executed fixtures. It contains
only symbolic arithmetic and metadata: vocabulary , width ,
blocks, query heads, key/value heads, feed-forward width
and context , giving parameters and factor .
It allocates no model arrays and supplies no seeded execution, measured health
or performance result. Unequal-head execution remains later work.
Optional practice: inspect initialization and its limits
- Use the fixed-example command above to inspect
worked_valuesat one, two and eight blocks. Explain the selected coefficients, unchanged query coefficients and ideal Xavier half-bounds and variances. - Inspect the eight-block fixture’s ordered sites. Explain why its factor uses eight blocks while its report has seventeen sites.
- Explain the stored-coordinate and base-coordinate derivatives for the scalar product with input two, and why forward needs no second factor.
- Inspect
synthetic_classifiers. Explain the upper, lower and inclusive outcomes, and distinguish them fromfixtures. - Inspect the initialized and same-base unscaled reports. Explain what the maximum-ratio comparisons establish, why both normalizers must be usable, and why the reported ratios do not establish convergence.
- Explain what happens when a fresh preparation is dropped, when its budget refuses construction, and when already initialized or restored parameters are supplied for fresh initialization. Distinguish restoration from the symbolic laptop entry.
Explanations for factors, site counts, derivatives, thresholds and construction states
- The factors are , and . Multiplying only the selected vector gives the coefficient values explained above; the query vector stays . For a selected matrix, the ideal half-bounds are , and , with variances , and . The variance changes by the square of the factor. These are distribution properties, not a finite-sample measurement.
- Eight complete blocks contain residual additions. The input plus each addition’s output gives sites; a block’s final state is the next block’s input and is counted once. The divisor remains .
- , while when the transformation is retained in the graph. Our parameter is the stored coefficient. Multiplying again in forward would change the intended initialized model.
- puts the first synthetic array’s site above the band. puts the second array’s site below it. Equality at or is allowed. The actual four initialized decoder reports lie within the band and are separate observations.
- The deeper fixed initialized fixtures have lower observed maximum ratios than their same-base controls; the one-block observations coincide. All controls also pass. Activation RMS divides by input RMS and adjoint RMS by terminal residual-adjoint RMS, so each denominator must be positive and finite. Ratios omit absolute scale and cover these fixtures before training, so they establish neither adequate absolute derivatives nor convergence.
- Dropping preparation leaves the caller RNG unchanged, as does refusal.
Checked parameter and position bounds precede model-array allocation.
Fresh initialization refuses
InitializedandRestoredstates before draws or mutation. Restoration takes a construction record and actual stored parameters and does not rescale. The laptop entry has metadata and arithmetic only, with no parameter arrays or execution to restore.
Carry explicit construction choices into the decoder core
Depth-aware initialization changes the starting output projections. It retains the selected source pairs, local positions, occurrence-local causal ranges and valid-target mean loss from Chapter 5. More blocks must preserve those boundaries; initialization cannot repair an incorrectly joined context or target.
Chapter 7 will place decoder dimensions and construction choices in a configurable core. Carry forward the distinction between fresh initialization and exact restoration, along with the meaning and limits of these residual observations. Unequal query and key/value head counts and trained-checkpoint serialization remain later work.