Skip to content

Packing, Masking, and Tokenization as a Model Interface

The same 44 content tokens, less padding

Document A: 20 content + 0 padding

Document B: 7 content + 13 padding

Document C: 14 content + 6 padding

Document D: 3 content + 17 padding

36/80 = 45% padding. Indigo, amber, green, and pink identify documents A–D; stone cells are padding. The packed grid wraps for display, not at document boundaries.

This compares token-slot utilization, not attention FLOPs. Preserving independent documents also requires a suitable attention mask and sequence-boundary handling.

A batch of short and long documents can spend more time processing padding than text. Packing puts several documents in a row so the accelerator does less wasted work. To make it behave like the original batch, we also need to preserve attention boundaries, prediction targets, and loss weighting.

Prerequisites: the tokenization pipeline, especially special tokens and chat templates. An attention mask is a grid specifying which positions can read from which other positions; Arc 4 develops the calculation. The variable-length kernel section also builds on the FlashAttention example in vectors and spaces.


The padding waste problem

A common training layout uses a rectangular tensor: every row has the same allocated length. If a batch contains sequences of length 14, 87, 203, and 51, padding to 203 adds 189, 116, 0, and 152 padding positions respectively.

A dense implementation can still compute states for padding positions even when their losses are ignored. The wasted fraction depends on the length distribution and batching policy. Krell et al. study packing as a way to reduce it while preserving independent examples.

Length bucketing reduces padding when rows remain rectangular. Variable-length kernels can skip padded positions entirely.


Sequence packing

Packing concatenates multiple sequences into a single fixed-length row. If your target length is 512 tokens and you have three sequences of lengths 140, 210, and 155, you pack them together into one row of 505 tokens with only 7 tokens of padding at the end. Instead of three rows padded to 210 each (630 tokens, 125 wasted), you get one row of 512 with 7 wasted.

Choosing which sequences share a row is a bin-packing problem. Heuristics such as first-fit-decreasing can reduce unused positions, but their utilization depends on the length distribution and allowed row sizes. Token utilization is not the same as GPU utilization: kernels, memory traffic, and scheduling still determine throughput.

The next question is whether the packed documents should be allowed to attend to one another.


Document boundaries

With one causal mask over a packed row, every position can attend to earlier positions regardless of document boundaries. A token in Document B can therefore read tokens in Document A; a token in Document C can read both preceding documents.

Some pretraining pipelines intentionally concatenate documents under one causal mask. If the goal is to reproduce independently processed examples, though, that changes the objective: a prediction in Document B now has access to Document A.

An EOS token marks a boundary in the text, but does not enforce one in attention. The model can learn to use that marker; only the attention mask or the kernel's sequence boundaries guarantee that tokens from the preceding document are inaccessible.

Select tokens below to inspect which earlier positions are allowed. Switch to isolated documents to remove cross-document connections.

Sequence Packing & Attention Masks
Original sequences
Doc AThecatsat
Doc BHelloworld
Doc Cx=5+2
Doc DGohome
↓pack into single row
ThecatsatHelloworldx=5+2GohomepackedThecatsatHelloworldx=5+2GohomeThecatsatHelloworldx=5+2Gohomequery ↓key →
cross-contamination (51 cells)Doc ADoc BDoc CDoc D
Select a token to inspect allowed attention

A causal mask across the whole row and separate causal blocks for each document. The red cells mark cross-document connections allowed by the first choice.


Packing-aware masking

To isolate documents, use a block-diagonal attention mask. Each document has its own triangular causal block, with connections outside the block masked to negative infinity so they receive zero softmax weight.

Concretely, if Document A occupies positions 0-2, Document B occupies positions 3-4, and Document C occupies positions 5-9, you construct a mask where position 4 (last token of Doc B) can only attend to positions 3 and 4, not to positions 0-2. The attention for each document is completely self-contained.

Position IDs also deserve a check. A model with learned absolute position embeddings sees a different input if Document B starts at position 3 instead of 0. Resetting positions preserves the unpacked convention. With standard RoPE and isolated attention, a constant offset within a document cancels from relative rotation differences; resetting is not inherently required for that mathematical reason. Some kernels nevertheless use position resets to infer sequence boundaries.

Position IDs in a packed row
mode: continuous
packedpos idThe0cat1sat2Hello3world4x5=657Doc ADoc BDoc Ccontinuous positions: Doc B begins at 3per-document positions: each document begins at 0

Two position-ID conventions for the same row. Resetting matches independent sequences. Whether an offset changes attention depends on the positional encoding and kernel interface.

