Padding, Masks, and Variable-Length Batching
Real batches contain sequences of different lengths, and the three ways of handling that difference, padding, packing, and varlen kernels, have wildly different costs and each has a signature bug.
A tensor is a rectangle. A batch of sixteen prompts is not. Reconciling those two facts is one of the most-touched pieces of plumbing in any LLM stack, and the naive reconciliation is expensive: pad every sequence to the longest one in the batch. Fifteen sequences of 32 tokens beside one of 1,024 gives a \(16 \times 1024\) tensor that is 97% padding, and attention's quadratic term is computed over all of it.
Three regimes
Padding with a mask. Fill short sequences to max_len, then add \(-\infty\) to attention logits at padded key positions so softmax assigns them zero weight. Simple, and it wastes compute proportional to the padding fraction.
Packing. Concatenate sequences end to end into fixed-length blocks and rely on a block-diagonal mask to stop cross-document attention. Zero padding waste; the mask has to be right or the model learns to attend across unrelated documents.
Varlen kernels. Concatenate into one flat buffer of shape (total_tokens, heads, dim) and pass cu_seqlens, the cumulative-length prefix array, so the kernel iterates each sequence's own extent and never computes the off-diagonal blocks at all. This is flash_attn_varlen_func in FlashAttention (Dao et al., 2022, FlashAttention, arXiv:2205.14135); the block-diagonal mask is not evaluated because it is never constructed.
For a batch of one 1,024-token sequence and fifteen 32-token sequences, the padded attention term is \(16 \times 1024^2 \approx 16.8\)M score entries. The true requirement is \(1024^2 + 15 \times 32^2 \approx 1.06\)M, a factor of roughly 16.
cu_seqlens, precisely
For lengths \([1024, 32, 32]\), cu_seqlens = [0, 1024, 1056, 1088], length \(B+1\), ending at the total token count. Sequence \(i\) occupies [cu_seqlens[i], cu_seqlens[i+1]). Two off-by-one errors dominate: forgetting the trailing total, which truncates the last sequence, and passing a max_seqlen smaller than the true maximum, which silently truncates the longest sequence's attention span.
The padding side that everyone gets wrong
Encoders pad on the right. Decoder-only generation must pad on the left. Generation reads the logits at the final position of the sequence tensor; with right padding that position holds a pad token, so the model is asked to continue from <pad>. Output is fluent, plausible, and wrong, and it degrades gradually with padding amount rather than failing outright, which is why it survives casual testing. Position IDs must also be built from the mask rather than from arange, or every left-padded sequence starts at a positive offset and the whole batch is positionally shifted relative to how it was trained.
Packing has its own signature failure: correct block-diagonal masking with unreset position IDs. Attention is properly isolated per document while RoPE sees the second document beginning at position 1,024, teaching the model that documents can start anywhere in the index range.
When it breaks
Masking is not free even when it works. A materialised \(B \times H \times L \times L\) boolean or float mask is often larger than the activations it guards; at \(B=8\), \(H=32\), \(L=8192\) an fp16 additive mask is roughly 34 GB, which is why real kernels take a mask description, a causal flag plus cu_seqlens, rather than a mask tensor.
Loss normalisation interacts badly with variable lengths. Averaging per-microbatch loss and then averaging across microbatches weights short sequences more heavily than long ones, so the gradient depends on how tokens were bucketed. The fix is to sum token losses and divide once by the global unmasked token count, and getting this wrong changes results without changing anything visible in the logs.
Throughput measurements quietly inherit the padding fraction. Tokens per second computed over the padded tensor counts work done on <pad>, so a sorted-by-length batching change can appear to make a system slower while making it faster, and vice versa. Report throughput over real tokens, and report the padding fraction next to it.
12 flashcards for this concept
Click a card to reveal the answer.