2026/06/21 27.0k words

Transformer System #numinens#transformer#training#teacher-forcing#causal-mask#padding-mask#packed-sequence#parallelism#loss#label-smoothing#optimizer#adam#mixed-precision#checkpointing#reproducibility

M3.9 — Training Execution: Teacher Forcing, Masks, Loss, and Parallelism

0x00 Overview And Background

There is a sentence that is easy to remember and easy to misunderstand:

Transformer training is parallel.

If we read it too quickly, it sounds as if training has stopped being autoregressive. That is not the case. The supervised objective is still a next-token objective:

p(y | x) = product_t p(y_t | y_<t, x)

For encoder-decoder training, the decoder row that predicts y_t should only use the source sequence and the previous target tokens y_<t. What changes during training is not the probability factorization. What changes is that the full gold target sentence is already available in the dataset. The code can therefore build all gold prefixes in one rectangular tensor, use a causal mask to prevent future leakage, and compute many target-position losses in one forward pass.

The local code path is:

for i, batch in enumerate(data_iter):
    out = model.forward(batch.src, batch.tgt, batch.src_mask, batch.tgt_mask)
    loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)

This is the part M3.9 owns. M3.1–M3.8 established the computation from masked attention through the output head. Here we study the training loop around those objects:

collate / Batch
    -> src, tgt, tgt_y, src_mask, tgt_mask, ntokens
        -> model.forward(...)
            -> decoder hidden states out
                -> SimpleLossCompute
                    -> generator(out)
                        -> flatten predictions and labels
                            -> admitted local criterion
                                -> additive numerator + denominator
                                    -> one backward seed
                                        -> distributed completion + one-time unscale
                                            -> finite agreement + one global clip
                                                -> optimizer / scheduler commit

The short version is:

training is parallel in tensor computation,
but autoregressive in the visibility rule and loss objective.

The rest of this article follows the objects that make this possible.

Owner question

How does a corpus become correctly shifted and masked supervised rows, and how do those rows turn into numerically stable gradients, distributed updates, and restartable training state without changing the autoregressive objective?

M3.9 owns the training-side semantic transaction. It defines the supervised rows, objective reduction, logical update, and restart requirements; it does not claim that a particular distributed backend, checkpoint store, converted artifact, or live release executed successfully:

examples -> token/segment metadata -> batches or packed streams
inputs + query/key visibility + target/loss ownership
forward -> admitted head/criterion -> additive objective -> one backward seed
complete declared gradient collectives + unique-owner/denominator readiness
-> one-time unscale -> finite agreement -> one binary32 global norm and clip
-> selected logical update -> bounded physical projection -> durable checkpoint
evaluation requirements -> later-owner artifact and release handoff

It does not reopen tokenizer learning (M2.1), initial embedding/model-width state (M2.2), block equations (M3.1–M3.8), single-request generation (M3.10), live scheduling/session/cache behavior (M5), physical artifact conversion and runtime admission (M8.9), or deployment promotion, monitoring, rollback, and release (M8.10).

Evidence and implementation boundary. Equations in this article state technical contracts. Pinned source excerpts describe those exact historical revisions. Pseudocode and finite fixtures are reference models, not evidence that a framework, distributed job, checkpoint store, evaluation, serving artifact, canary, or rollback ran. Whenever a later section compresses the transaction, this owner boundary and the typed contracts take precedence.


How the training contract reached this form

The phrase teacher forcing came from recurrent-network training. In a free-running recurrent decoder, the output produced at step becomes an input at step . During teacher-forced training, the known target from the dataset is supplied instead. The distinction was therefore originally a choice inside a sequential recurrence:

free-running recurrence:
    input at t <- model output sampled at t-1

teacher-forced recurrence:
    input at t <- gold target token at t-1

The source-series comparison below makes that replacement visible. Read the right-hand path as a training-input policy, not as permission to expose the future target. Each prediction still receives only the preceding gold target.

Recurrent free-running decoding compared with teacher-forced recurrent training
Historical recurrent comparison, reused from A5. The free-running branch feeds the model's selected output back as the next input; the teacher-forced branch supplies the dataset's previous gold token. It illustrates input policy, not future-token visibility or the complete Transformer update.
No-image fallback: at recurrent step t, free running consumes the model token selected at t-1; teacher forcing consumes gold token y_(t-1). In both cases the model predicts y_t.

Recurrent sequence-to-sequence systems retained this stepwise schedule even when every gold target was known. The Transformer changed the materialized schedule. Its masked self-attention has no recurrent edge between neighboring decoder rows, so all shifted gold inputs can be embedded and processed in one tensor call while the causal mask preserves the same conditional support. This is why the 2017 Transformer paper could describe a model that remained autoregressive while being substantially more parallelizable during training.

Modern decoder-only pretraining and supervised fine-tuning did not remove that contract. They widened it:

recurrent teacher forcing:
    gold previous token -> one sequential decoder step

Transformer teacher forcing:
    all gold previous-token rows -> one causally masked tensor schedule

packed pretraining:
    many document segments -> one physical stream + segment-safe visibility

instruction tuning:
    one chat-template stream -> causal visibility + assistant-target ownership

distributed training:
    many local token sums -> one declared global objective and update

The last two lines are not cosmetic batching changes. They add new identities: segment boundaries, position resets, supervised spans, global denominators, collective order, optimizer ownership, and restart state. M3.9 therefore cannot stop at loss.backward(). Its subject is the complete update transaction.

ClaimStatusEvidence boundary
teacher forcing feeds known targets instead of prior model outputs during trainingexternally verifiedWilliams and Zipser’s recurrent training procedure; A5’s source-local reconstruction
a Transformer remains autoregressive while masked target rows can execute in parallelexternally verifiedAttention Is All You Need and the Annotated Transformer
packing can be equivalent only when examples cannot contaminate one anotherexternally verifiedEfficient Sequence Packing without Cross-contamination
modern SFT needs a separate target-ownership policy over the serialized chat streamofficial implementation boundaryHugging Face chat-template and TRL SFT contracts; exact behavior is template/version specific
distributed training preserves one objective only when numerator, denominator, and reduction conventions agreederivation in this articleobjective, accumulation, and distributed-reduction algebra plus executable oracles

Ownership firewall

M3.9 consumesM3.9 ownsM3.9 defers
M2.1 token IDs, special-token policy, collated rows, and segment metadatashifted input/target addresses, visibility construction, loss ownership, reduction, backward, update, and restart statetokenizer learning, vocabulary construction, and generic collation algorithms remain in M2.1
M3.1 attention-mask semantics and M3.8 vocabulary score rowswhich training rows exist, which rows are supervised, and how their gradients form one updateattention kernel semantics remain in M3.1; head geometry and local score policy remain in M3.8
model parameters and logical block interfacesaccumulation, precision, parallel ownership, optimizer/scheduler transition, and checkpoint/evaluation requirementsphysical FLOPs, traffic, kernels, and hardware efficiency remain in M4
a declared training objectiveexact/statistical restart classification and the logical handoff requirements for later artifactsinference iteration remains in M3.10; live scheduling/session/cache lifecycle in M5; distillation in M6.10; speculative execution in M7; physical artifact admission in M8.9; deployment evaluation, canary, promotion, monitoring, rollback, and release in M8.10

Background knowledge: factorization is not an execution schedule

Teacher forcing predates the Transformer. Recurrent encoder-decoder models also trained by feeding the gold previous target while asking the model to predict the next one. The Transformer changes the schedule because causal masking lets all gold prefixes occupy one rectangular tensor:

probability model:
    still factorized left to right

training data:
    supplies every gold prefix simultaneously

causal mask:
    prevents row t from reading labels after t

loss mask:
    decides which predicted rows contribute to optimization

Parallel training therefore means parallel evaluation of conditionally valid rows. It does not mean the model is allowed to condition on future target tokens, and it does not make attention masks interchangeable with padding or loss masks.

0x01 The Code Object: run_epoch

The best place to start is not an abstract definition of teacher forcing. It is the training loop. The next listing is a pinned historical source specimen and navigation aid, not the denominator-safe, successful-update, checkpointable reference transaction developed later. In the Harvard / Annotated Transformer revision cited below, run_epoch() receives an iterable of Batch objects, the model, a loss-compute object, an optimizer, and a scheduler:

def run_epoch(
    data_iter,
    model,
    loss_compute,
    optimizer,
    scheduler,
    mode="train",
    accum_iter=1,
    train_state=TrainState(),
):
    total_tokens = 0
    total_loss = 0
    tokens = 0
    n_accum = 0

    for i, batch in enumerate(data_iter):
        out = model.forward(
            batch.src, batch.tgt, batch.src_mask, batch.tgt_mask
        )

        loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)

        if mode == "train" or mode == "train+log":
            loss_node.backward()
            train_state.step += 1
            train_state.samples += batch.src.shape[0]
            train_state.tokens += batch.ntokens

            if i % accum_iter == 0:
                optimizer.step()
                optimizer.zero_grad(set_to_none=True)
                n_accum += 1
                train_state.accum_step += 1

            scheduler.step()

        total_loss += loss
        total_tokens += batch.ntokens
        tokens += batch.ntokens

        del loss
        del loss_node

    return total_loss / total_tokens, train_state

This loop names many objects used by the article, but it does not by itself define their cadence, denominator, update-success, precision, distributed, or checkpoint contracts:

batch.src:
    source token IDs

batch.tgt:
    shifted target input IDs

batch.tgt_y:
    next-token labels

batch.src_mask:
    source padding visibility

batch.tgt_mask:
    target padding + causal visibility

out:
    decoder hidden states, not vocabulary logits

loss_compute:
    generator + criterion + normalization

loss_node:
    differentiable scalar used by backward()

optimizer / scheduler:
    parameter update and learning-rate schedule

The current article should be read from this loop outward. Teacher forcing is not a magic training mode hidden inside the Transformer block. Precisely, it is the gold-prefix input policy: a training prediction row receives the known previous target rather than a model-generated token. Shifting, hard visibility, target ownership, loss reduction, and update execution are separate contracts that make the complete training transaction correct; they are not all synonyms for teacher forcing.

The following ledger is the object contract for the rest of the article:

ObjectShapeCreated byRole
raw exampleslist of source/target pairsdataset iteratorunbatched training samples
src[B, S]collate_batchpadded source token IDs
full target[B, T_full]collate_batchpadded target token IDs before shift
batch.src[B, S]Batch.__init__source IDs passed to model.forward
batch.src_mask[B, 1, S](src != pad).unsqueeze(-2)source padding visibility
batch.tgt[B, T_full - 1]tgt[:, :-1]decoder input IDs, gold prefix side
batch.tgt_y[B, T_full - 1]tgt[:, 1:]next-token labels, supervision side
target pad mask[B, 1, T](tgt != pad).unsqueeze(-2)hides padded target keys
future mask[1, T, T]subsequent_mask(T)blocks target positions j > i
batch.tgt_mask[B, T, T]make_std_masktarget padding plus causal visibility
batch.ntokensscalar(tgt_y != pad).sum()historical baseline count of non-PAD target labels; not the general eligible-row denominator
out[B, T, d_model]model.forward(...)decoder hidden states
generator output[B, T, V_tgt]generator(out)vocabulary scores or log-probabilities
flat predictions[B*T, V_tgt]contiguous().view(-1, V_tgt)one row per target position
flat labels[B*T]contiguous().view(-1)one class index per target position
true_dist[B*T, V_tgt]LabelSmoothingsmoothed target distribution
loss_nodescalar tensorcriterion(...) / ntokensgraph-connected value for backprop

This table is also a debugging map. When training breaks, the fastest question is not “is teacher forcing enabled?” It is:

for this row of out,
which target input positions were visible,
and which row of tgt_y is it compared against?

The immutable training manifest

The tensor table describes one batch. It does not identify the run that created the batch or the update transaction that will consume it. Two jobs can have identical tensor shapes and still optimize different objectives because they use different templates, pack boundaries differently, average different units, or advance the schedule on different counters.

A reproducible run therefore needs an immutable manifest before the first optimizer step. The useful split is:

immutable contract:
    what examples, rows, objective, update, and topology mean

mutable progress state:
    where this run currently is inside that contract
Manifest namespaceRequired identityWhy shape checks cannot recover it
datadataset revision/checksum, split, mixture weights, filtering/dedup policy, sample-address schemethe same token shape can come from a different corpus or mixture
representationtokenizer hash, vocabulary size, special-token IDs, normalization, chat-template hashequal IDs can name different token strings or roles
batchmax length, padding side, truncation, packing algorithm, boundary-label policy, position reset, worker/sampler policyall produce legal rectangular tensors with different supervision
objectivefactorization, shift owner, attention-mask family, target-owner rule, ignore ID, smoothing convention, weights, reduction denominatorthe scalar can look plausible while weighting rows differently
updateaccumulation unit/window, optimizer type and groups, weight decay, clip rule, scheduler family and step unitthe same gradient can produce a different transition
numericsparameter/compute/reduction dtype, autocast policy, loss-scaling policy, finite-check order, deterministic policydtype names alone do not describe where conversion occurs
parallelDP/TP/PP/SP/CP/ZeRO degrees, rank map, collective semantics, pipeline scheduleworld size alone does not identify state ownership or reduction
rngnamed data/model/pipeline streams and derivation ruleone global seed does not reconstruct per-rank stochastic order
checkpointshard schema, alias/tied-weight policy, saved state classes, version and migration policyfiles can load while silently omitting optimizer or sampler state
releasecode revision, dependency/container/hardware contract, evaluation suite, acceptance thresholds, rollback targeta checkpoint is not a validated training release

One compact manifest might contain:

{
  "schema_version": "m3.9-training-v1",
  "data": {
    "dataset_manifest_sha256": "...",
    "split": "train",
    "sample_address": "dataset/document/segment/token-offset",
    "mixture_weights": {"web": 0.7, "code": 0.3}
  },
  "representation": {
    "tokenizer_sha256": "...",
    "chat_template_sha256": "...",
    "bos_id": 1,
    "eos_id": 2,
    "pad_id": 0
  },
  "batch": {
    "max_tokens": 8192,
    "packing": "segment-safe",
    "cross_segment_target": "masked",
    "position_policy": "reset-per-segment"
  },
  "objective": {
    "family": "causal-next-token",
    "shift_owner": "external-collator",
    "loss_owner": "assistant-target-token",
    "reduction": "global-valid-token-mean",
    "label_smoothing": {"epsilon": 0.0, "support": "none"}
  },
  "update": {
    "accumulation": {"unit": "microbatch", "count": 8},
    "optimizer": "AdamW",
    "parameter_group_sha256": "...",
    "clip": {"kind": "global-l2", "max_norm": 1.0},
    "scheduler": {"family": "cosine", "step_unit": "successful-update"}
  },
  "numerics": {
    "parameter_dtype": "bf16",
    "reduction_dtype": "fp32",
    "loss_scaling": "disabled-for-bf16"
  },
  "parallel": {"dp": 8, "tp": 2, "pp": 2, "sp": true, "zero_stage": 1},
  "rng": {"streams": ["data", "dropout", "pipeline"], "rank_derivation": "declared"}
}

The values above are an illustrative logical schema, not a closed serialized manifest, a recommended universal configuration, or replay evidence. A real immutable identity needs a deterministic serialization, its authenticated payload bytes and digest, schema/version validation, and an explicit classification of every absent or inapplicable field. The contract is the explicitness. In particular, step_unit="successful-update" means a skipped AMP update does not advance the scheduler. A token-based schedule would instead declare a globally committed token counter.

Mutable state belongs beside, but not inside, the immutable identity:

Mutable stateMeaning
microbatch_stepforward/backward transactions attempted
successful_update_stepoptimizer transitions actually committed
global_valid_tokenssupervised denominator committed across ranks
epoch and sampler cursornext sample address and shuffle position
accumulation remainderunstepped gradient window and its token denominator
optimizer moments and per-parameter stepupdate history
scheduler and scaler statenext learning rate and overflow policy
named RNG statesnext data order, dropout mask, corruption, and pipeline stochastic event

This separation creates three compatibility outcomes:

same immutable contract + same mutable state:
    exact-resume candidate under the declared software/hardware bound

same immutable contract + intentionally reset mutable state:
    warm start or new run, never mislabeled exact resume

different immutable objective/data/update identity:
    migration or new experiment requiring an explicit compatibility decision

An immutable manifest does not guarantee determinism. It makes divergence auditable: a reviewer can distinguish changed semantics from floating-point or runtime nondeterminism. Resolving a previously unspecified row, objective, parameter-group, precision, topology, or update field changes the immutable identity. Existing mutable progress must then be explicitly migrated and rebound to the new identity, or the result is a new run; it cannot be called an exact continuation merely because tensor shapes still match.


0x02 Before the Loop: collate_batch and Rectangular Tensors

The model cannot train on a list of Python strings. Before Batch exists, the dataloader has already converted examples into padded tensors. This is not the main tokenizer article; M2.1 owns tokenizer algorithms, vocabulary addresses, special-token semantics, and generic collation construction. M2.2 owns the initial model-width representation state and embedding/output-tie eligibility. Here we only need the upstream fact that src and full target tensors are rectangular under an authenticated M2.1 representation identity.