Here's what it looks like in PyTorch to build a block-diagonal mask from a list of sequence lengths:

import torch
 
def build_block_diagonal_mask(seq_lengths: list[int]):
    """
    Build a block-diagonal causal attention mask for packed sequences.
    
    Args:
        seq_lengths: list of individual document lengths within the packed row.
                     e.g. [3, 2, 5, 2] for four documents packed into length 12.
    
    Returns:
        mask of shape (total_len, total_len) where True = attend, False = block.
    """
    total_len = sum(seq_lengths)
    # Start with all zeros (block everything)
    mask = torch.zeros(total_len, total_len, dtype=torch.bool)
    
    offset = 0
    for length in seq_lengths:
        # For each document, create a causal (lower-triangular) block
        causal_block = torch.tril(torch.ones(length, length, dtype=torch.bool))
        mask[offset:offset + length, offset:offset + length] = causal_block
        offset += length
    
    return mask
 
def build_position_ids(seq_lengths: list[int]):
    """
    Build position IDs that reset to 0 at each document boundary.
    
    e.g. seq_lengths=[3, 2, 5] -> [0, 1, 2, 0, 1, 0, 1, 2, 3, 4]
    """
    position_ids = []
    for length in seq_lengths:
        position_ids.extend(range(length))
    return torch.tensor(position_ids, dtype=torch.long)
 
# Example usage:
seq_lengths = [3, 2, 5, 2]  # four documents packed into one row
mask = build_block_diagonal_mask(seq_lengths)
position_ids = build_position_ids(seq_lengths)
 
print(f"Packed length: {sum(seq_lengths)}")
print(f"Position IDs: {position_ids.tolist()}")
print(f"Mask shape: {mask.shape}")
print(f"Mask:\n{mask.int()}")
# Output:
# Position IDs: [0, 1, 2, 0, 1, 0, 1, 2, 3, 4, 0, 1]
# Each document gets its own causal block along the diagonal.

This Boolean mask follows PyTorch scaled-dot-product attention's convention. An additive mask instead needs 0 for allowed positions and negative infinity for blocked positions; a floating 0/1 matrix is not equivalent. Check the API because Boolean conventions differ.

There is also an allocation cost: the mask is a dense N×NN \times N matrix, where NN is the total packed sequence length. For a packed row of 8,192 tokens, that's a 67-million-element boolean matrix (which can be shared across heads).


Flash Attention and variable-length sequences

The FlashAttention implementation supports variable-length sequences through its cu_seqlens interface. It can enforce document boundaries without allocating a dense mask.

Instead of materializing a giant N×NN \times N mask, you pass FlashAttention a flat packed tensor and a list of cumulative sequence lengths. For our example with lengths 3, 2, 5, 2, that's cu_seqlens = [0, 3, 5, 10, 12]. FlashAttention internally computes attention within each document's boundaries without ever building the full mask matrix.

Kundu et al. evaluate variable-length packing for training efficiency.

# Using FlashAttention's variable-length interface (pseudocode)
from flash_attn import flash_attn_varlen_func
 
# q, k, v are packed tensors of shape (total_tokens, num_heads, head_dim)
# cu_seqlens marks document boundaries: [0, 3, 5, 10, 12]
# max_seqlen is the longest individual document
 
output = flash_attn_varlen_func(
    q, k, v,
    cu_seqlens_q=cu_seqlens,
    cu_seqlens_k=cu_seqlens,
    max_seqlen_q=max_seqlen,
    max_seqlen_k=max_seqlen,
    causal=True,
)
# Each document attends only within itself. No mask matrix allocated.

Preserve the targets and the update

An attention mask alone is not enough. In next-token training, the last token of Document A must not be assigned the first token of Document B as its target if the documents are meant to be independent.

For an interface that takes unshifted labels and shifts them internally, mask the label at each document start:

labels = input_ids.clone()
doc_starts = [0, 3, 5, 10]
labels[doc_starts] = -100  # ignore these targets after the internal shift

Here input_ids is one packed row. Keep each document's EOS token as a target predicted from its own preceding token. If you construct shifted targets yourself, apply the mask to the preceding prediction position instead. Padding and any prompt-only loss mask must also be applied.

Packing need not change the gradient. If both layouts contain the same examples in one logical batch, isolate their attention, preserve positions and targets, sum the same token losses, and divide by the same denominator, they compute the same objective. Kernel arithmetic and dropout can introduce numerical differences.

Padded: 3 rows × 7 positions

A
B
C

14 content positions, 7 padding positions

Packed: 1 row × 14 positions

A
B
C

14 content positions, no padding

Both: the same valid prediction losses, the same normalization, one optimizer step.

