Spot the error: 'I apply the causal mask by setting masked positions to 0 in the attention scores before softmax.'
Click any words you think contain an error. Click again to unmark.
Setting masked scores to zero leaves them with about one-over-N weight after softmax; the correct mask value is negative infinity, applied before softmax.
Picture a vote where every candidate's score gets converted into a vote count by a rule that turns even a zero into one vote, because the conversion exponentiates first. A candidate you tried to disqualify by giving them a zero score still ends up on the ballot with one vote. To actually remove them, you have to push their score down to 'negative infinity bad', which is the only score the rule turns into zero votes. The intuition that 'zero score equals zero influence' is wrong here, and that misunderstanding is exactly the masking bug we are spotting.
Detailed answer & concept explanation~4 min readEverything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example.
Everything important, quickly.
5 min: domain confusion between score and weight; arithmetic for softmax of [1, 2, 3, 0]; correct additive masking with -inf; dtype-aware mask values; multiplicative masking equivalent bug; training inference mismatch signature; how to test for leakage.
import math
import torch
B, H, T, D = 1, 1, 4, 8
q = torch.randn(B, H, T, D)
k = torch.randn(B, H, T, D)
causal_mask = torch.tril(torch.ones(T, T)) # 1 at allowed, 0 at forbidden
# WRONG: set masked scores to 0
scores = q @ k.transpose(-2, -1) / math.sqrt(D)
bad = scores.masked_fill(causal_mask == 0, 0.0)
weights_bad = torch.softmax(bad, dim=-1)
# weights_bad has nonzero weight at future positions
# CORRECT: set masked scores to -inf
good = scores.masked_fill(causal_mask == 0, float('-inf'))
weights_good = torch.softmax(good, dim=-1)
assert weights_good[..., 0, 1:].sum().item() == 0.0 # row 0 attends only to col 0Real products, models, and research that use this idea.
- PyTorch torch.nn.functional.scaled_dot_product_attention with is_causal=True handles masking correctly under all backends.
- HuggingFace transformers uses torch.finfo(dtype).min as the mask value, which is roughly -3.4e38 for fp32.
- A long tail of tutorial-blog attention implementations on the open web use 'scores times a binary mask' and silently leak attention, this remains one of the most common review findings in custom kernel code.
- Karpathy's nanoGPT correctly uses tril plus masked_fill with float('-inf') as the canonical reference pattern.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you detect this bug in production?
QWhy prefer a large finite negative over literal float('-inf') in fused kernels?
Don't say thisRed flags and common mistakes that signal junior thinking. Click to expand.
Red flags and common mistakes that signal junior thinking. Click to expand.
Conflating pre-softmax score with post-softmax weight. A score of zero exponentiates to one, so the masked position still claims a real slice of the probability mass.
The night-before-the-interview bullets. Scan these on the way to the call.
Primary sources. Skim if you want the original framing.
Same topic, related formats. Practice these next.