A simplified collation path is:

processed_src = torch.cat(
    [
        torch.tensor([bos_id]),
        torch.tensor(src_vocab(src_pipeline(_src))),
        torch.tensor([eos_id]),
    ],
    0,
)

processed_tgt = torch.cat(
    [
        torch.tensor([bos_id]),
        torch.tensor(tgt_vocab(tgt_pipeline(_tgt))),
        torch.tensor([eos_id]),
    ],
    0,
)

src_list.append(pad(processed_src, ...))
tgt_list.append(pad(processed_tgt, ...))

src = torch.stack(src_list)
tgt = torch.stack(tgt_list)
return src, tgt

The result is:

src:        [B, S]
full_tgt:   [B, T_full]

For example:

full target row 1:
    <bos> I ate an apple <eos>

full target row 2:
    <bos> I ate <eos> <pad> <pad>

The rectangular shape is necessary for efficient tensor computation, but it introduces artificial positions. Padding is not semantic content. Later, src_mask, tgt_mask, tgt_y, and ntokens exist because the code must distinguish real supervision from rectangular padding.

The training worker wraps collated examples like this:

(Batch(b[0], b[1], pad_idx) for b in train_dataloader)

So the next object to inspect is Batch.


0x03 Batch: The Training Contract Object

The Batch class is where the training example becomes the set of tensors consumed by the model and the loss:

class Batch:
    def __init__(self, src, tgt=None, pad=2):
        self.src = src
        self.src_mask = (src != pad).unsqueeze(-2)

        if tgt is not None:
            self.tgt = tgt[:, :-1]
            self.tgt_y = tgt[:, 1:]
            self.tgt_mask = self.make_std_mask(self.tgt, pad)
            self.ntokens = (self.tgt_y != pad).data.sum()

    @staticmethod
    def make_std_mask(tgt, pad):
        tgt_mask = (tgt != pad).unsqueeze(-2)
        tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
            tgt_mask.data
        )
        return tgt_mask

The source side is simple:

self.src:
    [B, S]

self.src_mask:
    [B, 1, S]

The source sequence is already available, so the source mask mainly hides padding positions. It does not create a causal future rule.

The target side is the key:

self.tgt = tgt[:, :-1]
self.tgt_y = tgt[:, 1:]

If the full target is:

<bos> I ate an apple <eos>

then:

batch.tgt:
    <bos> I ate an apple

batch.tgt_y:
    I ate an apple <eos>

The decoder receives batch.tgt. The loss compares the decoder output against batch.tgt_y.

Gold target shifted into decoder inputs and next-token labels
Externally shifted encoder-decoder baseline. From [BOS, I, ate, an, apple, EOS], decoder inputs omit EOS and labels omit BOS, so input row i predicts target i+1. Internal-shift APIs encode the same relation but must not be shifted a second time.
No-image fallback: decoder input is [BOS, I, ate, an, apple]; aligned labels are [I, ate, an, apple, EOS]. Each pair retains its source/target address and still requires the eligibility and head-passport checks below.

This is teacher forcing in code. At the row that predicts ate, the decoder input contains the gold prefix:

<bos> I

not a sampled previous output. The model can be wrong at one position while the next training row still receives the gold previous token. That is why training can build the whole target-side matrix at once.

Deriving the shifted rows from the factorization

The shift is not a framework convention that happened to become popular. It is the tensor form of the probability chain rule. For source and target ,

Introduce a start token . A teacher-forced decoder row indexed by consumes and represents the conditional state for target :

The causal visibility row is

The vocabulary head produces one score row , so the negative log-likelihood is

Every summand depends on a different allowed prefix, but there is no same-layer recurrent execution edge that requires row to finish before row begins. Rows may still depend on earlier token rows through the key/value states produced by earlier layers, as permitted by the attention support relation. The implementation can therefore materialize all , all mask rows , and all target addresses in one tensor schedule. Autoregression is in the support relation and in the factorization, not in a same-layer step-by-step recurrence.

Encoder-decoder and decoder-only training differ in where the conditioning source lives, not in this target-row relation:

Training familyConditioning stateShifted prediction row
encoder-decoderencoder states for all valid source tokens plus target prefix decoder row predicts
decoder-only pretrainingsame token stream prefix residual row predicts
response-only SFTserialized system/user/tool/assistant prefixrow predicts token , but contributes loss only when target owner
packed independent documentssame-segment causal prefixphysical row predicts the next same-segment token; cross-segment targets follow an explicit boundary policy

One source address must survive every transformation

Flattening [B,T,V] to [B*T,V] later erases the visible batch/sequence axes. An audit still needs to identify what each row means. Carry or reconstruct this logical address:

target address:
    dataset revision
    example/document ID
    segment ID inside physical sequence
    logical target-token offset j
    physical batch row b and column t
    target token ID
    prediction-row address (b, t-1)
    attention-visibility descriptor
    loss-owner bit and optional row weight

For a packed decoder-only stream, let pi(s,j) map segment s and logical offset j to physical column. Then the ordinary within-segment row relation is

Visibility also needs the segment equality:

At a physical boundary where , the naive next-token shift would create a cross-document pair. Independent-document packing normally sets that target row to the ignore ID; continuous-stream pretraining may deliberately retain it. Neither policy can be inferred from a lower-triangular mask.

A collator that preserves the address contract can make the separation explicit:

def materialize_rows(token_ids, segment_ids, target_owner, ignore_id=-100):
    input_ids = token_ids[:-1]
    labels = token_ids[1:].clone()

    input_segment = segment_ids[:-1]
    target_segment = segment_ids[1:]
    same_segment_target = input_segment == target_segment

    # Target ownership is defined on target tokens, then shifted to rows.
    row_owner = target_owner[1:] & same_segment_target
    labels[~row_owner] = ignore_id

    positions = reset_positions_within_each_segment(input_segment)
    visibility = (
        causal_relation(len(input_ids))
        & same_segment_matrix(input_segment)
    )

    row_address = build_row_address(
        input_ids=input_ids,
        labels=labels,
        segments=input_segment,
        positions=positions,
        row_owner=row_owner,
    )
    return input_ids, labels, positions, visibility, row_address

This particular pseudocode is the independent-document, externally shifted branch. It is not the continuous-stream branch and it is not a generic collator implementation. A complete admitted-row join must also carry query validity, target validity, emitted-copy identity, and unique downstream-copy ownership rather than infer them from labels != ignore_id.

For each candidate prediction row , define

where is query/input-row validity, is target validity, is the target-policy owner bit, authenticates the declared continuation relation (including the independent-versus-continuous boundary policy), proves membership in the emitted target-copy set, and designates exactly one downstream owner among duplicate copies. Only rows with may enter the local criterion. For ordinary causal pretraining, target_owner is true for every policy-valid target token. For response-only SFT, it is true only for the template-defined assistant target spans. For padding, it is false. The attention mask is built from query/key validity, visibility, and segment policy, not from row_owner.

A semantic target token ID is still only a token address at this point. It becomes a hard vocabulary class—or a frozen target distribution —only after the M3.8 head passport authenticates vocabulary order, special-token policy, score kind, and the local criterion ABI.

The following invariants catch the highest-risk one-position and boundary errors:

for row in row_address:
    if row.label != ignore_id:
        assert row.target_logical_offset == row.input_logical_offset + 1
        assert row.target_segment_id == row.input_segment_id
        assert row.loss_owner

assert visibility.is_causal()
assert visibility.has_no_cross_segment_edges()
assert row_owner.sum() == (labels != ignore_id).sum()

For the two-row padded example:

full target row 1:
    <bos> I ate an apple <eos>

full target row 2:
    <bos> I ate <eos> <pad> <pad>

the split is:

batch.tgt row 1:
    <bos> I ate an apple

batch.tgt_y row 1:
    I ate an apple <eos>

batch.tgt row 2:
    <bos> I ate <eos> <pad>

batch.tgt_y row 2:
    I ate <eos> <pad> <pad>

Assuming row 1 has five non-padding labels and row 2 has three non-padding labels:

batch.ntokens = 5 + 3 = 8

That number will later normalize the loss. Without it, heavily padded examples would change the scale of training even though they do not contain more real target labels.


0x04 make_std_mask: Padding Plus Causal Visibility

Target shifting alone is not enough. If the decoder sees the whole shifted target tensor with unrestricted self-attention, a row can read future answer tokens. For example, the row that should predict ate might directly read target positions containing ate, an, or apple.

The target mask fixes this:

@staticmethod
def make_std_mask(tgt, pad):
    tgt_mask = (tgt != pad).unsqueeze(-2)
    tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(tgt_mask.data)
    return tgt_mask

The first line creates a target padding mask:

tgt_mask_pad: [B, 1, T]

The second line intersects it with the future-token mask:

def subsequent_mask(size):
    attn_shape = (1, size, size)
    subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(
        torch.uint8
    )
    return subsequent_mask == 0

The result is:

tgt_mask: [B, T, T]

For target input:

<bos> I ate an apple

the causal part is:

             <bos>  I  ate  an  apple
<bos>          1    0   0   0    0
I              1    1   0   0    0
ate            1    1   1   0    0
an             1    1   1   1    0
apple          1    1   1   1    1

The semantic rule is:

visible(i, j) = 1[j <= i] AND 1[tgt[j] != PAD]

The figure below shows the same lower-triangular target visibility rule. Every row can see its prefix region, while the future region is masked.

Lower-triangular target-key visibility for admitted teacher-forced query rows
Causal key support. Admitted query row i may read valid target-input keys j <= i; future keys are excluded before normalization. Query validity, key validity, segment support, and loss eligibility remain separate predicates.
No-image fallback: for five target-input positions, row 0 reads column 0; row 1 reads columns 0–1; continuing this pattern, row 4 reads columns 0–4. Padding or cross-segment keys are then removed by their own support rules.

Inside the pinned historical attention implementation, the mask changes the score tensor before softmax:

scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = scores.softmax(dim=-1)
out = torch.matmul(p_attn, V)

This placement matters. If masking happens after softmax, disallowed positions have already influenced the normalization. The tensor shape can remain [B, h, T, T], but the visibility rule is wrong. The finite -1e9 literal is historical implementation behavior, not the semantic definition of hard support. A production profile must define the mask value or kernel primitive, compute dtype, and all-masked-query policy so a disallowed key receives exactly zero probability under that profile.

Four masks that must not be collapsed

“The mask” often hides four separate ownership questions:

ObjectTypical shapeActs onMeaning
key padding / attention mask[B,1,1,S] or broadcast equivalentattention logitspadded key rows are not visible
causal mask[1,1,T,T] or implicit kernel flagattention logitsquery i cannot read future key j>i
packed-segment mask[N,N], block metadata, or varlen descriptorattention logitsone document cannot read another packed document
loss mask / ignored labels[B,T]objective rowsrow does not contribute supervised loss

A prompt token in supervised fine-tuning may be fully visible to answer tokens yet excluded from the loss. A padding row may be excluded from both visibility and loss. A document boundary in packed pretraining may require visibility separation even though neither side contains padding.

Define query validity , key validity , causal relation , and segment ID . One packed causal visibility rule for admitted queries is

An invalid query is rejected or given an explicitly typed no-row disposition; it is not repaired by applying softmax to an all-masked score row. Loss eligibility and its objective coefficient are independent of key visibility:

This separation prevents a common SFT bug: hiding prompt tokens from answer attention because they were intended only to be ignored by the loss.

Decoder-only next-token construction

The Annotated Transformer path is encoder-decoder, but decoder-only causal LM training uses the same shift principle on one stream. For token matrix

tokens: [B,L]

a minimal fixed-length construction is

input_ids = tokens[:, :-1]   # [B,L-1]
labels = tokens[:, 1:]       # [B,L-1]

Some model APIs instead receive equal-shaped tensors and shift logits/labels internally:

model input_ids: [B,L]
model labels:    [B,L]
effective comparison:
    logits[:, :-1, :] vs labels[:, 1:]

Both are valid if shifting happens exactly once. Audit the model class and data collator together.

For instruction tuning:

sequence:
    <bos> prompt tokens ... answer tokens ... <eos>

attention:
    ordinary causal visibility over prompt + answer

loss labels:
    -100 for prompt rows
    answer/eos token IDs for supervised rows

The row immediately before the first answer token must be supervised to predict that first answer token. Masking based on token membership without accounting for the one-position shift often drops or misaligns it.

Multi-turn SFT: loss belongs to shifted prediction rows

A conversational dataset does not enter the model as a list of message objects. A checkpoint-specific chat template first serializes roles, content, turn delimiters, tool calls, and end markers into one causal token stream:

structured messages
    -> chat template
        -> control tokens + content tokens
            -> tokenizer
                -> one token sequence z_0, ..., z_(T-1)

For K user/assistant turns, a turn-level shorthand for response-only SFT is:

That notation hides the implementation boundary. Each assistant response y_k contains many tokens, and decoder row i predicts token z_(i+1). Define a_j = 1 when target token z_j belongs to a policy-supervised assistant span. The loss ownership of prediction row i is therefore:

not a_i. The token-normalized objective is:

This one-position relation is the part a token-colored conversation diagram usually omits.

A concrete two-turn trace

Suppose the template emits this stream and the policy supervises assistant content plus the assistant end-of-turn token, but not role headers:

index j:
     0       1      2       3       4      5       6          7    8      9
token:
   <sys>     S    <eot>   <user>    U1    <eot>  <assistant>  A1a  A1b   <eot>
target a_j:
     0       0      0       0       0      0       0          1    1      1

index j:
    10      11     12      13          14     15
token:
   <user>    U2   <eot>  <assistant>    A2   <eot>
target a_j:
     0        0     0       0            1     1

After shifting, the supervised rows are:

input row z_i       next target z_(i+1)    supervised?
------------------------------------------------------
<assistant>         A1a                    yes
A1a                A1b                    yes
A1b                <eot>                  yes
<eot>               <user>                 no
<assistant>         A2                     yes
A2                  <eot>                  yes

The first assistant-content token is predicted by the row containing the assistant role marker. That row is not assistant content, yet its next target is. A collator that colors input rows and then uses those colors without shifting will drop A1a and may accidentally supervise the later <user> marker.

Two-turn SFT trace aligning assistant-owned target tokens to their preceding prediction rows
Target-token ownership shifts onto prediction rows. Assistant ownership is defined on serialized target token z_(i+1), while the loss is emitted by input row z_i. Prompt and earlier-turn tokens remain causally visible even when they do not own loss.
No-image fallback: an assistant role marker row predicts the first assistant content token and therefore owns that loss; assistant content rows predict later content/end markers; a row whose next token is a user or other ignored token owns no loss. Internal- and external-shift branches must encode this relation exactly once.

Attention visibility remains a different tensor:

assistant row predicting A2 may read:
    system text
    user turn 1
    assistant turn 1
    user turn 2
    the assistant role marker for turn 2

loss ownership:
    only rows whose next target is policy-supervised

Response-only training therefore does not hide user, system, or tool-result tokens from the model. It keeps them in the causal prefix while excluding their target tokens from the objective.

Internal-shift and external-shift collators

If the model class shifts equal-shaped labels internally, construct ownership on target token positions:

input_ids, assistant_target_mask = render_and_tokenize_chat(messages)

labels = input_ids.clone()
labels[~assistant_target_mask] = -100

# The model compares logits[:, :-1] with labels[:, 1:].
loss = model(input_ids=input_ids, labels=labels).loss

If the collator performs the shift explicitly, shift the ownership mask with the target tokens:

input_ids = tokens[:-1]
targets = tokens[1:]
row_loss_mask = assistant_target_mask[1:]

targets = targets.clone()
targets[~row_loss_mask] = -100
logits = model(input_ids).logits
loss = cross_entropy(logits, targets, ignore_index=-100)

These are equivalent only if shifting happens exactly once and the mask is aligned to the same rendered tokenization. Reconstructing assistant spans by searching decoded text is unsafe: tokenization, escaped content, repeated strings, and control tokens can all break character-to-token alignment.

Current Hugging Face chat templates can expose assistant-token ownership from template generation regions, and TRL’s SFT trainer can consume that mask for assistant_only_loss. That API is an implementation example, not the semantic definition. The template must mark assistant generation regions, for example with its supported generation-block convention, before a returned assistant mask is meaningful. The invariant is still row i owns target i+1; enabling a trainer flag is not proof that those spans were marked correctly.

Loss policy is a design choice

“Assistant-only” does not fully define the training objective. A release must record at least these choices:

Policy axisCommon choicesConsequence
assistant turnsevery assistant turn; final turn onlychanges which historical responses receive direct supervision
role/control tokensignored; selected assistant delimiters supervisedchanges whether the model learns to emit chat grammar directly
assistant end markersupervised; ignoredchanges learned stopping behavior
assistant tool callsupervised; separately weighted; ignoredchanges whether the model learns tool-call syntax
tool resultusually visible but ignored; occasionally supervised by a different objectiveconfusing tool observations with model output changes role semantics
hidden reasoning fieldsincluded; excluded; separately maskedchanges both objective scope and data-governance requirements

Completion-only and assistant-only loss are also different axes. A completion may contain several roles, while a multi-turn conversation may contain assistant spans on both sides of a prompt/completion boundary. Framework flags cannot replace an explicit token-level ownership test.