Compare padding work for the same logical batch and optimizer update.

Changing the number of examples per optimizer step, averaging per row instead of per valid token, or changing the sampling order is a separate training choice. Control those choices when comparing packed and unpacked runs.


Tokenization as a model interface

The vocabulary maps text to IDs; the embedding table assigns those IDs learned vectors. Templates supply conversation structure, attention boundaries control available context, and loss masks select prediction targets.

Replacing a tokenizer with one that assigns different meanings to the same IDs breaks the learned lookup. Vocabulary adaptation is possible, but it requires remapping or initializing weights and further training, rather than merely changing a preprocessing file.

Save the tokenizer and template alongside the weights. For training, also record the packing, position, and loss conventions so a later run can reproduce the input and objective.

From messages to vectors

  1. 1. Raw messagesuser: "What is 2+2?"
  2. 2. Chat templateAdds role delimiters and message boundaries
  3. 3. TokenizerMaps the formatted sequence to vocabulary IDs
  4. 4. Token IDs[role_id, …, question_piece_id, …]
  5. 5. Embedding lookupSelects one learned row per ID
  6. 6. VectorsOne d-dimensional vector per sequence position

The placeholders describe the interface; they are not IDs from a named tokenizer. Training also supplies position IDs, attention boundaries, and prediction targets.

From raw text to model inputs: tokenization and templates define the sequence, while attention and loss conventions define the training task.

Worked example

Consider "world" in Document B. With a causal mask over the whole row, its attention can include "The", "cat", and "sat" from Document A, as well as "Hello" and "world" from its own document. With isolated blocks, only the latter two positions are available.

Removing keys changes the softmax denominator and generally changes the attention output. The visual shows allowed connections, not measured attention weights.


Misconceptions

"Packing must change the gradient." It need not. Preserve the logical batch, boundaries, targets, positions, and loss normalization. Comparing three separate optimizer updates with one combined update is a change of training procedure, not a necessary consequence of packing.

"EOS tokens isolate documents." EOS is a learned marker. Isolation requires the attention mask or sequence boundaries passed to the attention kernel.

"You can swap tokenizers on a trained model." An arbitrary swap is not compatible. The embedding matrix is indexed by token ID; if an ID changes meaning, its row still represents the old token. You would need to retrain or at least adapt the embedding layer, and even then the rest of the network has co-adapted to the original tokenization's statistical properties. Some people have done vocabulary extension (adding new tokens with initialized embeddings for fine-tuning), but that's additive, not a swap.

"Loss masking and packing masks are the same thing." Attention masks control which positions can read one another. Loss masks select which prediction errors contribute to the objective. A response-only SFT recipe trains on assistant targets while retaining the prompt as input; those prompt positions can still receive gradients through predictions that attend to them. A packed SFT batch can use both kinds of mask.

One response-only SFT example. Template markers and token boundaries are schematic; loss values are illustrative. Every position participates in the forward pass. Only the five response targets contribute to this objective.

System
<|s|>
masked
system
masked
You
masked
are
masked
helpful.
masked
User
<|u|>
masked
user
masked
What
masked
is
masked
2+2?
masked
Assistant
<|a|>
masked
assistant
masked
The
1.8 nats
answer
0.9 nats
is
0.4 nats
4.
0.6 nats
<|eot|>
0.3 nats

Mean response loss = 4.0 / 5 = 0.80 nats.

Each labeled loss scores that target from the preceding context. Masking a target loss does not prevent its input representation from receiving gradients through later predictions. Other SFT recipes also train on prompt tokens.

The five amber assistant targets contribute to this response-only SFT loss. System and user tokens remain in the input.

Closing Arc 3

The pipeline connects what text actually is at the byte level to BPE and other tokenization algorithms. Vocabulary design determines the available IDs; the embedding table turns those IDs into vectors. Special tokens and chat templates encode conversation structure, while packing and masking determine how examples share a training batch.

To check a packed batch, compare its loss with the unpacked examples under the same positions, targets, and normalization.

The input contract has several parts

Raw text: “What is 2+2?”

  1. What
  2. is
  3. 2
  4. +
  5. 2
  6. ?

Constructed vocabulary segmentation: six example pieces.

These pieces and abbreviated role delimiters are schematic, not the literal output of a named tokenizer or chat template.

Inspect four parts of the input and training contract. Vocabulary and templates define the sequence; attention masks control available context; loss masks select prediction targets.

What's next

Arc 4 starts with Self-Attention: Q, K, V from First Principles. It derives how each position combines information from the positions its attention mask allows.


Additional reading (and watching)