A Transformer trains without any error, the loss falls, and the model scores well on the usual validation set. Then it fails on anything where word order carries the meaning. "Transfer from Ramesh to Sunita" and "transfer from Sunita to Ramesh" get the same answer. "Delivered before the deadline" and "the deadline before delivered" get the same answer. On a task built so that order alone decides the label, module 2.8's model without positions read 0.503 on held-back rows and 0.543 on its own training rows, while an identical model with positions read 1.000 and 1.000. The training number is the tell that gets ignored: 0.543 is not overfitting, it is a model that never learned anything, because the information was never in its input. Nothing errors, and shuffling the words of an input produces a bit-identical output.
Any attention-based model where positions were forgotten, dropped or silently
disabled. Module 2.8's tiny GPT with positions=False, a classifier built on a
bare nn.MultiheadAttention, a pooling encoder over embeddings, a reranker
stacked on 3.4's vectors. The most common birth is the quiet one: the position
tensor was created and then never added, so the line pos = self.pos(...) exists
and the line x = x + pos does not. A second birth is a shape mismatch that got
"fixed" by deleting the addition. A third is a refactor to a longer input, where
the position table was sized for the old block length and the new code silently
slices or skips it. A fourth is real and worth respecting: on bag-like tasks such
as topic tagging, order genuinely does not matter much, so the model looks fine
on the validation set the team happens to own, and the fault only appears on
negation, direction, roles and sequence-of-events questions.
Linked modules: 2.8, 2.7 (order carries meaning), 1.5 (scores by slice), 3.4.
Attention computes a weighted sum over positions, and a weighted sum does not
care about order. Feed the same token vectors in a shuffled order and every
output row moves with its token and is otherwise unchanged. This is not an
approximation, it is exact: module 2.8 measured the largest gap between
run(X)[perm] and run(X[perm]) at 5.96e-08, which is float32 rounding, and
adding positions moved that gap to 0.264. So the diagnosis is a two-line test
rather than an argument. Write assert_order_blind(layer, x): run the layer on
x, run it on x[perm], unshuffle the second result, and return the largest
absolute difference. A gap under 1e-6 means the layer cannot tell one order
from another. Run it on the whole model, not only on one attention layer, because
a convolution or a recurrent layer elsewhere in the stack may be quietly carrying
the order for you. Confirm with a second, task-level check: build a small set of
input pairs that hold the same words in two orders with opposite correct answers,
and score the model on it. A model without positions scores near chance on that
set no matter how good its headline number is.
Add positions and re-run the test. The cheapest correct option is a learned
table, nn.Embedding(block_length, d_model), added to the token embedding before
the first block — that is what module 2.8's tiny GPT uses. The classic
alternative is the fixed sinusoidal encoding, which needs no parameters and
extends past the training length; a good sinusoidal table has three properties
you should print and check, namely every row different, no row repeating inside
the block, and neighbouring rows more similar than distant ones. Modern
decoder-only models mostly use rotary position embeddings applied inside
attention instead, so if you are copying a recent architecture, check where the
position information enters before assuming it is missing. Whichever you pick,
make the addition explicit and assert its shape on the line where it happens.
Then turn the diagnosis into a permanent unit test: assert_order_blind on the
full model must return more than a threshold, and the build fails if it does not.
Add the paired-input set to your evaluation table as a standing slice, beside
overall accuracy, the way 1.5 made per-class recall standing. Expect the fix to
be dramatic when order matters and invisible when it does not — the same 4,513
parameters went from 0.503 to 1.000 on the order task and would not have moved on
a bag-of-words task.
Make the permutation test part of the template you start every attention project from, so it exists before the model does. Keep one paired-input evaluation slice per project — same words, two orders, opposite labels — and refuse to ship without a number on it. Read training accuracy as evidence, not decoration: a model stuck near chance on data it has seen four thousand times is missing an input, not overfitting, and that single reading separates the two most common misdiagnoses in this module. When you change block length, embedding width or anything about the input pipeline, re-run the permutation test rather than assuming it still passes. In review, ask one question of any attention model: where do positions enter, and which test proves it.
Q: "Your model treats 'A before B' and 'B before A' identically. Where do you
look?" Strong answer: say in one sentence that attention is a weighted sum and
therefore permutation-equivariant by construction, so order has to be added to
the input. Then give the test rather than the theory — shuffle the input,
unshuffle the output, measure the gap, and expect float noise if positions are
missing. Quote the two numbers that make it concrete: a gap around 5.96e-08
without positions against 0.264 with them, and 0.503 against 1.000 on a task
where order is the only signal. Finish with the detail that shows experience:
training accuracy near chance is the fingerprint of a missing input, which is a
different disease from overfitting and has a different cure.