Collator assertions

For the equal-shaped internal-shift branch, a production collator should fail before training when its role metadata and target-position labels disagree:

assert input_ids.shape == labels.shape == assistant_target_mask.shape
assert (labels[~assistant_target_mask] == -100).all()

effective_targets = labels[:, 1:]
effective_rows = effective_targets != -100
assert effective_rows.sum().item() > 0

# No packed example may supervise a target across a segment boundary.
assert not ((segment_ids[:, :-1] != segment_ids[:, 1:]) & effective_rows).any()

# The row before each first supervised assistant token must remain causally valid.
for batch_index, target_index in first_supervised_token_of_each_assistant_span:
    assert target_index > 0
    assert labels[batch_index, target_index] != -100
    assert attention_mask[batch_index, target_index - 1] == 1

The externally shifted branch instead requires input_ids.shape == labels.shape == row_loss_mask.shape, verifies each prediction-row address against its target address directly, and must not slice labels[:, 1:] a second time. In both branches, predecessor validity is query/input-row validity; target ownership alone does not prove the predecessor row exists or has legal support.

Truncation needs the same discipline. Cutting away the assistant role marker, retaining only half a tool call, or leaving a sample with no supervised token changes the objective or creates an invalid batch. Packing adds another shift: the final row of one conversation must not be trained to predict the first token of the next independent conversation.

The practical failure signatures are:

assistant mask applied to input rows without target shift:
    first response token is dropped; a following user/control token may enter loss

loss mask reused as attention mask:
    responses cannot condition on the prompt or earlier turns

template and tokenizer applied in separate inconsistent passes:
    role boundaries no longer align with token indices

all labels are -100 after truncation:
    undefined normalization, skipped signal, or framework-specific NaN behavior

per-sequence loss means under variable response lengths:
    short and long responses receive a different weighting than a global token mean

Packed documents and reset state

Packing short documents removes padding but introduces boundaries. Suppose one physical packed batch sequence (not one prediction row) contains:

<bos> doc_A <eos> <bos> doc_B <eos>

Without a segment-aware mask, doc_B can attend to doc_A. Whether that is allowed is a data-objective choice, not an implementation default. If documents are meant independent, the runtime also needs position behavior:

absolute positions:
    continue globally or reset per segment according to training contract

RoPE positions:
    often reset per packed segment using explicit position_ids

loss:
    decide whether cross-boundary next-token pairs are supervised

FlashAttention-style variable-length kernels may represent boundaries with cumulative sequence lengths instead of materializing [N,N] masks:

cu_seqlens = [0, len(doc_0), len(doc_0)+len(doc_1), ...]

That metadata is semantically equivalent only for an authenticated adapter in which the kernel never mixes rows across those intervals and position IDs match the declared reset rule. Position coordinates are geometry; they do not universally delimit attention support or substitute for a segment boundary.

Packing equivalence has five independent conditions

For four independent sequences of lengths 3, 4, 2, 3, a single causal triangle over twelve physical positions is not equivalent to four examples. The reusable A11 figure below shows the required block-diagonal causal support:

Block-diagonal causal support for four packed independent sequences
Independent-sequence support, reused from A11. Four ordered segments of lengths 3, 4, 2, and 3 receive separate lower-triangular blocks. The bitmap establishes neither target-boundary policy, position geometry, nor weighting; those require independent contracts.
No-image fallback: concatenate segment intervals [0,3), [3,7), [7,9), and [9,12). A query may read earlier-or-equal keys only inside its own interval; every cross-interval cell is disallowed.

Packing is semantically equivalent to separate examples only when every owned contract agrees:

ContractSeparate examplesEquivalent packed representation
token contenteach sequence has its own token rowsconcatenation preserves every non-padding token in the intended order
visibilitycausal support inside each exampleblock-diagonal causal support or equivalent varlen intervals
target pairsnext token belongs to the same declared streamcross-segment prediction rows are ignored unless continuity is intentional
position geometryeach example starts from its declared position originposition_ids reset or continue exactly as the model/data contract declares
objective weighttoken/sequence weights are computed before paddingpacked physical layout does not redefine numerator or denominator

No single attention_mask proves all five. position_ids can delimit examples for one kernel implementation while remaining only geometry for another. Likewise, a varlen descriptor such as cu_seqlens can prevent cross-attention but says nothing about the label on the last row of a segment.

A11 reuses the LongAlign figure below because it exposes the joint systems and objective problem: padding creates idle work, packing removes much of it, block-diagonal attention preserves sequence isolation, and loss weighting decides whether physical packs change sequence influence.

LongAlign comparison of padded and packed layouts with block support and sequence-aware weighting
LongAlign-specific illustration, reused via A11. It contrasts padding with packed physical layout, block-diagonal attention, and the paper's sequence-aware weighting. Its weighting recipe is evidence for that paper, not a universal law of packing.
No-image fallback: packing can reduce padded computation, but equivalence to separate examples requires preserved token order, isolated attention support, declared cross-boundary targets, compatible positions, and an independently declared objective weight.

The figure is paper-specific. LongAlign’s weighting recipe is not a universal packing law; it is evidence that layout and objective weighting must be audited together. The separate Efficient Sequence Packing without Cross-contamination paper establishes packing as a bin-packing problem and tests mathematical equivalence for its model adaptations. Enhancing Training Efficiency Using Packing with Flash Attention compares several position/masking/packing combinations. Their names are not interchangeable implementation contracts.

Sorting, packing, and truncation alter the sample process

Length grouping can reduce padding and stragglers, but sorting also changes the order in which examples reach the optimizer. A deterministic packer can be described by one declared stage-order profile:

epoch seed
    -> global sample permutation
        -> optional length buckets with declared local/random order
            -> rank assignment
                -> deterministic packing plan
                    -> truncation/remainder policy
                        -> physical microbatches

The arrows above are not a commutation theorem. Changing the order of rank assignment and packing plan changes which examples share a device and how many tokens each rank receives. With synchronous data parallelism, every rank must execute compatible collective counts; a packer that yields different numbers of microbatches can hang the job unless the join/drop/pad policy is explicit.

Truncation needs a typed outcome:

truncate tail:
    keep prefix; record dropped target addresses

split with overlap:
    create new segment addresses; define duplicate-token loss ownership

defer whole sample:
    preserve example but carry packer remainder state

drop sample:
    change the training distribution and report it

Each outcome must retain the original sample/segment/token addresses, every emitted occurrence-copy address, duplicate-copy group and unique downstream owner, dropped/deferred intervals, stage-order version, and before/after packer state. Naming truncate, split, defer, or drop without those receipts is not enough to reconstruct target ownership or replay.

For response-only SFT, truncating before the first assistant target can produce a physically non-empty sequence with objective denominator zero. Filtering such rows, deferring them, or retaining them as context-only examples are different data policies.

Sampling probability and loss weight are separate levers

Suppose domain is sampled with probability , but the desired objective assigns domain mixture . For a sampled token from domain , an importance-weighted estimator can use

provided support and estimator assumptions hold. Oversampling a domain and also up-weighting its loss multiplies its influence. Conversely, changing only the sampler changes data order and frequency while leaving per-observation loss weight at one. Record both axes:

sample-selection distribution q
objective row/sequence weights w

Curricula add time dependence or , so their current phase and clock are checkpoint state. Empirical mixture choices remain source-reported; the equation does not claim importance weighting is always low variance or desirable.

Exact data resume is more than (epoch, seed)

For a map-style dataset, an epoch permutation might be a deterministic function only after the permutation algorithm, its byte-level inputs, tie behavior, and output order are versioned. Writing it as a mathematical set would lose order and repeated padded-tail occurrences, so a simple rank allocation is the ordered sequence

Real samplers may pad or drop the tail, form global batches before sharding, or balance token counts rather than sample counts. The exact rule is part of the manifest. PyTorch-style distributed samplers also require the epoch to be set consistently so a new permutation is selected rather than repeating one order.

Mid-epoch restart additionally needs the following state inventory:

StateWhy it matters
dataset/shard revision and global sample IDspositional indexes are unsafe after corpus mutation
sampler epoch, seed, permutation algorithm, rank/world size, and cursorreconstructs the next logical sample address
packer bins/remainders and truncation decisionsreconstructed samples can otherwise form different physical sequences
iterable-dataset source offsetsthere may be no random-access permutation to replay
worker RNG derivation and persistent-worker statecorruption, augmentation, and local shuffle can differ
prefetch/in-flight queueconsumed versus merely produced samples must be distinguished
committed update/data transaction markerprevents replaying or skipping work around asynchronous checkpoint capture

This inventory is not itself replay proof. Exact replay additionally needs a defined state transition, a capture-consistency boundary, and a commit rule that distinguishes produced, issued, consumed, and durably committed work. Iterable-source offsets and worker fields are requirements until the selected input implementation can actually serialize and restore them.

Saving only dataloader loop index i is insufficient when workers have already prefetched later examples. A practical exact-resume design either checkpoints the complete producer state at a quiescent boundary or uses idempotent sample addresses plus a committed cursor that lets queued work be regenerated safely.


0x05 Forward Pass: Known Targets Enter the Decoder All at Once

Now the training loop can call the model:

out = model.forward(
    batch.src,
    batch.tgt,
    batch.src_mask,
    batch.tgt_mask,
)

M3.1–M3.8 opened this model path. The dense full-vocabulary affine path below is one bounded M3.8 passport, not the only possible output interface. An adaptive, partitioned, sampled, or otherwise typed M3.8 head must expose its own admitted score and criterion socket before M3.9 can consume it. For the dense baseline, M3.9 now needs the training-side shape:

batch.src:       [B, S]
batch.tgt:       [B, T]
batch.src_mask:  [B, 1, S]
batch.tgt_mask:  [B, T, T]

out:             [B, T, d_model]

The decoder output has one row per target input position. It is not vocabulary logits yet:

out: [B, T, d_model]

This distinction is one of the most important implementation details in the source articles. In the Harvard implementation, EncoderDecoder.forward() does not call the generator. The generator is called later by SimpleLossCompute.

The training picture is therefore:

known source + known shifted target
    -> one masked forward pass
        -> one decoder hidden row per target position

The causal mask is why this is legal. Every row exists in the same tensor, but row t can only use target positions up to t.

At the objective level, nothing has stopped being autoregressive:

loss = - sum_{b,t : tgt_y[b,t] != PAD}
           log p_theta(tgt_y[b,t] | tgt[b,<=t], src_b)

The matrix form does not change the conditioning rule. It changes how many conditional distributions can be evaluated in one pass. For a single batch row:

decoder input row seeslabel row predicts
<bos>I
<bos> Iate
<bos> I atean
<bos> I ate anapple
<bos> I ate an apple<eos>

The model computes those rows together, but the target mask makes the row-wise conditioning match the autoregressive factorization.

Known shifted target rows evaluated together under causal support
Matrix-parallel evaluation of gold prefixes. Shifted inputs, causal key support, decoder rows, and aligned local losses are materialized together. The picture is a simple all-valid-label profile; general admission additionally requires query, target, policy, continuation, copy, owner, and head-passport checks.
No-image fallback: rows containing prefixes [BOS], [BOS,I], and so on are evaluated in one tensor; they predict I, ate, and later targets respectively, while each row's visibility ends at its own prefix.

Read this figure as the local training object: shifted target input, mask, parallel decoder output, and one target-position loss per valid label.


0x06 SimpleLossCompute: Generator, Flatten, Criterion, Normalize

The next object is the loss-compute wrapper:

class SimpleLossCompute:
    def __init__(self, generator, criterion):
        self.generator = generator
        self.criterion = criterion

    def __call__(self, x, y, norm):
        x = self.generator(x)
        sloss = (
            self.criterion(
                x.contiguous().view(-1, x.size(-1)),
                y.contiguous().view(-1)
            )
            / norm
        )
        return sloss.data * norm, sloss

The inputs are:

x:
    out from model.forward(...)
    shape [B, T, d_model]

y:
    batch.tgt_y
    shape [B, T]

norm:
    batch.ntokens
    scalar count of non-pad labels

The first line applies the generator:

x = self.generator(x)

Shape:

[B, T, d_model] -> [B, T, V_tgt]

Depending on implementation, the generator may return raw logits or log-probabilities. Those are different types, not merely prose conventions. A criterion passport must state at least the ordered vocabulary identity, score kind (logits or log_probabilities), dtype, admitted hard-class or frozen- target type, ignore serialization, reduction (none at the local boundary), and local VJP contract. In the Annotated Transformer style, the generator applies log_softmax, and the criterion is built around KL divergence / label smoothing. The generator and criterion passports must match exactly.

Then the batch and target-position dimensions are flattened:

x.contiguous().view(-1, x.size(-1))
y.contiguous().view(-1)

Shape:

generator output:
    [B, T, V_tgt] -> [B * T, V_tgt]

labels:
    [B, T] -> [B * T]

Conceptually, every admitted eligible prediction row becomes one local vocabulary criterion row in this dense baseline. Padding, an unowned SFT target, a broken continuation, or a non-owner duplicate may still occupy a physical flattened row without being eligible for the objective. Implementation-wise, contiguous() is not decorative. After slicing or transposition, PyTorch tensors may not have a memory layout that view() can flatten directly. The source code calls contiguous() before view() because shape algebra is not the same thing as memory layout.

The criterion returns a summed loss, and the code normalizes it:

sloss = criterion(flat_x, flat_y) / norm

In this pinned baseline, using norm = batch.ntokens means:

normalize by real target labels,
not by rectangular padded positions.

That shorthand is valid only for the simple profile in which every non-PAD target is query-valid, target-valid, policy-owned, a valid declared continuation, an emitted copy, and the unique downstream-owner copy. General packing, SFT, duplication, and weighted objectives use the six-term eligibility join and the additive reduction below; they must not infer admission from ntokens alone.

The return values have different purposes:

return sloss.data * norm, sloss

The first value is used for logging/token accounting. The second value, sloss, is still connected to the computation graph and is assigned to loss_node:

loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)

Only loss_node should receive:

loss_node.backward()

Calling .backward() on a detached logging value is a shape-invisible but fatal training bug.


0x07 LabelSmoothing: Target Distribution and PAD Exclusion

The criterion in the source training code is often:

criterion = LabelSmoothing(
    size=len(vocab_tgt),
    padding_idx=pad_idx,
    smoothing=0.1,
)

The class does two things at once:

1. build a smoothed target distribution
2. compute KLDivLoss against the generator output

The implementation begins with:

class LabelSmoothing(nn.Module):
    def __init__(self, size, padding_idx, smoothing=0.0):
        super(LabelSmoothing, self).__init__()
        self.criterion = nn.KLDivLoss(reduction="sum")
        self.padding_idx = padding_idx
        self.confidence = 1.0 - smoothing
        self.smoothing = smoothing
        self.size = size
        self.true_dist = None

After SimpleLossCompute flattens the tensors, LabelSmoothing.forward() receives:

x:       [B * T, V_tgt]    log-probabilities in this implementation
target:  [B * T]           target token IDs

It constructs:

true_dist: [B * T, V_tgt]

The core logic is:

assert x.size(1) == self.size
true_dist = x.data.clone()
true_dist.fill_(self.smoothing / (self.size - 2))
true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
true_dist[:, self.padding_idx] = 0

mask = torch.nonzero(target.data == self.padding_idx)
if mask.dim() > 0:
    true_dist.index_fill_(0, mask.squeeze(), 0.0)

self.true_dist = true_dist
return self.criterion(x, true_dist.clone().detach())

For a non-pad target class y, vocabulary size V, pad index p, and smoothing value epsilon, this exact recipe requires V >= 3, y != p, and 0 <= epsilon <= 1:

q_y = 1 - epsilon
q_p = 0
q_i = epsilon / (V - 2), for i not in {y, p}

For a row whose serialized target is padding:

stored ignore row = 0 for every vocabulary item i

That all-zero row is an ignore serialization, not a normalized target distribution . It is excluded before local-criterion admission and must not be passed downstream as though it were a probability distribution.

This is why the padding_idx handling appears twice:

zero the PAD column:
    do not assign smoothing mass to PAD

zero rows whose target is PAD:
    do not train on artificial padded labels
Harvard label-smoothing target construction with gold confidence and PAD exclusion
Harvard source-profile label smoothing. For an admitted non-PAD gold class, confidence 1-epsilon goes to gold, PAD gets zero, and epsilon is divided among the other V-2 classes. An all-zero serialized PAD row is ignored; it is not a target distribution.
No-image fallback: require V >= 3, gold different from PAD, and epsilon in [0,1]. Then Q_gold=1-epsilon, Q_PAD=0, and every other class gets epsilon/(V-2).

This section is easy to summarize incorrectly as “label smoothing makes one-hot labels softer.” That is true but too incomplete for implementation. In this code, label smoothing is also where vocabulary size, pad index, flattened target rows, and the loss function meet.

Cross entropy from the autoregressive likelihood

For one valid target row with class and logits ,

Substitute softmax:

The stable implementation subtracts the row maximum :

Differentiation yields

This expression structurally couples the loss to the vocabulary row, but it does not guarantee a nonzero value in every coordinate: a coordinate can be exactly zero when , and the sole coordinate of a degenerate V=1 softmax is zero. M3.8 owns the head-level interpretation; M3.9 owns which admitted training rows and coefficients form the scalar reduced across devices.

