Chapter 08 · Content revision 3
Make optional dropout explicit and replayable
Apply optional residual dropout with saved-mask gradients, exact evaluation identity and checked random counters. Keep local replay distinct from durable job resume.
The problem: a random forward needs a matching backward
Optional dropout zeros selected elements during training. The forward result therefore depends on the execution mode and random-number state as well as the input values. Backward needs the mask used by that forward. Evaluation needs its own explicit behavior, and recomputing an activation must recover the original mask without consuming fresh randomness.
We will add inverted dropout to the existing decoder’s projected residual branches. The default drop probability is zero, preserving the disabled path. The one-half examples teach forward, gradient and replay correctness. They do not establish that dropout improves this model’s held-out loss.
Explain one complete saved mask
Take the literal input [2,-4,0,6] with shape [1,1,4]: one batch row, one
token position and four features. Use drop probability , keep probability
, and the supplied mask [1,0,1,1]. A mask bit of one means keep;
zero means drop. This mask is a fixed mathematical fixture, with no claim that
an arbitrary seed produces it.
Inverted dropout multiplies each kept input by . The first input becomes
, the second is dropped, the third remains zero, and the fourth becomes
. Multiplying the negative second input by a nonnegative zero coefficient
can store negative zero in f64, so the training vector is [4,-0,0,12].
Supply upstream gradient [3,5,7,-2], meaning the loss derivative with respect
to each output element. Backward multiplies it by the same saved coefficients
[2,0,2,2], giving final input gradient [6,0,14,-4]. The third input was
kept zero: its zero output does not mean it was dropped. Its input gradient
is . Reconstructing the mask from nonzero outputs would erase that
derivative incorrectly.
The linear probe deliberately fixes those upstream gradients and gives . It does not stand for a nonlinear model loss with a mask-independent gradient.
rust/demos/practical-ch08-dropout-semantics/src/lib.rs#dropout-fixed-mask-evidence /// The kept zero differentiates through its saved coefficient of two.
pub fn fixed_mask_evidence() -> FixtureResult<Value> {
let input = TensorValue::parameter(tensor(&[1, 1, 4], &INPUT)?)?;
let y = apply_supplied_mask_with_context(
AutogradContext::recording(),
&input,
DropoutPolicy::new(0.5)?,
&[1, 0, 1, 1],
DropoutLimits::default(),
)?;
y.output().backward_with_seed(
&tensor(&[1, 1, 4], &UPSTREAM)?.view(),
GraphRetention::Release,
)?;
let mut state = DropoutCounterState::from_seed(39);
let eval = apply_dropout_with_context(
AutogradContext::recording(),
&input,
DropoutMode::Eval,
DropoutPolicy::new(0.5)?,
fixture_site(),
&binding(),
&mut state,
DropoutLimits::default(),
)?;
let mut average = [0.0; 4];
for code in 0u8..16 {
let mask = std::array::from_fn::<_, 4, _>(|i| (code >> i) & 1);
let out = apply_supplied_mask_with_context(
AutogradContext::no_grad(),
&input,
DropoutPolicy::new(0.5)?,
&mask,
DropoutLimits::default(),
)?;
for (sum, value) in average.iter_mut().zip(out.output().value().as_slice()) {
*sum += value;
}
}
for sum in &mut average {
*sum /= 16.0;
}
require(average == INPUT, "mask enumeration mean differs")?;
Ok(
json!({"shape":[1,1,4],"input":INPUT,"drop_probability":0.5,"keep_probability":0.5,"mask":[1,0,1,1],"training_output":y.output().value().as_slice(),"training_output_bits":hex_bits(&bits(y.output().value().as_slice())),"upstream_gradient":UPSTREAM,"input_gradient":input.gradient().ok_or("fixed input gradient")?.as_slice(),"input_gradient_bits":hex_bits(&bits(input.gradient().ok_or("fixed input gradient")?.as_slice())),"eval_same_handle":eval.output().is_same_node(&input),"eval_output":eval.output().value().as_slice(),"eval_end_counter":state.next_counter(),"enumerated_masks":16,"enumerated_mean":average,"linear_probe":y.output().value().as_slice().iter().zip(UPSTREAM).map(|(x,g)| x*g).sum::<f64>()}),
)
} The executed fixed_mask record reports training_output as
[4.0,-0.0,0.0,12.0], input_gradient as [6.0,0.0,14.0,-4.0], and
linear_probe as -12.0. It also reports eval_same_handle: true and
eval_end_counter: 0. The stored second output has bits 8000000000000000,
while the final second input gradient has bits 0000000000000000: forward
negative zero and final accumulated positive zero are distinct observations.
Derive the training rule and its saved-mask derivative
For finite fixed input , let be the saved mask bit. Under the ideal Bernoulli model, : an element is kept with probability , where . The policy probability is fixed, rather than learned. With upstream derivative , training and the chain rule give
Backward uses the mask and scale saved by its own forward. A later call’s mode, probability or random cursor cannot replace them. Sampling another mask in backward would differentiate a different computation.
For this ideal distribution, and . Hence
For input at drop probability one half, the possible outputs are and , each with ideal probability one half; their expectation is . Enumerating all masks of the four-feature fixture also gives the original vector as its exact-arithmetic average. Enumeration checks that distribution, rather than measuring pseudorandom frequencies or training quality.
An individual call need not preserve a sum, magnitude or norm. Even an all-dropped small mask is legal. Elementwise expectation at one layer also does not make a nonlinear stochastic network’s average output equal to its deterministic evaluation output.
Keep probabilities, features and derivatives distinct
denotes the requested drop probability; denotes retention. identifies one element in batch, token, feature flattening order. and are that element’s input and training output, is its saved keep/drop bit, and is the supplied upstream loss derivative.
A decoder site has shape : counts rows, token positions per row and residual features per token. counts complete blocks. These dimensions determine how many words a fresh active forward consumes; they do not change which targets belong to the token-mean loss.
Reuse multiplication and keep mode separate from autograd
DropoutProbability::new rejects nonfinite probabilities, negative values and
values at least one. It preserves the accepted probability’s exact bits,
including an accepted signed zero, and stores the keep probability and scale.
Configuration validation still applies in Eval, even though Eval will not use
a mask.
Active dropout creates an untracked constant multiplier with the same shape as
the input. Each coefficient is zero or the saved training scale. Existing
TensorValue::mul_with_context applies it, and the existing Multiply VJP
retains the coefficients for backward. The mask is not a parameter, and no new
gradient rule is needed.
rust/crates/llm-from-scratch-practical/src/nn/dropout.rs#dropout-primitive /// Train samples even under no-grad; Eval and zero probability preserve the handle.
#[allow(clippy::too_many_arguments)]
pub fn apply_dropout_with_context(
context: AutogradContext,
input: &TensorValue,
mode: DropoutMode,
policy: DropoutPolicy,
site: DropoutSiteId,
binding: &DropoutBinding,
counter: &mut DropoutCounterState,
limits: DropoutLimits,
) -> Result<DropoutApplication, DropoutError> {
if !policy.active(mode) {
return Ok(DropoutApplication {
output: input.clone(),
record: None,
});
}
let n = input_entries(input)?;
let end = checked_range(counter.next, n)?;
limits.admit(n, 1)?;
if n == 0 {
return Ok(DropoutApplication {
output: input.clone(),
record: None,
});
}
let (key, coefficients) =
coefficients_from_validated_range(counter.next, end, n, policy.probability, || {
counter.key.unwrap_or_else(|| derive_key(counter.seed))
})?;
let (multiplier, output) = multiply(context, input, coefficients)?;
let shape = input.shape();
let ticket = DropoutReplayTicket {
version: DROPOUT_COUNTER_VERSION,
placement: DROPOUT_PLACEMENT,
purpose: DROPOUT_PURPOSE,
seed: counter.seed,
key,
start: counter.next,
end,
probability_bits: policy.probability.bits(),
scale_bits: policy.probability.scale().to_bits(),
site,
shape: [shape[0], shape[1], shape[2]],
binding: binding.clone(),
};
counter.next = end;
counter.key = Some(key);
Ok(DropoutApplication {
output: output.clone(),
record: Some(DropoutSiteRecord {
ticket,
input: input.clone(),
multiplier,
output,
}),
})
} DropoutMode chooses Train or Eval. AutogradContext separately permits or
disables new graph recording. The following table uses an already tracked
input, a nonempty site and a valid positive probability; zero probability
returns the input handle in every row.
| Dropout mode | New graph recording | Forward and backward consequence |
|---|---|---|
| Train | On | Sample a mask, apply the training scale and retain the multiplier for backward. |
| Train | Off | Sample and apply the same numerical rule without recording a new training graph. |
| Eval | On | Return the original input handle and its gradient connection, without sampling. |
| Eval | Off | Return the original input handle without creating a new graph or sampling. |
Eval and zero probability preserve every input bit, including signed zero.
They allocate no dropout mask, derive no dropout key and consume no random
words. no_grad() controls new operations; an identity return does not
retroactively detach an already tracked input handle. If an Eval identity
result receives upstream derivative , that connection passes to the
input without training rescaling.
Place dropout after each complete branch projection
The course policy residual-output-element-dropout-v1 has two sites per block:
the complete attention output after , just before the attention residual
addition, and the complete SwiGLU output after , just before
the feed-forward residual addition. The untouched residual input is still added.
Embeddings, normalization gains, attention probabilities and vocabulary logits
are not dropout sites in this policy.
Each active site draws for every element of its full rectangle, including PAD cells. Blocks run in ascending order, attention before feed-forward, then row-major batch, token, feature order. Padding, causal attention and valid-target masks keep their existing meanings. Repacking or changing microbatch boundaries can assign different counters to valid coordinates, even with the same seed.
Legacy model and run records remain version one with unchanged bytes and
digests. DropoutRunConfig is an explicit version-two wrapper containing that
legacy run and a version-one dropout policy; its identity domain is
decoder-dropout-run-v2. The policy records probability bits, seed, placement,
counter mapping and purpose. Changing it changes replay compatibility, while
the learned parameter count stays unchanged.
Specify the words and check the cursor
The course registry assigns initialization purpose 1, corpus order 2,
packing 3, optional dropout 4, sampling 5 and evaluation bootstrap 6.
Dropout owns purpose 4 and its own seed/key and next-word counter. Calls to
other mutable generators cannot advance that counter.
course-dropout-counter-v1 reuses SplitMix64. Let be the dropout seed,
the existing increment 0x9e3779b97f4a7c15, and
the result of one next_u64() call starting from
SplitMix64::from_state(a). The key and counter-indexed word are
Here is bitwise exclusive-or, and is the counter of the requested
word. Only the specified generator arithmetic wraps modulo .
For a request starting at with entries, the end is checked before
allocation. Overflow or an exhausted u64::MAX cursor rejects without partial
consumption. The generated range is .
rust/crates/llm-from-scratch-practical/src/nn/dropout.rs#dropout-counter fn derive_key(seed: u64) -> u64 {
SplitMix64::from_state(seed ^ SplitMix64::WORD_INCREMENT.wrapping_mul(DROPOUT_PURPOSE))
.next_u64()
}
fn word_at(key: u64, counter: u64) -> u64 {
SplitMix64::from_state(key.wrapping_add(SplitMix64::WORD_INCREMENT.wrapping_mul(counter)))
.next_u64()
}
fn checked_range(start: u64, count: usize) -> Result<u64, DropoutError> {
let n = u64::try_from(count).map_err(|_| DropoutError::Arithmetic("counter count"))?;
if n > 0 && start == u64::MAX {
return Err(DropoutError::CounterExhausted);
}
start.checked_add(n).ok_or(DropoutError::CounterExhausted)
} The conversion keeps the high bits:
The values are for integer , so one is never generated. Equality with the threshold keeps the element. Assuming uniform words, counting this grid gives effective drop probability
At , the grid represents the threshold exactly. At requested , grid zero drops while keeps. With the requested scale, even exact real arithmetic would then give
Stored f64 subtraction and division can add rounding as well. The earlier
expectation equality is the ideal Bernoulli statement; it is not an exact claim
for every requested floating-point threshold. Separate counter ownership also
does not prove statistically independent or cryptographically secure streams:
with an odd increment, keyed full-period sequences are cyclic shifts.
Inspect ten predetermined seeded masks
The executed primitive fixture uses seeds 0 through 8, then 39, with the
same input, upstream gradient, shape [1,1,4], purpose 4, half-drop policy
and initial counter 0. Each record ends at counter 4. The summary retains
all four generated hexadecimal words and exact output/final-gradient bits.
The table writes those f64 bit patterns as their corresponding scalar values:
| Seed | Mask, one keeps | Train output | Final input gradient |
|---|---|---|---|
| 0 | 0110 | [0,-8,0,0] | [0,10,14,0] |
| 1 | 0011 | [0,-0,0,12] | [0,0,14,-4] |
| 2 | 1101 | [4,-8,0,12] | [6,10,0,-4] |
| 3 | 1001 | [4,-0,0,12] | [6,0,0,-4] |
| 4 | 0110 | [0,-8,0,0] | [0,10,14,0] |
| 5 | 1110 | [4,-8,0,0] | [6,10,14,0] |
| 6 | 1000 | [4,-0,0,0] | [6,0,0,0] |
| 7 | 0011 | [0,-0,0,12] | [0,0,14,-4] |
| 8 | 1011 | [4,-0,0,12] | [6,0,14,-4] |
| 39 | 1101 | [4,-8,0,12] | [6,10,0,-4] |
For seed 39, the generated words are e0f2d7d7dd3d13ac,
bbe95911f05f90b5, 210c4710466fdb4c and 88a1d7a436e3ab9b.
At the one-half threshold, their high bits give mask 1101. The seeds were
fixed before execution; these records check the specified mapping, not
pseudorandom frequencies. A local Multiply edge can contribute negative zero,
while accumulation into an initially positive-zero adjoint gives the final
stored zeros shown here. Identity paths separately preserve input zero signs.
Commit only a successful complete forward
The active text wrapper checks the full counter demand and dropout resource limits before sampling. One candidate counter then covers every block and both sites. The existing private text decoder consumes that candidate; the caller’s live state is published only after the complete forward succeeds.
rust/crates/llm-from-scratch-practical/src/models/decoder_dropout.rs#dropout-text-forward /// One candidate counter covers every site and commits after the complete forward.
#[allow(clippy::too_many_arguments)]
pub fn forward_with_dropout(
&self,
context: AutogradContext,
token_ids: &[u32],
token_shape: &[usize],
mode: DropoutMode,
policy: &DropoutRunPolicy,
state: &mut DropoutCounterState,
limits: DropoutLimits,
) -> Result<DropoutModelForward, DecoderModelError> {
let p = policy
.policy()
.map_err(|_| DropoutError::Binding("invalid run policy"))?;
if !p.active(mode) {
return Ok(DropoutModelForward {
forward: self.forward_with_context(context, token_ids, token_shape)?,
sites: Vec::new(),
binding: None,
parameters: Vec::new(),
});
}
self.validate_tokens(token_ids, token_shape)?;
let shape = [token_shape[0], token_shape[1], self.config().model_width()];
let execution = self.fresh_dropout_execution(
context,
shape,
policy,
ordinary_layout(token_ids, token_shape)?,
state,
limits,
)?;
let input = TextInputEncoder::tokens(self, token_ids, token_shape)
.encode(context, &self.backbone())
.map_err(ordinary_error)?;
self.consume_dropout_text(input, execution, Some(state))
} A later-layer failure discards partial results and leaves the live cursor, parameters and pre-existing gradients unchanged. Active nonfinite arithmetic is an error, rather than a value repaired by clipping or replacing it with zero. This forward boundary does not rewind every allocation or internal graph ID, and it does not make backward or the optimizer update transactional. After a successful forward, the cursor has advanced; a failed update needs restoration of the last committed training state before retry.
Cached decoding explicitly selects Eval and its existing no_grad context.
The mode and graph choices are separate. An explicit cached Train request
returns CachedTrain before cache, work or dropout state changes. Disabled
ordinary and cached calls retain the preceding error order and behavior.
Bind replay to the original computation
DropoutReplayTicket is immutable evidence for a site. It binds mapping and
placement versions, purpose, seed/key, counter start/end, probability/scale
bits, layer/site, actual shape and run/text-layout/model-revision identities.
Full-model replay also checks the original live parameter nodes and revisions
before using the same private decoder core. A same-shaped different packed or
ordinary batch does not gain replay authority.
replay_dropout_with_context regenerates the saved coefficient tensor from the
ticket. It accepts no live counter and consumes zero live words. The complete
text replay validates all site tickets before decoder work. A ticket for old
weights cannot establish recomputation under changed weights.
For the supplied bridge, : each site has
entries and a fresh forward uses
words. Inputs are [0,99,100], targets
[99,100,1], positions [0,1,2], initialization and dropout seeds 39, and
drop probability . All three targets belong to the existing token-mean
loss.
| Boundary | Captured site and word range | Resumed operation and comparison |
|---|---|---|
| R1 | Layer 0 attention output, [0,48) | Regenerate its multiplier and output; compare input derivatives using the original model-loss upstream derivatives. |
| R2 | Layer 0 feed-forward output, [48,96) | Repeat the same comparisons at this later site, without reserving another live range. |
| R3 | Layer 1 attention output, [96,144) | Repeat at the third boundary; retain the final feed-forward range [144,192) for complete-core replay. |
The full replay compares every logit, token-mean loss and all named parameter gradients, using all four saved tickets. Gradients are deliberately cleared between comparison backward passes. These are local site and same-core recomputation checks; they do not provide Chapter 13’s activation scheduler or demonstrate a memory-saving training schedule.
The executed R1, R2 and R3 records each compare multiplier, output and
input-gradient coordinates exactly and retain live cursor 192. Full-core
replay reports equal bits for all logits, the token-mean loss and all
parameter-gradient coordinates, plus all four masks. A changed
same-shaped text input is rejected.
The continuation fixture runs four shared-trainer updates on that repeated
batch, with learning rate , AdamW coefficients and ,
epsilon , weight decay and global gradient-norm cap .
U1, U2 and U3 capture committed zero-gradient states after updates one, two and
three, at dropout cursors 192, 384 and 576; four fresh forwards end at
768.
Each in-memory snapshot carries parameter names/order/values and admitted model identity, AdamW moments/step, the zero-gradient and commit phase, dropout mapping/seed/cursor, run/config/dtype/backend bindings, batch identity/cursor, valid-target count and denominator, optimizer settings, schedule and remaining updates. Restoration creates independent live parameter nodes. Comparisons cover every remaining mask, logit, loss, raw and clipped gradient, update, parameter, moment, step, final cursor and next word. Missing or mismatched counter, phase, run, probability or batch cursor rejects.
The executed U1, U2 and U3 continuations report all_remaining_tapes_equal: true for three, two and one remaining updates respectively. All finish at
counter 768, with next word a8c1103f72c54933, matching final parameter,
optimizer and declared-state bits. In the injected post-forward failure, the
counter first reaches 192; restoring the last committed snapshot returns it
to 0 and restores the commit phase before an identical retry. The seven
missing/mismatched snapshot cases in the report all reject.
This snapshot is scoped to a four-update in-memory fixture. The unchanged version-one component checkpoint is not a complete training-job record. Chapter 15 still owns durable job continuation and restart policy.
Count mask data separately from memory peaks
The multiplier stores f64 coefficients, so one active site owns
mask-data bytes. If both multipliers in all blocks are live, their subtotal is
At nominal full-context shapes, this gives reference bytes, bridge
bytes and laptop bytes. These are data subtotals, not
allocator peaks: output tensors, saved graph primals, adjoints, temporary
storage, alignment and metadata also matter. The existing planner accounts for
their lifetimes. The laptop number is counterfactual active-f64 accounting;
mandatory 8gb-gpu-core keeps dropout off, and no GPU execution is established
here. Eval and zero probability allocate zero dropout masks.
The recorded compiled CPU summary run had peak resident set size bytes and took about seconds. That process measurement covers this bounded fixture, separate from compilation, container setup and nominal full-context planning. It does not measure the laptop or a GPU allocator peak.
Relate inverted scaling to earlier dropout
A random training subnetwork needs a separate inference rule; its sampled result and gradient depend on the saved mask.
Hinton and colleagues’ 2012 preprint describes random unit omission during training and an unthinned test network with halved outgoing weights in its one-half case. It does not specify the course’s decoder placement or counter. Srivastava and colleagues’ 2014 paper describes random unit removal during training and an unthinned inference network with retention-scaled outgoing weights. Its probability notation denotes retention, and its results do not establish benefit for this course’s decoder. The 2014 paper also describes backward through the sampled subnetwork. The 2014 paper uses for retention. Our notation uses for dropping and for retention.
For one linear contribution, retention-scaling an outgoing weight is equivalent to applying that factor to its input activation. The local Rust contrast therefore compares earlier unscaled training and retention-scaled evaluation with our inverted training and identity evaluation:
The two conventions place the retention factor in different modes. This
scalar comparison reuses TensorValue::mul_with_context; it is not another
dropout implementation or a reproduction of the paper’s whole network. The
2014 paper’s task results do not choose this decoder’s probability or residual
placement or specify its counter.
For the executed scalar input 2.0 with supplied keep bit 1 and retention
0.5, the earlier convention reports training/evaluation values 2.0/1.0.
The inverted convention reports 4.0/2.0. This is the same linear example in
both conventions, with the retention factor placed in different modes.
rust/demos/practical-ch08-dropout-semantics/src/lib.rs#dropout-historical-scaling let half = DropoutProbability::new(0.5)?;
let x = TensorValue::constant(tensor(&[1, 1, 1], &[2.0])?)?;
let one = TensorValue::constant(tensor(&[1, 1, 1], &[1.0])?)?;
let q = TensorValue::constant(tensor(&[1, 1, 1], &[0.5])?)?;
let two = TensorValue::constant(tensor(&[1, 1, 1], &[half.scale()])?)?;
let historical_train = x.mul_with_context(AutogradContext::no_grad(), &one)?;
let historical_eval = x.mul_with_context(AutogradContext::no_grad(), &q)?;
let inverted_train = historical_train.mul_with_context(AutogradContext::no_grad(), &two)?;
let mut off = DropoutCounterState::from_seed(39);
let inverted_eval = apply_dropout_with_context(
AutogradContext::no_grad(),
&x,
DropoutMode::Eval,
DropoutPolicy::new(0.5)?,
fixture_site(),
&binding(),
&mut off,
DropoutLimits::default(),
)?; Optional decoder dropout needs explicit mode, saved-mask backward and bound random-counter replay, with inference and continuation limits stated separately.
The PyTorch 2.14 reproducibility notes also state limits across releases, platforms and CPU/GPU execution. That contextual evidence supports a controlled-environment replay claim. PyTorch is not a dependency or random-number oracle for this implementation; equal seeds alone do not promise hardware-independent results.
Follow the same saved mask through forward and backward
The aligned rows connect each input to its saved coefficient and Train output, then connect its upstream derivative to its input derivative using that same coefficient. The third row explicitly identifies the kept zero. Eval uses the identity branch; site replay reuses an original range without advancing live random state.
One saved dropout mask governs Train output and backward
For input [2,-4,0,6] and drop probability one half, mask [1,0,1,1] means keep, drop, keep, keep. Train multiplies kept inputs by two, producing [4,-0,0,12]. Backward reuses that same mask and scale: upstream [3,5,7,-2] becomes input gradient [6,0,14,-4]. The third input is a kept zero, so its zero output still has input gradient fourteen. Eval returns the original input handle without scaling or sampling. R1 replay reuses the original layer-zero attention range [0,48); the complete forward's live cursor stays at 192.
Input → saved mask and Train scale → output; the same coefficient carries backward
| Feature and saved state | Input | Saved mask bit | Train coefficient | Train output | Upstream gradient | Input gradient |
|---|---|---|---|---|---|---|
| 1Kept | 2 | 1 | 2 | 4 | 3 | 6 |
| 2Dropped | -4 | 0 | 0 | -0 | 5 | 0 |
| 3Kept zero | 0 | 1 | 2 | 0 | 7 | 14 |
| 4Kept | 6 | 1 | 2 | 12 | -2 | -4 |
Eval preserves the input handle
The original values and gradient connection pass through unchanged. There is no training rescaling, mask or random draw.
- Eval output
- [2,-4,0,6]
Eval input derivative equals the supplied upstream derivative:
R1 reuses its original counter range
Site replay regenerates the layer-zero attention multiplier from its ticket. It makes no live reservation after the original complete forward.
- Original and replayed word range
- [0,48)
- Live cursor before and after site replay
- 192 → 192
Optional practice: reproduce and explain the fixed behavior
Run from the repository root with Docker running. The course launcher prepares
its workspace image, then runs the supplied CPU fixtures with locked offline
Cargo and container networking disabled. No corpus or model download is needed.
The default command prints one compact JSON summary with schema
practical-dropout-semantics-v1, including ten primitive seed rows, R1–R3
recomputation points and U1–U3 continuation points. Observable results include
fixed_mask.input_gradient with third value 14.0,
recompute.full_core.every_parameter_gradient_equal: true,
resume.final_counter: 768, and all_remaining_tapes_equal: true at each
continuation point:
./course run cargo run --offline --locked --release -p practical-ch08-dropout-semanticsFor the same fixtures’ individual word, tensor, update and snapshot values, use the trace option from the same repository root and Docker setup. It prints newline-delimited JSON records, ending with the compact summary:
./course run cargo run --offline --locked --release -p practical-ch08-dropout-semantics -- --tracerust/demos/practical-ch08-dropout-semantics/src/main.rs#dropout-semantics-demo fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let trace = match args.next().as_deref() {
None => false,
Some("--trace") => true,
_ => return Err("usage: practical-ch08-dropout-semantics [--trace]".into()),
};
if args.next().is_some() {
return Err("usage: practical-ch08-dropout-semantics [--trace]".into());
}
let report = practical_ch08_dropout_semantics::evidence()?;
let mut output = io::stdout().lock();
if trace {
for record in report.trace {
output.write_all(&canonical_json_bytes(&record)?)?;
}
} else {
output.write_all(&canonical_json_bytes(&report.summary)?)?;
}
Ok(())
} - Inspect the supplied mask’s third input, output and gradient. Explain why a zero output still has input gradient fourteen.
- Explain why backward must retain the forward coefficient, even after another call changes mode, probability or cursor. Relate the answer to the original upstream gradient.
- Use the training rule to explain an all-dropped four-element mask. Distinguish a legal individual result from ideal expectation preservation.
- Inspect the Eval/no-grad mode row. Explain why its result can still expose a tracked input handle, and identify the random state it leaves unchanged.
- Inspect the counter traversal and finite-grid cases. Explain why changing microbatch boundaries can change masks with the same seed, and why threshold equality keeps an element.
- Inspect R1 and U1. Identify the state each operation reuses or restores and explain why their comparisons do not establish an activation scheduler or durable job resume.
Checked explanations for kept zero, saved backward, all-drop, Eval identity, counter scope and local replay
- The third mask bit is one, so its saved coefficient is . Its input value is zero, giving zero output, but its upstream derivative is . Multiplication therefore gives input derivative . Output values alone do not recover the mask.
- The forward map’s derivative is its saved coefficient . For upstream
[3,5,7,-2], the supplied coefficients[2,0,2,2]give[6,0,14,-4]. A resampled mask belongs to a different forward and cannot differentiate this one; backward needs no new random words. - All four zero mask bits give zero coefficients, zero numerical outputs and zero input derivatives. A negative input may store negative zero in the output. At ideal drop probability one half, all-drop has probability , so it is legal and is not retried. Expectation averages over masks, rather than constraining each call’s norm or sum.
- Eval returns the input handle itself, so an already tracked handle retains its gradient connection.
no_grad()prevents new graph operations and does not detach that existing handle. Eval allocates no mask and changes neither the dropout key nor live cursor. - Counters follow full rectangular batch/token/feature order at every site. A changed layout or call boundary can assign a different counter to the same valid feature. The keep comparison is , so equality keeps; at , grid zero drops and the first positive grid value keeps. An equal seed alone does not remove layout or execution-environment bindings.
- R1 reuses the original site’s immutable ticket and
[0,48)range, while the live counter stays192. U1 restores a complete declared in-memory state after update one, including counter192, parameters, optimizer and committed zero-gradient phase, then compares the remaining three updates. R1 lacks a memory-saving activation schedule; U1 lacks a durable complete-job artifact and restart protocol.
Carry precise failure boundaries into Chapter 9
Optional inverted dropout now has a saved forward mask, explicit Train/Eval
behavior, checked purpose-four random ownership and bound local replay. The
disabled path retains the preceding decoder behavior. Chapter 9,
09-dependency-error-contract, makes dependency roles and error contracts
explicit; it must retain the difference between a failed complete forward and
a failed later training update. Chapter 13’s activation scheduler and Chapter
15’s durable job continuation remain separate capabilities.