Label smoothing as a mixture target

A common conceptual target is

where distributes mass over allowed non-target classes. Cross entropy becomes

For a complete, normalized, parameter-independent target distribution ,

minimizing smoothed cross entropy is equivalent to minimizing KL divergence from the fixed target distribution up to the constant . Under those same conditions their raw-logit VJPs agree, although their scalar values differ. This explains why the Harvard implementation can use KLDivLoss on log-probabilities.

The exact convention differs:

Harvard shown above:
    target class gets 1-epsilon
    PAD gets zero
    epsilon spread over V-2 remaining rows

some framework cross-entropy implementations:
    mix with uniform mass over all V classes,
    so the target also receives epsilon/V

Both can be called label smoothing while producing different targets. Reproducing a run requires the exact formula, ignore-index behavior, reduction, and denominator.

The source-series figure below contrasts one-hot targets, smoothed targets, and model probability rows over several positions. It is an intuition diagram: the exact mass allocation still comes from the declared formula, not from the bar heights.

Illustrative one-hot, smoothed-target, and model probability bars
Qualitative smoothing comparison, reused from A5. The bars contrast a one-hot target, a softer target, and model probabilities. Their drawn heights are not authoritative; the selected target-distribution recipe and head passport define the exact values.
No-image fallback: one-hot supervision places all mass on gold; smoothing reallocates declared mass according to one exact recipe; the model row is a separately normalized prediction. CE and forward KL share the stated VJP only for a complete normalized frozen target.

The general weighted row objective

criterion(...) / ntokens is one special case of a more general reduction. Index every materialized prediction row by . Let:

Q_r(v): frozen admitted target distribution for vocabulary item v
p_r(v): model probability for vocabulary item v
e_r:    six-term eligible-row predicate defined above
a_r:    non-negative numerator coefficient
b_r:    non-negative denominator coefficient

The unreduced cross entropy of row is

Define the objective numerator and denominator separately:

Then

Both coefficient arrays and are fixed data, or explicitly stop-gradient, for the VJP shown below. Keeping and separate prevents one normalized-weight profile from being mistaken for every weighted estimator. This form identifies common policy choices:

Policya_rb_rDenominator identity
ordinary token meane_re_religible target-copy rows
response-only SFTe_r with assistant target policysame as a_religible assistant-owned targets
per-sequence meane_r / N_s for semantic sequence se_r / N_snumber of non-empty semantic sequence identities, not physical packs
self-normalized domain/importance weightinge_r w_re_r w_rselected weight mass
count-normalized weighted objectivee_r w_re_religible-row count; a distinct objective, not an error by definition
fixed microbatch meane_r / N_m inside non-empty microbatch me_r / N_mnumber of non-empty microbatches; differs from token mean unless conditions align

For complete normalized frozen , the logit gradient follows directly. Because ,

Rows with e_r = 0 have a_r=b_r=0 and contribute no local objective. Whether a larger weight changes the numerator, denominator, or both is part of the declared objective. Multiplying only the numerator is not universally wrong; it defines the distinct count-normalized weighted profile above. What is wrong is claiming one profile while implementing another.

The zero-denominator case must be a typed objective result. A microbatch can contain no eligible rows after truncation or response-only masking, or it can contain eligible rows whose declared denominator coefficients are all zero. These causes are different even though both project to GLOBAL_ZERO_MASS after the relevant accumulation/distributed scope. There is then no scalar objective and no objective cotangent. This layer does not decide whether later update policy replays, skips, or otherwise disposes of the window:

row_loss = admitted_local_criterion(scores, targets, reduction="none")
eligible = (query_valid & target_valid & policy_owner
            & continuation_valid & emitted_copy & unique_copy_owner)
numerator_coeff = eligible * row_numerator_coeff
denominator_coeff = eligible * row_denominator_coeff

numerator = (row_loss * numerator_coeff).sum(dtype=torch.float32)
denominator = denominator_coeff.sum(dtype=torch.float64)

if denominator == 0:
    cause = ("EMPTY_ELIGIBLE_SET" if not eligible.any()
             else "NONEMPTY_ZERO_WEIGHT_MASS")
    return NoObjective(state="GLOBAL_ZERO_MASS", cause=cause)
else:
    return ScalarObjective(value=numerator / denominator)

In an accumulation or distributed window, a locally empty microbatch need not force a globally empty objective. The global objective state is formed only after the declared numerator and denominator scope is complete. Even then, objective construction exports a scalar/cotangent or a typed no-objective state; later update policy owns the action.

Label-smoothing conventions are different objectives

For vocabulary size , gold class , PAD class , and smoothing , the Harvard source path uses:

The row sums to one because

Another common implementation mixes a one-hot target with the uniform distribution over all classes:

Then the gold class receives and PAD receives unless ignore_index removes the whole row. A third convention mixes over all non-PAD classes, giving the target some of the uniform mass but none to PAD. These variants have different gradients even when all are called label_smoothing=0.1.

Also separate the optimized quantity from the reported one:

For complete normalized frozen , minimizing smoothed cross entropy and KL divergence has the same parameter minimizer and raw-logit VJP, but their scalar values differ by . Those conclusions do not automatically extend to an incomplete, unnormalized, or parameter-dependent target. A training log that changes from CE to KL without recording the convention has a discontinuity even when the admitted gradients agree.

Token mean, sequence mean, and microbatch mean are different objectives

Token-normalized loss is

Sequence-normalized loss first averages each sequence:

The second gives a short sequence and long sequence equal total weight; the first gives each valid token equal weight. Equal averaging of microbatch means adds a third weighting. Dataset mixture weights, curriculum sampling, and packed sequence lengths interact with this choice even if the same tokens are observed.

For two sequences with token-loss sums and counts ,

while

If , these expressions generally differ. Packing does not decide which is correct; the experiment contract does. LongAlign, for example, explicitly studies packing together with loss weighting because physically placing several sequences in one pack must not accidentally redefine their relative objective weight.

Perplexity is an aggregate of unsmoothed negative log-likelihood

For a held-out token set, perplexity requires a separate authenticated gold-NLL evaluation socket: the exact checkpoint/artifact and head passport, ordered vocabulary, gold hard-class targets, eligibility policy, natural-log criterion, dataset/split identity, and global aggregation manifest must all be frozen. Given accepted total gold NLL and accepted eligible-token count ,

This uses natural logarithms. With base-2 logarithms, the corresponding exponential base is 2. More importantly, the conventional language-model perplexity is computed from the gold one-hot NLL, even if training used label smoothing. Exponentiating a smoothed CE/KL training scalar produces a different quantity and should not be reported as directly comparable perplexity.

Perplexity aggregation must preserve the same numerator/denominator order:

global_nll = completed_all_reduce_sum(local_gold_nll_sum)
global_tokens = completed_all_reduce_sum(local_eligible_gold_tokens)
mean_nll = global_nll / global_tokens if global_tokens > 0 else NA
ppl = exp(mean_nll) if isfinite(mean_nll) and exp_is_finite(mean_nll) else NA

Averaging per-sequence, per-batch, or per-rank perplexities is invalid because the exponential is nonlinear. Report zero-token evaluation shards separately; evaluate the final ratio only when the global denominator is positive; and publish PPL only when both the mean NLL and its exponentiation are finite. Otherwise the result is explicitly N/A, together with its cause—not a silently clamped or overflowed metric.


0x08 Backward, Accumulation, Optimizer, Scheduler

After the scalar loss node is created, the training loop turns it into gradients and parameter updates:

At the formal boundary there are two mutually exclusive seed routes: one declared global scalar objective root, or one explicitly supplied cotangent for typed score rows. A transaction uses exactly one route. The objective has already applied its one declared denominator, so no child denominator, ntokens, rank count, or accumulation-count division may be applied again. The complete window is bound to one parameter revision, graph/linearization revision, alias map, parameter-presence roster, objective revision, and set of logical gradient owners. A detached logging scalar is neither seed route.

if mode == "train" or mode == "train+log":
    loss_node.backward()
    train_state.step += 1
    train_state.samples += batch.src.shape[0]
    train_state.tokens += batch.ntokens

    if i % accum_iter == 0:
        optimizer.step()
        optimizer.zero_grad(set_to_none=True)
        n_accum += 1
        train_state.accum_step += 1

    scheduler.step()

The basic path is:

loss_node.backward():
    compute gradients through generator, decoder, encoder, embeddings, etc.

optimizer.step():
    update parameters using accumulated gradients

optimizer.zero_grad(set_to_none=True):
    clear gradients for the next accumulation window

scheduler.step():
    update learning-rate state

The source code uses gradient accumulation:

if i % accum_iter == 0:
    optimizer.step()
    optimizer.zero_grad(set_to_none=True)

Backward happens every training batch. Optimizer step happens only on the accumulation cadence. The source also calls:

scheduler.step()

every training batch after the accumulation block. If this cadence is changed, it should be changed intentionally. It is easy to claim “scheduler steps when optimizer steps,” but that is not what this source loop shows.

The source includes a commented line:

# loss_node = loss_node / accum_iter

This is worth noticing. In gradient accumulation, one must decide whether to scale each microbatch loss by accum_iter, change the learning rate, or accept a different effective gradient scale. The source line is commented, so M3.9 should not pretend accumulation is automatically scale-correct without explanation. The safe statement is: accumulation changes update cadence, and loss scaling must be handled deliberately.

Quantitative accumulation audit

There are two separate invariants: when an update occurs and what objective its accumulated gradient represents.

The displayed source loop enumerates from zero. With accum_iter=4 and eight microbatches indexed 0..7, its condition i % accum_iter == 0 steps at:

i = 0: update after one microbatch
i = 4: update after microbatches 1,2,3,4
i = 5,6,7: gradients remain unstepped when the loop exits

That is not four-plus-four accumulation. A full-window condition is normally based on (i + 1) % accum_iter == 0, and the final partial window must be flushed explicitly. This is a source-level off-by-one and remainder hazard, not a property of gradient accumulation itself.

Loss scaling has a second trap. Let microbatch contain real target tokens and let be its token-mean loss. The exact token-normalized objective for an accumulation window is:

Simply dividing every by the number of microbatches computes an equal microbatch average. It equals the token average only when all are equal. A reference implementation can backpropagate each unnormalized token sum loss_node * batch.ntokens, accumulate window_tokens, divide accumulated gradients by that token count, and then update:

pending = 0
window_tokens = 0
optimizer.zero_grad(set_to_none=True)

for i, batch in enumerate(data_iter):
    out = model.forward(
        batch.src, batch.tgt, batch.src_mask, batch.tgt_mask
    )
    loss, loss_node = loss_compute(out, batch.tgt_y, batch.ntokens)
    (loss_node * batch.ntokens).backward()
    pending += 1
    window_tokens += int(batch.ntokens)

    if pending == accum_iter:
        for p in model.parameters():
            if p.grad is not None:
                p.grad.div_(window_tokens)
        optimizer.step()
        optimizer.zero_grad(set_to_none=True)
        scheduler.step()
        pending = 0
        window_tokens = 0

# Repeat the same divide/step/zero sequence when pending > 0.

That snippet is single-process. Distributed data parallelism adds a specific normalization factor, not just a vague reduction step. Let rank r accumulate an unnormalized token-sum gradient g_r, let R be the world size, and let:

For the bounded profile used in this derivation—PyTorch DDP’s default reducer, fixed initial world size , no custom communication hook, no join-mode divisor change, and a common parameter-use/bucket contract—the synchronized gradient is world-size averaged, so each replica holds:

The desired global token-mean gradient is therefore:

Each rank must all-reduce the token count and scale the synchronized gradient by world_size / global_tokens. Dividing by each rank’s local token count gives different objectives when padding or sequence lengths differ across ranks.

Communication cadence matters too. Under DDP, the first accum_iter-1 forward/backward calls can run inside model.no_sync(); the final call exits that context and synchronizes the accumulated gradients. The forward pass must also be inside no_sync(), as required by the PyTorch contract. Synchronizing every microbatch is mathematically usable but pays an all-reduce every time and defeats the communication-saving purpose of accumulation.

Mixed precision fixes the remaining order. Keep one loss scale throughout the effective batch, accumulate scaled gradients, call scaler.unscale_(optimizer) once after the final backward, apply the global token-normalization factor, then clip, step, and update the scaler. Unscaling or changing the scale before the window is complete mixes incompatible gradient scales. If the scheduler is optimizer-step based, stepping it once per microbatch advances the learning-rate schedule accum_iter times faster than the parameter updates.

When accumulation is exactly a large-batch gradient

The source-series diagram below compares several small batches accumulated at fixed weights with one larger batch. Its approximation note concerns taking an optimizer update between small batches. Exact gradient accumulation does not do that: all microbatches in the window differentiate the same parameter state.

Historical comparison of sequential small-batch updates and a large-batch update
Historical approximation contrast, reused from A5. Its “approximately equal” path concerns taking parameter updates between small batches. Exact accumulation instead evaluates every microbatch at one parameter revision, adds numerator gradients, and applies the complete window denominator once.
No-image fallback: sequential small-batch SGD changes parameters between gradients and is generally not a large-batch gradient. Exact accumulation holds parameters fixed, sums all numerator gradients, divides once by the total denominator, and then performs one update; floating-point order can still prevent bit identity.

Let microbatch have weighted loss numerator and denominator . The effective-window objective is

Because differentiation is linear and every microbatch uses the same ,

There are two exact implementation forms:

form A: accumulate unnormalized numerator gradients
    backward(S_m) for every m
    divide parameter gradients by D_W once at update boundary

form B: pre-scale each numerator when D_W is known
    backward(S_m / D_W) for every m

Backpropagating each microbatch mean and dividing by instead computes

which equals the window token mean only under special cases such as equal denominators. Dividing by the configured accumulation count also mishandles a final partial window unless its actual count and token denominator happen to match the full window.

The word exact is bounded by the execution contract:

ConditionRequired for what kind of equivalence?
no optimizer step inside the windowsame parameter point for every microbatch gradient
same examples, row weights, masks, and total denominatorsame mathematical objective
gradient summation in exact arithmeticalgebraic gradient equivalence
same floating-point reduction orderbitwise gradient equivalence, usually not guaranteed
same stochastic masks per logical examplesame stochastic realization rather than only same distribution
stateful layers updated equivalentlysame non-parameter state; dropout has no running state, BatchNorm does

Even when the algebra matches, separate microbatch kernels can sum in a different order from one large-batch kernel. Floating-point results can differ slightly. That does not make the objectives different, but it limits bitwise claims.

Nonlinear operations belong after the complete gradient

Global-norm clipping is nonlinear. For clipping operator

in general

Here the clip passport must freeze , the exact non-negative stabilizer , comparison boundary (< or <= at the threshold), norm dtype, parameter/alias/shard owner map, handling of zero, absent, sparse, and non-finite gradients, and the collective completion rule. The norm is over each logical gradient exactly once; counting aliases or replicated copies changes the operator.

Clipping each microbatch gradient and then summing does not simulate clipping the effective-batch gradient. The same applies to Adam moment updates: calling optimizer.step() after every microbatch produces state transitions and evaluates later gradients at changed parameters. It is not gradient accumulation.

The transaction boundary is therefore, for a declared physical profile:

for each microbatch in window:
    forward at the same parameter version
    form weighted numerator
    backward numerator
    retain accumulated gradients and denominator

once at window close or final remainder:
    complete every declared gradient collective and its completion token
    obtain the global denominator and unique logical-owner roster
    unscale each active optimizer exactly once if FP16 scaling is active
    apply the one declared global normalization under the reducer convention
    evaluate the closed finite predicate over all declared owners/buffers
    compute one binary32 norm over unique logical parameters and clip once
    optimizer step once if finite
    zero gradients
    scaler update
    scheduler/counters advance only under declared success policy

An update attempt and a committed update are different events. If a finite check rejects the gradient, the declared skip-all policy keeps model parameters, optimizer moments, and an update-based scheduler at the old committed version. That atomic skip across every optimizer is a policy this article selects; it is not a universal promise of each framework scaler. The data cursor and attempted-token counter may still advance; that policy must be manifested because replaying or dropping the rejected window changes the run. Logical all-old/all-new selection, physical writes, scaler state, disposition logging, and durable checkpoint publication are separate transitions. A finite flag alone does not make them fault-atomic across rank or process failure.

A denominator-safe local accumulation skeleton

optimizer.zero_grad(set_to_none=True)
pending_microbatches = 0
window_denominator = torch.zeros((), dtype=torch.float64, device=device)
window_has_objective = False

for is_last, batch in with_last(data_iter):
    logits = model(batch)
    row_loss = criterion(logits, batch.labels, reduction="none")
    weights = batch.loss_owner * batch.row_weight
    numerator = (row_loss * weights).sum(dtype=torch.float32)
    denominator = weights.sum(dtype=torch.float64)

    if denominator > 0:
        scaled_backward(numerator)  # unnormalized S_m
        window_has_objective = True
    window_denominator += denominator
    pending_microbatches += 1

    close = pending_microbatches == accum_iter or is_last
    if close:
        objective_state = classify_global_objective(window_has_objective,
                                                    window_denominator)
        if objective_state == "SCALAR_OBJECTIVE":
            unscale_if_needed_exactly_once(optimizer)
            divide_gradients_(model.parameters(), window_denominator)
            finite = closed_owner_roster_is_finite()
            if finite:
                clip_grad_norm_(model.parameters(), max_norm)
                optimizer.step()
                scheduler.step()  # successful-update schedule
            update_scaler_from_attempt_result(finite)
        else:
            record_no_objective(objective_state)
            # No backward/update attempt occurred: do not advance scaler growth.
        optimizer.zero_grad(set_to_none=True)
        verify_present_gradient_buffers_cleared_and_absent_ones_still_absent()
        pending_microbatches = 0
        window_has_objective = False
        window_denominator.zero_()

This is a single-process semantic skeleton, not framework-ready code. Its zero_grad(set_to_none=True) call is an intent until the implementation records before/after receipts for every present physical gradient buffer while keeping absent, frozen, and no-objective states distinct. The distributed and numerical sections below add DDP’s averaged all-reduce factor, no_sync() placement, sharded norms, fixed-window loss scaling, globally agreed finite checks, and recomputed RNG state.

The optimizer and scheduler are constructed like this:

optimizer = torch.optim.Adam(
    model.parameters(),
    lr=config["base_lr"],
    betas=(0.9, 0.98),
    eps=1e-9,
)

lr_scheduler = LambdaLR(
    optimizer=optimizer,
    lr_lambda=lambda step: rate(
        step, d_model, factor=1, warmup=config["warmup"]
    ),
)

The rate() function implements the Noam-style schedule:

def rate(step, model_size, factor, warmup):
    if step == 0:
        step = 1
    return factor * (
        model_size ** (-0.5) * min(step ** (-0.5), step * warmup ** (-1.5))
    )

Equation:

lrate = factor * d_model^(-0.5)
        * min(step^(-0.5), step * warmup^(-1.5))

The first phase warms up. The later phase decays.

Noam learning-rate curve rising through warmup and then decaying
Noam schedule shape. The learning rate rises linearly to update w and then decays with inverse square root. The scheduler passport must state which clock is used and distinguish the value visible before an attempt, the value used by a committed update, and the next value.
No-image fallback: eta(t)=c*d_model^(-1/2)*min(t^(-1/2), t*w^(-3/2)); the branches meet at t=w. Under a successful-update clock, rejected or no-objective windows do not advance t.

Adam state and bias correction

For gradient , Adam maintains first and second moments:

Zero initialization biases both estimates toward zero, so Adam uses

and updates

The step counter is optimizer state. Restoring weights without moments and step creates a different training trajectory even if the next batch and learning-rate value look correct.

AdamW decouples weight decay:

Applying L2 regularization inside is not generally equivalent under Adam’s adaptive denominator. Bias, norm, and embedding parameters are often assigned different decay policies; that parameter-group map is checkpointed training configuration.

Optimizer families change state, scale, and migration

The optimizer name is not a minor implementation detail after the objective is fixed. It determines persistent state, the nonlinear transformation applied to the gradient, and what must be migrated with a checkpoint.

Plain SGD applies

Momentum adds a velocity state:

Adam keeps parameter-shaped first and second moments. Adafactor instead factorizes the second-moment estimate of a matrix into row and column statistics. Its auxiliary second-moment storage can scale like rather than for that matrix. The paper also introduces update clipping, a changing second-moment decay, and parameter-scale-relative steps; selecting only “factored state” is not the complete paper contract.

LAMB begins from an Adam-like direction and applies a layer/tensor trust ratio, schematically:

The exact clipping, decay, zero-norm, and bias-correction rules belong to the chosen implementation. LAMB’s source-reported large-batch results do not make it a universal replacement for AdamW.

FamilyPersistent auxiliary state per parameterDistinctive transitionMigration consequence
SGDnoneraw gradienteasiest warm start; still preserve schedule/update counters
momentum SGDone velocityexponentially averaged directiondropping velocity changes next update
Adam/AdamWfirst moment, second moment, stepcoordinate-wise adaptive denominator; AdamW decouples decaymoments, per-parameter step, groups, and decay policy are required
Adafactorfactored or full second moment; optional first moment by configurationfactored adaptive scale and update clippingstate shape depends on parameter rank and implementation options
LAMBAdam-like moments and trust-ratio state/conventiontensor/layer norm ratio scales adaptive directionparameter grouping and norm boundaries are part of the contract

The storage table is conceptual. Fused, sharded, quantized, or master-weight implementations change physical bytes without changing the logical state that a resume must reconstruct.

For every logical parameter and optimizer field, PRESENT_NONZERO, PRESENT_EXACT_ZERO, ABSENT, FROZEN, and NOT_APPLICABLE are distinct states. A zero moment is data; a missing moment may mean uninitialized, unsupported, or corrupt; a frozen parameter owns no update; and a per-parameter step can differ from a group/global clock in some algorithms. Save/load and migration rules must preserve those distinctions rather than fill missing state from shape alone.

Parameter groups are named update policy

A weight-decay rule is applied to parameters, not to textual substrings. A robust builder resolves aliases, assigns each unique parameter exactly once, and hashes the result:

decay = []
no_decay = []
seen_parameter_ids = set()

for module_path, module in model.named_modules():
    for local_name, parameter in module.named_parameters(recurse=False):
        address = f"{module_path}.{local_name}".lstrip(".")
        parameter_id, aliases = parameter_registry.resolve(
            model_schema_id=model_schema_id,
            address=address,
            parameter=parameter,
        )
        if parameter_id in seen_parameter_ids:
            continue  # tied embedding/head or another alias
        seen_parameter_ids.add(parameter_id)

        if is_declared_norm_or_bias(module, local_name):
            no_decay.append((parameter_id, aliases, parameter))
        else:
            decay.append((parameter_id, aliases, parameter))

assert_unique_and_complete(decay, no_decay, model)
parameter_group_sha256 = hash_addresses_and_hyperparameters(
    decay=decay,
    no_decay=no_decay,
)

data_ptr() may help inspect aliasing inside one live process, but allocation addresses change across save/load and are not stable parameter or optimizer state identities. The registry above is a required logical mapping: canonical fully qualified names, explicit alias groups, model-schema revision, shapes, dtypes, presence state, and unique ownership. Using parameter.ndim == 1 as a no-decay shortcut is a policy, not a theorem; it can classify embeddings, gates, or unusual parameters incorrectly. Name matching alone is also fragile under refactors. The manifest should record resolved parameter IDs and aliases, group order, decay kind (decoupled or coupled), exact hyperparameters/flags, state schema, and an authenticated payload digest. Resolving or changing any of those fields creates a new update identity and requires explicit optimizer/progress migration before state is attached.

A learning-rate schedule needs a clock

The word step can mean at least four counters:

microbatch step:
    every attempted forward/backward transaction

successful update step u:
    every optimizer transition actually committed

global supervised-token count tau:
    total valid objective denominator committed across ranks

sample/sequence count:
    examples committed, sensitive to packing and variable length

Noam in the Transformer paper is expressed using an update-like step. A cosine decay over successful updates can be written

A token-clock schedule instead uses , for example linear warmup to followed by cosine decay to token budget :

Token clocks make a schedule less sensitive to changed microbatch and packing shapes, but only if tau is the globally reduced committed supervised denominator. Counting padded or attempted tokens produces another clock.

Cosine warm restarts, polynomial decay, constant-with-warmup, inverse-square- root, and warmup-stable-decay schedules all define different curves and state. Their reproducibility tuple is:

family + all hyperparameters + clock type + current clock value
+ update-skip policy + endpoint behavior + restart/cycle state
+ learning rate visible before the attempt
+ learning rate actually used by every committed parameter group
+ next learning rate after the declared clock transition
+ separate no-objective, rejected-attempt, and committed-update dispositions

These values are not interchangeable. A scheduler can expose a “current” value while the optimizer used the prior value, and a rejected or no-objective window may leave both the update clock and next learning rate unchanged under the declared successful-update policy.

Changing effective batch size and scaling the learning rate is an empirical optimization choice, not part of gradient-accumulation equivalence. Linear scaling and warmup have source-reported successes in large-batch regimes, but the valid scaling range depends on optimizer, model, data, and gradient noise.

Deriving the Noam peak

The schedule is

with warmup . The branches meet when

Therefore the learning rate rises linearly until the warmup step and peaks at

then decays as . The scheduler’s definition of t must match actual optimizer updates, not merely dataloader iterations, when gradient accumulation is active.

Gradient clipping order under mixed precision

For the declared clip passport, global norm clipping computes

and scales by

Under loss scaling , backward produces . Clipping before unscale compares with threshold and over-clips by the scale factor. The scale is bound to the complete accumulation window and active optimizer, and the one-time-unscale receipt must be present before the clip-ready state. The order is:

accumulate scaled backward passes
    -> unscale optimizer gradients once
    -> apply global token/world-size normalization
    -> check finite gradients
    -> clip unscaled global norm
    -> optimizer step
    -> update loss scaler
    -> scheduler step if update succeeded

If overflow causes the scaler to skip an optimizer update, an update-count-based scheduler should not advance as though parameters changed.

Precision is a graph of states, not one dtype

The phrase “train in BF16” is incomplete. A compact introduction starts with four numerical roles:

RoleExample policyWhy it is separate
parameter storageBF16 model weights or FP32 weightsdetermines persistent model representation
operator computeautocast selects BF16/FP16 GEMMs and safer reduction pathsdifferent operators have different range and accuracy needs
accumulation/reductionFP32 dot-product accumulation, norm/loss sums, and collective bufferslong reductions amplify rounding and overflow risk
optimizer stateFP32 master parameters and FP32 moments in a common mixed-precision recipesmall updates and moment history can be lost in narrow formats

The common binary formats illuminate the tradeoff:

FormatExponent bitsFraction bitsApproximate largest finite valueTraining consequence
FP32823broad range and finer precision, higher traffic/storage
BF1687FP32-like range with coarse significand
FP16510finer significand than BF16 but much narrower range
TF32 matmul mode8about 10 effective input fraction bitsFP32-like rangeNVIDIA matmul execution policy, not a checkpoint storage dtype

Subnormal handling and flush-to-zero behavior depend on the device and kernel. Those details are runtime state, not facts implied by the file extension. Likewise, autocast is an operator policy: a matrix multiply, softmax, norm, loss reduction, and optimizer update need not execute in the same dtype. The exact allowlist and promotion behavior are framework-, device-, and version-specific.

A reproducible precision passport must go further. For every logical parameter, master copy, activation family, score/logit row, local loss, numerator, denominator, gradient buffer, collective buffer, norm accumulator, finite predicate, optimizer state, and checkpoint field, it records storage, input, compute, accumulation, reduction, and output dtypes where applicable. It also binds device/backend, autocast implementation and version, enabled regions, explicit casts, rounding and subnormal policy, scaler identity/configuration, and the owner/completion receipt for each conversion. This passport selects a logical numerical branch; later physical realization and durable publication are separate evidence layers.

Why loss scaling works, and where it stops working

Let be the loss scale. Backward on

produces

If the scaled gradient remains finite, dividing by recovers in exact arithmetic. The practical benefit is that small intermediate gradients are shifted into FP16’s representable range before they underflow. Scaling cannot repair a forward activation that already overflowed, and a scale that is too large can overflow the backward pass.

All microbatches in one accumulation window must use the same :

If the scale changes from to without first separating or unscaling the stored gradients, the buffer contains and no single division recovers .

A dynamic scaler is a feedback controller, not part of the mathematical objective:

overflow on attempted update:
    skip every parameter/optimizer/scheduler update
    next_scale = current_scale * backoff_factor

growth_interval successful updates:
    next_scale = current_scale * growth_factor

otherwise:
    next_scale = current_scale

In the pinned PyTorch GradScaler state dictionary, the five scalar fields are scale, growth_factor, backoff_factor, growth_interval, and _growth_tracker. An article-level rejected/skipped-attempt counter can be useful audit state, but it is not one of those five framework fields and must be stored in a separate disposition ledger. A GLOBAL_ZERO_MASS no-objective window made no scaler-backed update attempt, so it must not be reported to the scaler as a successful finite attempt merely to advance the growth tracker.

Distributed finite checking is one decision

In a replicated or sharded run, every rank and pipeline stage must agree on whether the logical update commits:

where means every declared gradient and required optimizer-facing buffer in rank ‘s closed participant roster passes the declared finite predicate. The roster binds ranks, pipeline stages, unique logical storage/alias owners, buffer presence states, predicate dtype, process group, collective sequence, and completion token. If one tensor-parallel shard overflows while another stage steps, the checkpoint no longer represents one parameter version.

An implementation may obtain this agreement implicitly when infinities propagate through an authenticated replicated-gradient profile, or explicitly by reducing a finite flag and waiting for completion. Neither route is universal across sharded buffers or custom communication hooks. The declared semantic contract is:

close accumulation window
-> finish required gradient collectives
-> unscale exactly once
-> apply declared global loss normalization
-> reduce finite flags across every update owner
-> if finite: global logical clip -> optimizer step
-> if not finite: no parameter or optimizer transition
-> update scaler on every owner from the same result
-> advance update-based scheduler only after a committed step

Loss normalization and unscaling commute in exact arithmetic because both are linear scalar divisions. Neither commutes with clipping. Framework APIs can also track whether an optimizer has already been unscaled, so production code should follow the runtime’s required call order rather than manually reaching into scaler internals.

Activation checkpointing replays a stochastic program

Activation checkpointing saves selected boundary tensors and recomputes an interior forward region during backward. If an ordinary block stores activation state and costs forward work , checkpointing approximately trades some of that saved for another execution of selected forward operators. The exact memory and compute change depends on the partition; it is not a universal “half memory, twice compute” rule.

For a stochastic region,

the random state is an input to the realized forward graph. Backward must recompute with the same dropout mask, not . Otherwise it differentiates a different sampled function.

def checkpointed_region(x):
    # The checkpoint runtime preserves or reconstructs the RNG state that
    # existed when this region ran in the original forward.
    return dropout(ffn(x), p=0.1, training=True)

y = checkpoint(checkpointed_region, x, preserve_rng_state=True)

PyTorch exposes reentrant and non-reentrant checkpoint mechanisms with different recording and recomputation behavior; current documentation recommends passing use_reentrant explicitly. The article’s invariant is implementation-independent: recomputed values, side effects, collectives, and random draws must satisfy the selected checkpoint contract.

RNG ownership and topology

One integer seed is not complete stochastic state. The immutable manifest and mutable progress payload must name and address every stochastic stream. A large training run can have separate streams for:

dataset permutation and worker-local augmentation
token corruption or span masking
dropout in replicated layers
tensor-parallel-consistent dropout
sequence-parallel token-local dropout
pipeline-stage stochastic operators
sampling used by alternative training procedures

Saving only the initial seed does not reveal each stream’s current counter. Exact resume saves generator states or reproducible counter addresses together with algorithm/version, device, topology/replication rule, and the next semantic event address. A topology-stable counter design can derive a random block from semantic keys such as:

(run_seed, global_sample_id, layer_id, operator_id, logical_token_address)

rather than from “whatever rank happened to execute next.” That design is not free: replicated operators that require identical masks must share addresses, while token-sharded operators require disjoint addresses. Changing tensor, sequence, or pipeline topology therefore needs an explicit RNG migration policy.

For a checkpointed region, preserved RNG is necessary but not sufficient. A recompute receipt also binds the region/graph revision, checkpoint variant, device migration, autocast context, parameter/buffer versions, input values, side effects and collective order, output-value comparator, and early-stop policy. Deterministic replay also has a boundary. The same masks and batches do not guarantee bitwise results when kernels, collective trees, compiler schedules, or floating-point reduction order change. The release contract should say whether it promises exact replay on a pinned stack, numerical closeness, or only statistical equivalence.

Historical compressed mixed-precision and recomputation diagram; the caption states its missing type and evidence boundaries
Historical compressed precision diagram—not an execution receipt. It introduces fixed-window scaling, one-time unscale, normalization, finite selection, clipping, and RNG-preserving recomputation, but the authoritative contract is the precision passport and typed no-objective path in the text. Logical branch selection, physical realization, and durable checkpoint evidence remain separate.
No-image fallback: bind one scaler and scale to a complete objective-bearing window; complete its backward/collectives; unscale the active optimizer once; apply the declared normalization; wait for a closed-roster finite decision; clip one unique-owner norm; then commit or retain all-old logical state. A no-objective window performs no scaler update. Recompute separately binds graph, inputs, RNG, device, autocast, side effects, and output comparator.

The precision transaction and recomputation transaction meet at backward, but they own different state: the scaler controls gradient representation; the checkpoint runtime controls which forward values and RNG states are replayed.

Again, this is not a separate optimizer article. It belongs here because loss_node.backward() is not the end of training. The scalar loss only matters because it produces gradients that the optimizer and scheduler use to change model parameters.


0x09 Train/Eval Mode and Dropout

The source training worker switches modes explicitly:

model.train()
run_epoch(..., mode="train+log", optimizer=optimizer, scheduler=lr_scheduler)

model.eval()
run_epoch(
    ...,
    optimizer=DummyOptimizer(),
    scheduler=DummyScheduler(),
    mode="eval",
)

This is not only a bookkeeping convention. It changes the forward pass because dropout behaves differently in training and evaluation.

The handwritten training article shows dropout at several Transformer locations:

after token embedding + positional encoding
after attention softmax, on attention weights
inside the feed-forward network
inside the sublayer/residual wrapper
Dropout sites around embeddings, attention, feed-forward, and residual branches
Source-profile dropout locations. The diagram marks dropout after embedding plus position, on attention probabilities, inside the feed-forward path, and on the sublayer/residual branch. Exact locations and RNG ownership are model-version specific.
No-image fallback: the pinned model applies dropout at four named sites. Training mode enables their stochastic behavior; evaluation mode disables it, while a separate autograd context controls gradient recording.

In code:

class PositionalEncoding(nn.Module):
    def forward(self, x):
        x = x + self.pe[:, : x.size(1)].requires_grad_(False)
        return self.dropout(x)
def attention(query, key, value, mask=None, dropout=None):
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = scores.softmax(dim=-1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value), p_attn
class PositionwiseFeedForward(nn.Module):
    def forward(self, x):
        return self.w_2(self.dropout(self.w_1(x).relu()))
class SublayerConnection(nn.Module):
    def forward(self, x, sublayer):
        return x + self.dropout(sublayer(self.norm(x)))

Module mode and autograd recording are separate controls. model.eval() changes the behavior of modules such as dropout; it does not disable gradient recording. Conversely, torch.no_grad() or inference mode suppresses selected autograd work but does not switch dropout modules to evaluation behavior. The practical rule is:

model.train():
    dropout active
    combine with the declared autograd/update context for training

model.eval():
    dropout disabled
    combine with an explicit no-grad/inference context for ordinary validation
    prohibit parameter/buffer/progress mutation except declared metric state
    restore the prior module mode and RNG/progress state after evaluation

If validation is run with model.train(), dropout remains active and validation loss becomes noisy. If training is run with model.eval(), dropout is disabled and the training behavior no longer matches the intended configuration.

An evaluation claim requires separate receipts for module mode, gradient suppression, RNG isolation, data/progress isolation, absence of model and optimizer-state mutation, metric-state ownership, and restoration of the prior training mode. This section states those requirements; it is not evidence that an evaluation job executed.


0x0A What Is Actually Parallel?

Now we can answer the title precisely.

The model is not generating the output sequence in parallel. The objective is still:

p(y | x) = product_t p(y_t | y_<t, x)

The parallel part is that the training code can represent all gold prefixes in one target matrix:

tgt:
    [B, T]

and all next-token labels in one label matrix:

tgt_y:
    [B, T]

The mask enforces the causal rule row by row:

row t can see only target keys <= t

Therefore these operations can run as tensor operations over batch and sequence dimensions:

embedding lookup for all source and target tokens
Q/K/V projections for all visible target rows
masked attention score construction
attention-value aggregation
position-wise FFN for all target rows
generator projection for all target rows
loss over all six-term eligible, head-admitted target-copy rows

The key phrase should be:

matrix-parallel training over known gold prefixes

not:

non-autoregressive training objective

During inference, future target tokens do not exist. The decoder input is built from generated tokens, so the next prefix cannot be constructed until the current token is selected. M3.10 opens the inference loop, last-position selection, EOS handling, padding-side bugs, and KV cache. M3.9 only needs the contrast:

training:
    gold target sequence exists -> build tgt/tgt_y matrix

inference:
    future target sequence does not exist -> generate one token at a time

Training parallelism has several independent axes

“Distributed training” can mean different ownership transformations:

ParallelismWhat is partitionedCore communicationSemantic invariant
data parallelexamples/tokensgradient all-reduce or reduce-scatterupdate equals declared global objective
tensor parallelmatrix/head/intermediate dimensionsall-reduce, all-gather, reduce-scattersharded operator equals dense operator
pipeline parallelconsecutive layer rangesactivation/gradient sends between stagesmicrobatch schedule preserves forward/backward dependencies
sequence/context paralleltoken rows or context blocksnorm/attention-specific exchangevisibility and global reductions remain correct
expert parallelMoE expertstoken all-to-allrouting and combine preserve token ownership

These axes can be composed. A 3D layout might use data-parallel replicas of a tensor/pipeline-parallel model. World size alone does not explain the update.

Data-parallel objective equivalence

Rank owns token-sum loss and valid-token count . The desired global token mean is

If a framework averages gradients across ranks after each rank backpropagates , each replica holds . Multiplying by recovers the global token-mean gradient. Backpropagating local means and then averaging instead gives each rank equal weight regardless of token count.

That one sentence hides the most important distributed-loss derivation in this article. Let

be the gradient of rank ‘s token-sum objective. The gradient of the declared global token mean is

Under the same bounded default-reducer profile (fixed initial world size, no custom communication hook, no join divisor change, and compatible parameter use), DistributedDataParallel averages corresponding gradient buckets, so after completed reduction each replica owns

Therefore the exact conversion from DDP’s averaged token-sum gradient to the global token-mean gradient is

Renderer-safe accounting form:

D_global = all_reduce_sum(N_r)
g_ddp_avg = (1 / world_size) * sum_r grad(S_r)
g_global_token_mean = g_ddp_avg * world_size / D_global

The factor of is not cosmetic. DDP already divided the summed bucket by world size; the global objective divides by valid tokens instead. With and , averaging S_0 / 2 and S_1 / 8 gives each rank half of the update even though rank 1 owns four times as many supervised rows.

There are two correct implementation families:

  1. Backward the rank-local token sum, let DDP average gradients, then multiply gradients by at the update boundary.
  2. Compute a globally weighted local mean before backward, multiplying rank ‘s local mean by . DDP’s later world-size average then produces the same global token mean.

The first form composes more cleanly with gradient accumulation because numerators and denominators remain separate until the transaction commits.

DDP accumulation is a synchronization schedule

Gradient accumulation under DDP is not merely a division by accum_steps. For the first microbatches, no_sync() prevents gradient-bucket collectives; the final microbatch executes a normal forward/backward and reduces the whole accumulated bucket. The documented context must enclose the forward as well as backward, because DDP prepares reduction state during forward.

The following is profile-specific semantic pseudocode. It chooses the unnormalized numerator-gradient implementation, so R / D is its one normalization. If an upstream L07 interface already exports the normalized logical gradient, physical projection must reconcile against that receipt and must not divide by D, R, ntokens, or accum_steps again.

optimizer.zero_grad(set_to_none=True)
window_tokens_local = 0

for micro_idx, batch in enumerate(loader):
    closes_window = ((micro_idx + 1) % accum_steps == 0)
    closes_epoch = (micro_idx + 1 == len(loader))
    should_sync = closes_window or closes_epoch

    sync_context = nullcontext() if should_sync else model.no_sync()
    with sync_context:                         # encloses forward + backward
        token_sum, valid_tokens = forward_token_sum(model, batch)
        scaler.scale(token_sum).backward()     # fixed scale for this window
        window_tokens_local += valid_tokens

    if should_sync:
        gradient_completion = wait_for_declared_gradient_collectives()
        D = wait(all_reduce_sum(window_tokens_local))
        scaler.unscale_(optimizer)             # exactly once after accumulation
        scale_gradients_(model, world_size / max(D, 1))
        finite = wait(closed_owner_roster_finite_agreement())
        if finite and D > 0:
            clip_global_logical_norm_(model, max_norm)
            scaler.step(optimizer)
            scheduler.step()                  # declared successful-update clock
        if D > 0:
            scaler.update()                   # same attempted-update result on owners
        optimizer.zero_grad(set_to_none=True)
        verify_declared_buffers_cleared()
        window_tokens_local = 0

The placeholders above require the frozen rank/stage/storage roster, collective group and sequence, and explicit completion tokens; an asynchronous collective handle is not a completed value. They also require one alias/replica/shard owner map so a norm counts each logical parameter once. The final partial window must synchronize and update. Dropping it changes data coverage. A rank with zero supervised tokens must still participate in the same collectives; use a graph-connected zero token sum rather than skipping forward/backward on only that rank. Otherwise one rank waits in a collective that another rank never enters.

A graph-connected zero is only one ingredient: the zero-work branch must use the same parameter-use graph, bucket sequence, unused-parameter policy, and collective participation as the other ranks. It is not by itself proof of bucket parity or safe behavior for every DDP configuration.

DDP does not shard input batches. The sampler or input pipeline owns the example partition, padding policy, and exact-resume cursor. DDP owns parameter replication, gradient hooks, buckets, and reductions. Confusing those two owners is a common source of duplicated examples and false effective-batch claims.

Global clipping over logical parameters

Global-norm clipping is another distributed reduction, not a rank-local postscript. The unique-owner map must be authenticated before the norm begins, and the squared-norm collective must complete before its result is consumed. For a set of uniquely owned gradient shards ,

Every shard is multiplied by the same . Fully replicated DDP parameters must be counted once logically, not once per replica; ZeRO/FSDP or tensor-parallel shards are counted on their unique owners and then reduced. Clipping each rank or tensor independently implements a different optimizer.

unscale AMP gradients
-> convert DDP average to declared global loss normalization
-> all-reduce unique-shard squared norms
-> apply one clipping coefficient
-> optimizer step

This order matters because clipping is nonlinear. Loss normalization and unscaling are linear rescalings; clipping before either observes the wrong norm.

Ownership ledger across parallel methods

MethodPersistent ownershipActivation ownershipGradient/update contract
DDPfull parameters and optimizer state per rankrank-local examplesaverage gradient buckets, then recover declared global reduction
tensor parallelmatrix/head/intermediate shardspartial operator outputscollectives reconstruct the dense operator and logical parameter gradient
sequence parallelusually token-row activation shardsdisjoint token rows for selected operatorsshared norm/bias gradients still reduce across all token rows
context parallelcontext blocks plus attention communication statedisjoint long-context regionsattention visibility and backward exchange must equal the unsharded graph
pipeline parallelconsecutive layer ranges per stagestage-local microbatch activationspoint-to-point forward/backward plus one coherent weight-version policy
ZeRO-1replicated parameters/gradients, optimizer states partitionedrank-local examplesupdate shards then make updated parameters coherent
ZeRO-2replicated parameters, gradients and optimizer states partitionedrank-local examplesreduce-scatter logical gradients before sharded update
ZeRO-3 / FSDP full shardparameters, gradients, and optimizer states partitionedtransient gathered parameters plus local activationsgather for compute, reduce-scatter for backward, checkpoint by logical names

Each row requires its own versioned topology schema: ranks/groups, logical owners, aliases, shard coordinates, parameter and activation placement, collective order/completion, zero-work behavior, optimizer/scaler ownership, and checkpoint mapping. The rows cannot be combined into one universal DDP/ FSDP/ZeRO/TP/SP/CP/PP recipe. These are accounting contracts, not universal implementation diagrams. Collective placement, bucketing, prefetch, and overlap differ by runtime. M4 owns the physical communication and kernel cost; M3.9 owns whether the distributed execution still represents the same training objective.

Pipeline schedule and stale state boundaries

Pipeline parallelism splits layers into stages and sends microbatch activations forward and gradients backward. A simple GPipe schedule fills the pipeline with forward microbatches, then drains backward; 1F1B schedules interleave one forward and one backward after warmup to reduce activation residency.

The optimizer normally updates only after all pipeline stages have completed the effective batch. Updating one stage early would make later backward passes combine activations from one parameter version with weights from another unless the algorithm explicitly supports weight stashing.

GPipe’s fill-then-drain schedule and a synchronous 1F1B schedule can represent the same effective-batch objective while exposing different activation peaks and bubbles. PipeDream-style asynchronous schedules are a different contract: they may require weight stashing so a backward microbatch uses the parameter version that produced its forward activations. Do not call these schedules equivalent merely because their tensor shapes match.

The last stage usually owns token-loss numerators and denominators. The global denominator and finite/skip decision must reach every stage that participates in the update. Otherwise different stages can step different versions of one logical model.

Pipeline checkpointing must preserve:

stage-to-layer assignment
parameter shards
optimizer shards and step counters
microbatch/accumulation progress
data sampler position
RNG states used by dropout on each stage

Tensor and sequence parallel training state

Tensor-parallel layers can produce partial gradients that require collective reduction before an optimizer sees the logical parameter gradient. Sequence-parallel norms can process different token rows independently, but norm-parameter gradients sum over all rows and still require cross-rank reduction.

Activation checkpointing is orthogonal: it discards selected forward activations and recomputes them during backward. With dropout, deterministic recomputation requires restoring or preserving RNG state so the same mask is regenerated. A different mask means backward is no longer the derivative of the original forward sample.

Topology metadata is therefore part of the training state. A topology-changing resume must map logical parameter names to new shards, reconstruct optimizer moments, restore or deliberately rebase RNG streams, and state whether the sampler cursor is preserved. Loading whatever tensors happen to have matching local filenames is not a resharding algorithm.

Historical compressed distributed-objective diagram; the caption separates validation and projection lanes
Bounded two-replica illustration—not a universal topology or fault-atomic transaction. Default DDP averaging applies only under the frozen reducer assumptions stated above. Objective reconstruction, collective completion, finite agreement, unique-owner norm, logical candidate selection, physical projection, buffer clear, and durability are distinct receipts.
No-image fallback: Arrow A checks that the completed physical gradient maps to the accepted normalized logical gradient and binary32 clip reference, then stops. Arrow B projects an already selected immutable logical update candidate into the declared physical owners. It does not rerun the optimizer from Arrow A, and neither arrow proves durable checkpoint publication.

The diagram is deliberately about semantic state. It does not prescribe a particular bucket size, collective algorithm, or overlap schedule.

For audit purposes, keep two evidence arrows separate. Arrow A starts from the accepted logical gradient and checks binary32 readiness, norm, and clipped vector against the componentwise reference, then stops. Arrow B starts from the already selected immutable logical update candidate and projects its core state to the bounded physical topology. Arrow B does not recompute an optimizer transition from Arrow A’s clipped vector. Neither arrow proves durable publication, and a globally agreed finite flag does not make multi-rank writes fault-atomic.


0x0AA Checkpoint, restart, evaluation, and reproducibility

A weight-only checkpoint is enough for inference and insufficient for an exact training resume. A restartable training snapshot typically needs:

model parameters and buffers
optimizer moments, master weights, and step counters
learning-rate scheduler state
mixed-precision scaler state
global update and consumed-token counters
data sampler/shuffle state and dataset version
Python, NumPy, CPU, and accelerator RNG states
distributed topology and sharding metadata
gradient-accumulation remainder, if mid-window checkpoints are allowed
code/config/tokenizer revisions

That list is only an inventory. A closed checkpoint schema binds every state class to: logical owner; schema/version; stable parameter and alias IDs; exact shape/dtype/layout or structured type; presence state (PRESENT, ABSENT, FROZEN, or NOT_APPLICABLE); canonical payload serialization and digest; source update/capture ID; supported migration policy; and immutable training- manifest digest. A digest without its authenticated payload and classification is not state. Completeness means every required schema row is present or has an allowed explicit absence—not merely that a directory contains plausible files.

Exact resume versus statistically equivalent resume

An exact-resume candidate aims to reproduce the next batch, dropout masks, collectives, and update under one frozen comparator stack: code and dependency revisions, compiler flags, device/hardware class, topology/rank map, kernels and determinism controls, input addresses, and canonical state schema. Equal saved state is not proof of bitwise framework replay. The comparator must separately declare byte-exact fields and numeric fields, dtype, shape, NaN/signed-zero policy, absolute/relative or ULP tolerance, reduction domain, and fresh-process replay steps. A statistically equivalent resume may restart at an epoch boundary with a new shuffle/RNG stream while preserving a declared training distribution. Both can be acceptable, but the evidence must say which was tested.

Even an exact state snapshot may diverge across hardware or library versions because collective order, fused kernels, floating-point associativity, or nondeterministic operators change. Reproducibility is therefore a bounded contract, not a binary property.

Evaluation mode is more than model.eval()

Validation should define:

dropout and stochastic-depth disabled
no parameter updates or gradient accumulation
same tokenization and masking semantics as the evaluated objective
loss numerator and denominator reduced globally
evaluation datasets and versions frozen
generation policy frozen for generative metrics

Perplexity is

Averaging per-batch perplexities is wrong because exponential is nonlinear and batch token counts differ. Reduce total negative log-likelihood and total valid tokens first, then exponentiate once.

Checkpoint validation sequence

There is no framework-independent “load model, load optimizer, then load scheduler” recipe. Before trusting a resume artifact, select and version the framework-specific construction/load order and then perform these semantic phases:

1. authenticate the manifest, schemas, payload digests, source stack, and
   supported source/target topology profile before attaching state;
2. construct model, optimizer, scheduler, and scaler in the pinned API order;
   record that order because scheduler/optimizer initialization and loading can
   interact;
3. resolve canonical parameter IDs and alias groups, then load every model and
   optimizer state row by that passport—not by data pointer, list position, or
   unverified parameter names;
4. restore scheduler/scaler/update ledgers and the complete producer state:
   sampler algorithm/cursor, worker derivations, prefetch and in-flight queues,
   open packer remainder, produced/issued/consumed/committed addresses, and RNG;
5. in a fresh process, replay the frozen batch and compare each declared
   exact or tolerance domain for losses, logits, gradients, parameters, and
   optimizer/scaler/scheduler/producer state;
6. execute one candidate update, verify all-old/all-new state and clear
   receipts, save, reload again, and repeat the declared comparator.

In particular, the base PyTorch optimizer loader does not by itself authenticate that parameter names carry the intended semantics; the external logical passport must do that. Checksums require canonical serialization and complete coverage, while numerical comparators require explicit domains and tolerances. Neither an unspecified checksum subset nor an unspecified tolerance establishes resume equivalence.

Pinned implementation reconciliation

Paper equations and library APIs sit at different abstraction levels. The evidence bundle pins exact source revisions so the article can say which layer owns each transformation:

Pinned implementationObserved source contractArticle interpretation
Annotated Transformer debc9fdBatch constructs tgt[:, :-1], tgt[:, 1:], padding + subsequent mask, and ntokensdirect source-to-tensor baseline for externally shifted encoder-decoder training
Annotated Transformer run_epochcalls backward each batch, steps on i % accum_iter == 0, and calls scheduler.step() each training iterationhistorical pedagogical loop, not the article’s denominator-safe or successful-update reference transaction
PyTorch DDP 9dc95feno_sync() accumulates module gradients and warns that forward must be inside the contextconfirms the synchronization-schedule contract used above
PyTorch GradScaler 9dc95feunscale_ may be called for inspection/clipping, exactly once per optimizer after all gradients accumulate; step skips on non-finite gradientsconfirms scaler state and call ordering, but multi-rank agreement remains the distributed program’s responsibility
PyTorch checkpoint 9dc95feexposes RNG preservation and distinct reentrant/non-reentrant mechanisms; asks callers to pass use_reentrant explicitlyconfirms that recomputation policy and RNG replay are versioned runtime state
Transformers v5.13.1 causal lossupcasts logits to float, pads labels on the right, shifts labels left internally, flattens rows, and honors ignore_indexconfirms that callers must know whether shift ownership is external or internal
Transformers v5.13.1 data collatorsconstruct padded tensors and objective-specific labels/corruptionconfirms that a collator can own objective compilation, not merely rectangular padding
TRL 38b4b81 SFT pathbuilds completion/assistant masks, applies ignore labels, manages packing/padding-free constraints, and passes labels to model lossconfirms that chat serialization, target ownership, packing, and model-internal shift cross library boundaries

The exact revisions and SHA-256 hashes are recorded under docs/numinens-evidence/m3-9-training-execution/sources/. These snapshots are evidence, not vendored runtime dependencies.

One especially important correction follows from the historical loop:

# Pinned pedagogical source behavior:
if i % accum_iter == 0:
    optimizer.step()      # true for i == 0
scheduler.step()          # every training iteration

That code is useful for reconstructing the source article but should not be generalized into an exact accumulation recipe. A production transaction closes on (i + 1) % accum_iter == 0 or the final remainder, normalizes by the declared denominator, and advances an update-clock scheduler only after a committed update.

A checkpoint is an atomic training transaction

Saving a set of rank-local files does not prove that all files describe the same model version. A robust checkpoint protocol has a quiescent logical boundary, a capture-consistency rule, and a backend-specific publication phase:

1. quiesce at a declared update boundary (or record every mid-window buffer)
2. assign checkpoint_id and committed_update_id
3. capture the complete produced/issued/consumed/committed input frontier,
   worker/prefetch queues, open packer remainder, parameters, optimizer,
   scaler, scheduler, RNG, and pending-window state under one consistency rule
4. each owner writes those typed payloads and shards
   into a private temporary namespace
5. write logical-name/shard metadata, dtypes, shapes, aliases, topology,
   source revisions, and checksums
6. wait for upload/write completion and the selected durability condition;
   validate completeness and cross-rank agreement
7. load a fresh-process probe view or replay the declared validation batch
8. conditionally publish the immutable manifest/pointer as COMMITTED using
   the expected prior pointer/version and a fenced writer identity

Readers must ignore an uncommitted temporary namespace. In object storage, publication may mean writing immutable content-addressed objects and then conditionally updating one small manifest pointer; it need not mean renaming a POSIX directory. That pointer operation does not make preceding shard writes atomic or durable. The storage profile must define upload completion, read-after-write/list behavior, durability acknowledgement, concurrent-writer fencing, pointer compare-and-swap preconditions, failure linearization, retry idempotence, and orphan cleanup. Until those receipts exist, “atomic publication” is a logical requirement rather than an observed backend fact.

Mid-window checkpoints are possible but materially larger in semantic scope. They must preserve accumulated gradient shards, current loss scale, pending numerator/denominator, no_sync position, pipeline microbatch state, sampler algorithm/cursor, worker derivations, prefetch and in-flight queues, open packer remainder, produced/issued/consumed/committed addresses, and RNG counters under one capture boundary. Otherwise resume from the previous committed update and replay the window according to an explicit data-replay policy.

A topology-changing resume is a separate migration profile. It needs a supported source/target topology pair, canonical fully qualified parameter and alias map, shard-coordinate transformation, optimizer-state reshard plan, precision and RNG migration policy, producer repartitioning rule, and declared equivalence comparator. The two-replica examples in this article do not prove arbitrary resharding.

Evaluation is another immutable manifest

model.eval() changes stochastic module behavior but does not define an evaluation. A comparison manifest should freeze:

SurfaceRequired declaration
artifactcheckpoint/release ID, tokenizer/template, model dtype, adapter state
datadataset revision, split IDs, filtering/decontamination policy, preprocessing hash
objective metricsexact labels, loss weights, denominator, distributed aggregation, bootstrap/unit of analysis
generation metricsprompt serialization, decoding policy from M3.8, max tokens, stop rules, seeds, judge/tool revisions
runtimecode/container, hardware class, batching and precision policy
acceptancethresholds, confidence rule, regression budget, required safety/robustness suites

Execution must additionally record independent receipts for: the prior and selected module modes; explicit gradient-suppression context; RNG snapshot and isolation; immutable data/progress cursor; absence of parameter, buffer, optimizer, scaler, scheduler, and training-metric mutation; metric accumulator ownership; collective completion; and restoration of the prior training mode and state. This article specifies that socket but does not claim those receipts were produced by a real evaluation run.

Validation loss should normally reuse the training objective compiler with stochastic corruption either frozen by example ID or averaged under a declared sampling policy. A generative benchmark can measure a different deployment objective; its result must not be mislabeled as validation perplexity.

Data decontamination is not solved by naming a public split or by documenting a policy. Record the accessible training-corpus snapshot, benchmark revision, normalization/matcher version, match unit and granularity, threshold, candidate and removed IDs, and report digest. When the training corpus or required comparison bytes are unavailable, the honest disposition is UNKNOWN or UNCHECKABLE; policy prose is not proof of exclusion. Otherwise the same benchmark name can denote a different evidence set after upstream or preprocessing changes.

Release, canary, and rollback: requirements, not execution

A training checkpoint is restartable state; it is not yet a serving artifact or a release. M3.9 owns only the logical handoff requirements. M8.9 owns physical artifact conversion, loading, and kernel/runtime admission. M3.10 owns the single-request prefill/decode/stop/KV semantics consumed by that admission. M5 owns live scheduling, sessions, and cache lifecycle. M8.10 owns deployment evaluation, canary, promotion, monitoring, rollback, and release.

Before M8.9 can evaluate a converted candidate, a separate immutable candidate passport must bind:

source checkpoint manifest and committed update
converter implementation/revision and conversion profile
artifact format/schema and every file digest
model/config/adapter identities and parameter/alias map
tokenizer, template, vocabulary, and special-token policy
generation configuration and M3.10 inference-contract revision
storage/compute dtype, quantization, layout, and shard map
intended loader, kernels, runtime/container, hardware, and topology tuple

The following arrows are required evidence sockets, not claims that the work occurred:

M3.9 checkpoint + logical candidate requirements
    -> M8.9 conversion and physical admission evidence [required, not supplied]
    -> M3.10 request-state and decoding equivalence evidence [required, not supplied]
    -> M8.10 frozen offline evaluation [required, not supplied]
    -> M5/M8.10 bounded live lifecycle and release [required, not supplied]

An offline or canary manifest must close the candidate and control identities, dataset/request cohort, serialization, comparator domain and tolerance, exposure/window, metric numerators and denominators, attribution, thresholds, approval authority, and evidence digests. Merely naming “same probes,” frozen suites, or signals such as NaN/Inf rate, latency, output length, stop behavior, or safety regressions does not establish any result.

Likewise, moving a release pointer describes only logical routing for new traffic. Stateful rollback additionally requires M3.10/M5/M8.10 policies for in-flight generations, sessions, KV and prefix caches, drain versus cancel, readiness/capacity, fencing and idempotence, candidate quarantine, and forensic retention. It does not restore a training cursor. Training rollback separately owns optimizer/scaler/sampler/RNG state and the replay/skip/reweight policy for consumed examples.

Historical compressed checkpoint-to-release requirement diagram; the adjacent caption supplies the authoritative owner and evidence boundaries
Historical compressed requirements diagram—not an executed lifecycle. Read its checkpoint lane as M3.9/L11 logical input, its candidate lane as requirements handed to M8.9, and its canary/promotion/rollback lane as missing M8.10 evidence. The image does not prove conversion, inference equivalence, offline evaluation, traffic exposure, cache/session handling, promotion, rollback, or release.
No-image fallback: a committed M3.9 checkpoint stops at a logical handoff. M8.9 must create and admit a passport-bound physical artifact; M3.10 must validate single-request state semantics; M5 must define live request/session/cache behavior; and M8.10 must produce offline, canary, promotion, monitoring, rollback, and release evidence. Every downstream state remains “required, not supplied” here.

0x0B Exposure Bias as a Training-Procedure Limitation

Teacher forcing gives efficient and stable supervision, but it creates a mismatch. During training, the decoder prefix comes from the gold target. During inference, the prefix comes from the model’s own previous outputs.

Suppose the gold target is:

<bos> I ate an apple <eos>

During training, the row that predicts an is conditioned on:

<bos> I ate

If inference produces:

<bos> I like

then the next decoder state is conditioned on a prefix that may not resemble the supervised prefix distribution. One wrong token changes the future input distribution. This is usually called exposure bias.

Scheduled sampling and related methods try to reduce this gap by sometimes feeding generated outputs during training, but that is outside the standard M3.9 teacher-forced mechanism. The baseline source-series path is:

gold previous tokens + causal mask + next-token labels

The caveat matters, but it should not replace the code path.

Training-procedure variants change data and visibility

Objective/procedureModel inputVisibilityLabelsMain change
causal LMtoken prefixcausalnext tokenstandard decoder pretraining
encoder-decoder teacher forcingsource + shifted targetbidirectional source, causal target, cross-attentionnext target tokenconditional generation
prefix LMprefix + continuationbidirectional within prefix, causal continuationoften continuation rowshybrid attention mask
masked LMcorrupted sequenceusually bidirectionalselected masked/original tokensdenoising, not left-to-right generation
span corruptionsentinel-marked source + target spansencoder/decoder contractmissing spansstructured denoising
scheduled samplingmixture of gold/generated previous tokensautoregressivenext target tokenprefix distribution becomes model-dependent
sequence-level distillationteacher-generated targets or distributionstask-dependentteacher sequence/logitschanges target distribution

That compact table is only an index. Two procedures that both “change the input” can have different factorization, estimator, runtime, and checkpoint state. A useful comparison must carry the complete contract:

Paper contractInput constructionVisibility / executionTarget ownerAdded mutable stateWhat it does not guarantee
teacher-forced causal or encoder-decoder MLEgold prefix shifted rightall gold-prefix rows parallel under causal visibilityevery declared next-token rowordinary data/RNG/optimizer statebehavior on prefixes generated by the model
Scheduled Samplingchoose gold or sampled previous token at each stepinput construction becomes sequential and model-dependentnext gold tokenschedule clock, sampling RNG, decoding policya consistent maximum-likelihood estimator
Professor Forcingrun teacher-forced and free-running trajectoriestwo recurrent execution modes plus discriminatorMLE token rows + trajectory-domain objectivediscriminator, optimizer(s), weighting, free-run policyidentical token distributions merely from similar hidden dynamics
BERT MLMcorrupt selected positions, retain original targetsbidirectional valid-token visibilityselected original token positionscorruption RNG and replacement policyleft-to-right generation training
generalized prefix LMcompile segment-dependent block visibilityparallel over the materialized sequencedeclared shifted continuation rowsobjective mode and segment boundariespaper-exact UniLM selected-cloze ownership merely because the mask is similar
XLNet permutation LMretain natural token positions, sample factorization orderephemeral content/query activations use different masksselected positions in permutation orderarchitecture, mask/target/permutation program, memory policy, and RNG/replay identityimplementation by physically shuffling one token tensor
MASSmask a contiguous source fragmentencoder context plus autoregressive masked-fragment decodermasked fragmentspan sampler and boundary policygeneric arbitrary denoising equivalence
T5 span corruptionreplace each input span by a sentinel; target concatenates sentinel + removed spanbidirectional encoder, causal decodercompact removed-span target streamspan RNG, sentinel allocation, packing policysame token weighting as full-sequence reconstruction
BART denoisingapply a declared noising function to a documentbidirectional encoder, autoregressive decoderoriginal document tokensnoising RNG and transform parametersone canonical corruption distribution
MIXER / sequence-level policy trainingroll out model actions and score completed sequencesequential sampled trajectoryreturn/advantage rather than only token likelihoodreward, baseline, rollout policy, curriculumlow-variance credit assignment or MLE equivalence

Sequence-level distillation is intentionally absent from the detailed rows. M6.10 owns teacher/student target construction, temperature, cached logits, and distillation release contracts. Here it appears only as a boundary: replacing gold targets with teacher targets changes the training dataset/objective.

Scheduled Sampling changes the estimator and the execution graph

At step , the original procedure samples a Bernoulli decision

from a schedule indexed by training clock , then chooses the next decoder input token

This is a token-ID branch, not a convex combination of embeddings. Once a model-produced token is selected, the next input depends on the preceding forward result. The full target matrix no longer specifies every decoder input in advance, so the ordinary teacher-forced one-pass execution is lost for that sampled trajectory.

prefix = [bos]
for t, gold_target in enumerate(target):
    logits = model.next(prefix, source)
    loss += cross_entropy(logits, gold_target)
    sampled = sample(logits, policy=sampling_policy, rng=rollout_rng)
    use_gold = bernoulli(epsilon(update_clock), rng=rollout_rng)
    prefix.append(gold_target if use_gold else sampled)

The original paper explores linear, exponential, and inverse-sigmoid schedule families. A reproducible procedure must freeze whether the model token is sampled or selected by argmax, the Bernoulli-coin scope (token, sequence, microbatch, or another unit), schedule family and clock, decoding temperature and constraints, semantic RNG addresses, model revision used for selection, and treatment of the discrete choice in differentiation. Those fields—not the method name—are procedure identity. Later theoretical criticism shows why exposure to generated prefixes should not be summarized as “unbiased MLE with better inputs”: the training prefix distribution depends on the model and the discrete sampled path.

Professor Forcing adds a second model and trajectory objective

Professor Forcing retains a teacher-forced likelihood path and compares its hidden-state trajectory with a free-running trajectory. The following is only a schematic generator-side summary,

where discriminator learns to distinguish trajectories from the two execution modes and generator receives an adversarial signal encouraging their dynamics to be difficult to distinguish. A complete procedure separately specifies the discriminator loss, teacher-forced generator cost, free-running generator cost, any optional teacher-forced adversarial cost, behavior extractor and masks, parameter ownership, optimizer(s), alternating update order, coefficients, rollout policy, and RNG. The one displayed scalar must not be used to erase those computations or their distinct state transitions.

That adds at least:

teacher-forced rollout
free-running sampled rollout
trajectory representation and masking
discriminator parameters and optimizer
generator/discriminator update schedule
adversarial coefficient and RNG state

It is therefore not a different causal mask and not a drop-in replacement for the one-pass loss function.

Corruption objectives compile different source and target sequences

BERT first samples a selected set (reported as 15% of input tokens in the paper). For selected tokens, its source-reported 80/10/10 policy uses a mask token, a random token, or the unchanged token respectively. All selected positions still own reconstruction loss:

The unchanged 10% is not loss masking; it is an input-corruption branch.

T5 span corruption changes the tensor topology more radically. For

original:  A B C D E F
spans:       [B C]  [E]

one compiled pair is

encoder input: A <s0> D <s1> F
decoder target: <s0> B C <s1> E <s2> <eos>

Each sentinel appears once in the corrupted input and introduces its removed span in the target; the final <s2> is a target-only sentinel marking the end of the last removed span. Any following EOS is an independent tokenizer/ sequence policy, not a replacement for that sentinel. The target is shorter and has different row ownership from full-document reconstruction. MASS instead masks a contiguous fragment and trains the decoder to predict that fragment; BART allows a family of noising functions and reconstructs the original document. “Denoising objective” does not identify one source-to-target compiler.

Each corruption family therefore needs its own compiler passport: selection law, eligible special tokens, replacement/random-token distribution, sentinel allocation order, collision and sentinel-exhaustion behavior, document and pack boundaries, exact source/target construction, target ownership, and semantic RNG address space. BERT’s selection/replacement policy, MASS’s fragment policy, T5’s sentinel program, and BART’s noising functions cannot be filled in by a generic span_corruption method name.

Sequence-level objectives replace local credit with a trajectory return

Maximum likelihood decomposes a supervised sequence into token losses. A sequence-level metric instead induces an expected objective

For sampled discrete trajectories, a score-function gradient has the form

For this displayed score-function estimator, baseline may depend on state or other action-independent information but must not depend on the current sampled action; it is treated stop-gradient in this term. The equation is written for reward ascent on . A minimized loss uses the corresponding negative sign. This is the REINFORCE family described by Williams (1992), not a pathwise gradient through discrete token IDs. MIXER introduces a curriculum from cross-entropy training toward sequence-level REINFORCE. This changes label ownership, execution, variance, and evaluation coupling; it is not “teacher forcing with a sequence metric appended.”

One objective compiler interface

The alternatives can share a requirements interface without pretending they are the same algorithm. The pseudocode below is an author sketch; it was not executed as a framework/compiler and a finite fixture that agrees with itself would not prove production training:

@dataclass
class CompiledTrainingExample:
    model_inputs: dict[str, Tensor]
    attention_visibility: Tensor | StructuredMask
    labels: Tensor
    loss_weights: Tensor
    factorization_metadata: dict
    stochastic_state: dict

def compile_training_example(example, objective_manifest, rng):
    if objective_manifest.family == "causal_lm":
        return compile_shifted_causal(example, objective_manifest)
    if objective_manifest.family == "masked_lm":
        return compile_masked_reconstruction(example, objective_manifest, rng)
    if objective_manifest.family == "span_corruption":
        return compile_sentinel_spans(example, objective_manifest, rng)
    if objective_manifest.family == "permutation_lm":
        return compile_two_stream_permutation(example, objective_manifest, rng)
    raise UnsupportedObjective(objective_manifest.family)

Compilation must occur before collation or provide structured metadata that the collator can combine without losing sample boundaries. The manifest, not a generic labels != -100 convention, owns factorization, visibility, targets, weights, and stochastic transforms.

Historical high-level comparison of training procedures; the caption supplies the missing requirements boundary
Requirements-only procedure comparison. The visual is a high-level index, not an implemented compiler. Scheduled Sampling requires a frozen coin scope, schedule, selection/decoding policy, model revision, RNG, and discrete-gradient rule. Professor Forcing requires separate teacher/free generator paths, discriminator costs, behavior extractor, parameters, optimizers, update order, coefficients, and RNG.
No-image fallback: teacher forcing materializes gold prefixes; Scheduled Sampling sequentially chooses gold or a model token under a versioned stochastic policy; Professor Forcing additionally executes teacher-forced and free-running behavior paths plus a discriminator; each corruption objective uses its own source/target compiler. No rollout, discriminator, framework kernel, or production training was executed by this article's author fixtures.

Objective topology has two independent axes

The table above mixes several training procedures, but four common language-model objectives can be compared more precisely by separating two tensors:

attention visibility M_attn[i,j]:
    may prediction state i read token state j?

loss ownership ell[i]:
    does prediction row i contribute to the objective?

The first tensor changes the information available to a representation. The second selects which predictions are optimized. Neither determines the other.

For a decoder-only causal LM whose row i predicts token x_(i+1), the usual contract is

A masked LM instead corrupts a set of token positions to form . Its encoder normally has bidirectional visibility over valid input positions, while the reconstruction loss belongs only to selected corrupted positions:

The target is the original , not the visible corrupted token . BERT’s 80/10/10 replacement policy changes how selected inputs are corrupted; it does not change which selected positions own reconstruction loss.

For a prefix boundary , a generalized prefix-LM objective can be expressed with this block mask:

Prefix queries read the complete prefix, continuation queries read the prefix and earlier continuation, and loss usually belongs to continuation targets. Under an externally shifted next-token implementation, that ownership is ; an API that shifts labels internally must encode the same target-token policy before its internal shift.

This visibility matrix can resemble a UniLM sequence-to-sequence mask, but the ordinary shifted-continuation policy above is not paper-exact UniLM selected- cloze ownership. UniLM’s selected prediction positions and its segment/task program must be specified separately. Sharing a support matrix does not make the two target compilers identical.

Permutation language modeling changes a third object: the factorization order, not the natural order of the input sequence. For a permutation ,

That equation is still not a complete XLNet compiler passport. It additionally requires the permutation distribution and replay receipt, partial-prediction cutoff and target mapping, stream- and layer-specific masks, positional inputs, memory policy, and semantic RNG addresses. A single hidden state cannot both contain the target token for later predictions and hide that same content while predicting it. XLNet therefore uses two streams. At a factorization step , the content stream may incorporate , while the query stream used to predict receives the target position and earlier factorization context but not the target content:

content stream h(z_t): factorization context z_<=t, including x_(z_t)
query stream   g(z_t): factorization context z_<t + target position z_t
prediction:             softmax(W_out * g(z_t)) -> x_(z_t)

Actual XLNet masks are layer- and stream-specific, and training may predict only a target subsequence. The content/query rows shown here are ordinarily ephemeral activations, not persistent checkpoint “state.” Persistent identity covers the architecture, mask and target mapping, factorization program, memory policy, and RNG/replay contract unless a concrete implementation declares more. Treat the equations above as an objective requirement, not as permission to implement XLNet by shuffling tokens or replacing one causal mask with one ordinary binary matrix.

High-level objective topology comparison whose exact paper-specific qualifications are stated in the caption
Static objective topology, with paper-specific limits. Causal, masked, generalized prefix, and permutation profiles separate attention support from target ownership. The prefix row is not by itself paper-exact UniLM selected-cloze supervision; XLNet additionally requires its permutation distribution, target cutoff/map, stream-specific masks, positional and memory policy, and RNG/replay passport.
No-image fallback: causal LM uses earlier-or-equal support and shifted next-token targets; masked LM uses bidirectional valid-token support and selected original-token targets; a generalized prefix LM uses bidirectional prefix plus causal continuation support with a declared target program; XLNet factors over sampled orders and uses separate ephemeral content/query activations so target content is hidden on the prediction path.

The implementation audit is therefore three-dimensional:

assert attention_mask.shape_is_compatible_with(attention_logits)
assert loss_mask.shape_is_compatible_with(target_rows)
assert objective_factorization_matches_checkpoint_contract()

Shape agreement checks only the tensor interface. The matrix entries, target shift, corruption policy, factorization order, and stream ownership determine the objective actually being trained.

Scheduled sampling samples a Bernoulli choice between gold and model-produced previous input. It reduces pure teacher-forcing exposure but introduces a discrete policy and a training distribution that depends on current model errors. The original paper proposes schedules; later analysis points out statistical inconsistency concerns. It is not a free correction to standard maximum likelihood.

Professor Forcing instead adversarially encourages similar hidden-state dynamics under teacher-forced and free-running modes. That adds a discriminator and an additional objective rather than changing only the target mask.

Prefix LM illustrates why causal and bidirectional are not model-wide labels. If prefix positions may see each other but continuation positions can only see the prefix and earlier continuation, the mask is block structured:

prefix queries -> all prefix keys
continuation queries -> all prefix keys + earlier continuation keys
prefix queries -> no continuation keys

Training code must generate the exact block mask and loss ownership expected by the checkpoint. Reusing a lower triangle everywhere changes the objective.


0x0C Shape-Correct Failure Lab

The most useful failures here are not only crashes. Many preserve shape and break the training semantics.

Target Shift Bugs

No shift:

self.tgt = tgt
self.tgt_y = tgt

The tensors are rectangular, but the model is no longer trained on next-token alignment. It may learn to predict the current token.

Reversed shift:

self.tgt = tgt[:, 1:]
self.tgt_y = tgt[:, :-1]

The shapes match the correct version, but the temporal direction is wrong.

Using labels as decoder input:

decoder_input = batch.tgt_y

tgt_y has the same shape as tgt, but it is one step ahead. Feeding it to the decoder leaks label alignment.

Dropping <eos> from labels means the model never learns when to stop. Keeping <eos> in the decoder input is not itself a bug: row 2 of the padded batch in 0x03 feeds <eos> with a <pad> label, and the packed stream in 0x04 feeds doc_A’s <eos> as an input (the stream’s final <eos>, after doc_B, is only a label). The bug is supervising an <eos> input row whose next target is undefined: a <pad> label that the criterion still counts (counting it in ntokens as well is the separate normalization bug under Padding and ntokens Bugs below), or, under independent-document packing, the next document’s <bos>.

Mask Bugs

Padding-only target mask:

tgt_mask = (tgt != pad).unsqueeze(-2)

This hides <pad>, but not future target positions. Teacher-forced training can leak answers.

Wrong mask length:

tgt_mask = subsequent_mask(full_tgt.size(-1))

The decoder input has length T_full - 1, but the mask was built for T_full.

Inverted triangle:

future visible, past hidden

The matrix still has shape [T, T], but the causal rule is reversed.

Mask after softmax:

softmax first, mask later

The masked positions already affected normalization.

Wrong axis broadcast:

[B, T, 1] instead of [B, 1, T]

Both may broadcast, but one hides query rows while the other hides key columns.

Generator and Loss Bugs

Treating out as vocabulary logits:

out: [B, T, d_model]

The criterion needs vocabulary-width rows:

generator(out): [B, T, V_tgt]

Applying generator twice can happen if code precomputes generator(out) and then passes that result into SimpleLossCompute, which calls self.generator(x) again.

Flattening wrong:

x.view(-1)

destroys the vocabulary axis. The source code keeps:

x.contiguous().view(-1, x.size(-1))

Using probabilities with a criterion that expects log-probabilities, or log-probabilities with a criterion that expects logits, can preserve shape while changing the loss meaning.

Padding and ntokens Bugs

Counting all positions:

norm = B * T

makes loss scale depend on padding ratio.

Counting tgt instead of tgt_y can count BOS or miss EOS depending on implementation. The source uses:

self.ntokens = (self.tgt_y != pad).data.sum()

If <pad> labels are included in the criterion, the model is rewarded for predicting padding. If source and target vocabularies have different pad IDs, using the source pad ID for target labels can silently poison the loss.

Label Smoothing Bugs

If smoothing mass is assigned to the PAD class, the model is trained to allocate probability to padding.

If rows whose target is PAD are not zeroed, artificial padded labels contribute to the loss.

If the denominator uses size - 1 instead of size - 2, the distribution may still sum plausibly but no longer matches the intended exclusion of gold and pad classes.

If scatter_ is applied along the wrong dimension, confidence can be written across rows instead of vocabulary columns.

Backward / Optimizer / Scheduler Bugs

Backpropagating the wrong value:

loss.backward()

is wrong if loss is the detached logging value. The training loop uses:

loss_node.backward()

Zeroing gradients before optimizer.step() erases the update. Forgetting optimizer.zero_grad(set_to_none=True) accumulates gradients unintentionally.

With gradient accumulation, changing accum_iter without thinking about loss scaling, optimizer step cadence, and scheduler step cadence changes training behavior. The source loop performs backward every batch, optimizer step on the accumulation condition, and scheduler step every training batch.

In DDP, dividing each replica’s accumulated gradient by its local non-padding token count is not a global token mean when ranks contain different token counts. Reduce the denominator globally and account for DDP’s default world-size averaging.

Using DDP accumulation without no_sync() preserves a plausible gradient but performs communication on every microbatch. Putting only backward, but not the corresponding forward, inside no_sync() also violates the documented DDP usage contract.

With AMP, unscaling before all microbatches have accumulated mixes later scaled gradients with earlier unscaled gradients. Unscale once at the update boundary, then normalize and clip before stepping.

Train / Eval Mode Bugs

Training with:

model.eval()

disables dropout. Validation with:

model.train()

keeps dropout active. Both bugs preserve shapes and produce real numbers, but the forward pass no longer means what the training or validation code says it means.

Training / Inference Boundary Bugs

Using:

out[:, -1]

to explain training is a category error. Training flattens all valid target positions and compares them with tgt_y. Last-position selection belongs to inference.

Saying “training is not autoregressive” is also wrong. The objective is autoregressive. The computation is parallel because gold prefixes are available and visibility is enforced by masks.

Packed-data bugs

Using one lower-triangular mask over concatenated documents leaks context across document boundaries. Resetting RoPE positions without segment masking does not prevent leakage; segment masking without the intended position reset changes positional geometry.

Supervising the token after one document’s EOS to predict the next document’s BOS creates a cross-document target pair. That may be intentional for one continuous stream or wrong for independent-document packing; the dataset contract must decide.

Treating loss-ignored prompt rows as attention-invisible prevents answers from conditioning on the prompt. Loss ownership and attention visibility are different tensors.

Objective and optimizer bugs

Averaging per-sequence means while claiming a token mean changes short/long sequence weighting. Averaging rank-local token means changes rank weighting when token counts differ.

Restoring model weights without Adam moments, optimizer step, scheduler step, or scaler state is a warm restart, not an exact resume. Stepping a scheduler when AMP skipped the optimizer update desynchronizes update count and learning rate.

Applying ordinary L2 regularization while claiming AdamW changes the adaptive update. Weight decay parameter groups must also exclude/include the intended norms, biases, embeddings, and tied parameters.

Clipping scaled gradients before AMP unscale uses the wrong norm. Clipping each tensor independently is not global-norm clipping.

Distributed and restart bugs

Using no_sync() around backward but not the corresponding forward violates the DDP context contract. Forgetting the final remainder update drops gradients at epoch end.

Recomputing checkpointed dropout under a different RNG state means backward differentiates a different stochastic forward. Saving only a global seed is insufficient when data, model, and pipeline ranks own independent streams.

Changing world size can alter data order, optimizer sharding, reduction order, microbatch count, and schedule units. A topology-changing resume needs an explicit reshard and equivalence policy.

Averaging batch perplexities instead of global negative log-likelihood produces a biased aggregate. Sum NLL and token counts first.

Publishing rank shards as they arrive can expose a checkpoint whose parameter, optimizer, and sampler files belong to different update IDs. Readers must follow one committed manifest, never directory completeness.

Loading a causal-LM checkpoint with externally pre-shifted labels into a model that shifts labels internally creates a two-position error while all flattened shapes remain valid. Shift ownership belongs in the model/loss interface manifest.

Treating the Annotated Transformer accumulation condition as a generic recipe steps on the first microbatch because i == 0 satisfies i % accum_iter == 0. The pinned code is historical evidence; the corrected transaction closes on (i + 1) % accum_iter == 0 or the final remainder.

Calling GradScaler.unscale_ twice between steps is not an idempotent safety check; the pinned implementation raises. Calling it before the accumulation window closes mixes scaled and unscaled gradients.

Evaluating a converted serving artifact only through the original training checkpoint misses export, dtype, tokenizer, and generation-policy regressions. Probe the artifact that will actually be released.

Deleting or overwriting the failed candidate during rollback destroys the forensic artifact and makes the release pointer non-auditable. Roll back by moving the active pointer to an immutable prior release.


0x0D Summary

The training path is now concrete:

collate_batch
    -> src [B,S], full_tgt [B,T_full]

Batch
    -> src, src_mask
    -> tgt = full_tgt[:, :-1]
    -> tgt_y = full_tgt[:, 1:]
    -> tgt_mask
    -> ntokens

run_epoch
    -> out = model.forward(src, tgt, src_mask, tgt_mask)

SimpleLossCompute
    -> generator(out)
    -> flatten predictions and labels
    -> historical dense-head criterion / ntokens baseline

general objective
    -> six-term eligible-row join
    -> admitted hard class or frozen Q under the M3.8 passport
    -> additive numerator S and denominator D
    -> one scalar/cotangent seed, or typed no-objective

chat / SFT branch
    -> chat template serializes one token stream
    -> assistant target ownership mask a_j
    -> prediction-row mask ell_i = a_(i+1)
    -> token-normalized loss over supervised next-token rows

training branch
    -> scaled backward over accumulation window
    -> complete declared distributed gradients + denominator + owner roster
    -> one-time unscale / one normalization / completed finite agreement
    -> one binary32 unique-owner norm and gradient clip
    -> selected logical update and bounded physical projection
    -> optimizer/scaler/scheduler commit under the declared success policy
    -> verified gradient-buffer clear

checkpoint branch
    -> model + optimizer + scheduler + scaler
    -> sampler + RNG + topology + counters

This summary is pedagogical compression; the typed contracts above take precedence. The full gold target sequence lets the code build every target prefix in one tensor. Hard visibility prevents future leakage. In the dense head profile, the loss path turns each six-term eligible, uniquely owned target-copy row into a vocabulary criterion row after M3.8 passport admission; non-PAD alone is insufficient. The update program consumes exactly one declared objective seed and commits parameters only after its readiness, finite, owner, and success policies are satisfied.

Inference remains sequential because the future target sequence does not exist yet. Training can process known target positions in parallel; inference must create future positions by generating them.

0xFF Reference