Where the notepad starts beating attention
To score your next article, an attention model has to hold on to every past click it is still allowed to look at. Keep-the-pad holds one small notepad instead, the same size whether you have clicked ten times or five hundred. That means there is a history length where attention's stored clicks get more expensive than the notepad, and nobody here had measured it. So we did: we stopped the clock on bytes and milliseconds for six models across seven history lengths. SASRec overtakes the dense notepad at 33 clicks; HSTU at 26. Against the smaller rank-16 notepad it is 17 and 13. Nothing was trained — these numbers come from the shape of the models, not from what they learned.
Attention's per-user memory grows by about a kilobyte for every click. The notepad's does not grow at all. They cross between 13 and 33 clicks, which is well inside a normal news-reading history. But a cached GRU keeps only 256 bytes — 129× less than the dense notepad — and it never stops being cheaper. The honest claim is about attention, not about every baseline.
Bytes kept per user, by history length
Both axes are logarithmic, so a flat line means "does not grow" and a straight rising line means "grows in step with the history". The three flat lines are the models that summarise your history into a fixed amount of state. The two rising lines are the attention models, which keep the summary of each past click — the key/value cache — so they can attend to it again next time. The open circles are the crossings.
Where each attention model overtakes each fixed-size model
| Grows with history | Fixed size | First length it costs more | Exact crossing |
|---|---|---|---|
| SASRec | FWPRec dense | 33 | 32.25 |
| SASRec | FWPRec rank-16 | 17 | 16.27 |
| SASRec | GRU4Rec | 1 | 0.25 |
| SASRecProfile | FWPRec dense | 33 | 32.25 |
| SASRecProfile | FWPRec rank-16 | 17 | 16.27 |
| SASRecProfile | GRU4Rec | 1 | 0.25 |
| HSTU | FWPRec dense | 26 | 25.72 |
| HSTU | FWPRec rank-16 | 13 | 12.97 |
| HSTU | GRU4Rec | 1 | 0.20 |
SASRec and SASRecProfile share a backbone — two layers, 64 dimensions, two heads — so their caches come out byte-for-byte identical and they cross at the same place. HSTU crosses earlier because it stores a wider value vector per click plus each click's timestamp, which its relative-time bias needs.
Full memory sweep · bytes per user
| Model | Per extra click | 10 | 25 | 50 | 100 | 200 | 350 | 500 |
|---|---|---|---|---|---|---|---|---|
| FWPRec dense | 0 | 33,024 | 33,024 | 33,024 | 33,024 | 33,024 | 33,024 | 33,024 |
| FWPRec rank-16 | 0 | 16,656 | 16,656 | 16,656 | 16,656 | 16,656 | 16,656 | 16,656 |
| GRU4Rec | 0 | 256 | 256 | 256 | 256 | 256 | 256 | 256 |
| SASRec | +1,024 | 10,240 | 25,600 | 51,200 | 102,400 | 204,800 | 358,400 | 512,000 |
| SASRecProfile | +1,024 | 10,240 | 25,600 | 51,200 | 102,400 | 204,800 | 358,400 | 512,000 |
| HSTU | +1,284 | 12,840 | 32,100 | 64,200 | 128,400 | 256,800 | 449,400 | 642,000 |
At 500 clicks SASRec is holding 512,000 bytes per user and HSTU 642,000 — 15.5× and 19.4× the dense notepad. You can argue that a real deployment would cut the history off rather than let the cache run away, and that is fair. But the window these baselines were actually trained with is 50 clicks, and even truncated there SASRec keeps 51,200 bytes — 1.55× the dense notepad and 3.07× the rank-16 one, while having thrown away everything older. The notepad's 33,024 bytes still summarise the whole history.
The part that does not favour us: speed
| Model | Ingest 1 click @10 | Ingest 1 click @500 | Score 50 @500 | Total @500 | p95 total @500 |
|---|---|---|---|---|---|
| FWPRec dense | 0.646 | 0.649 | 0.182 | 0.833 | 0.854 |
| FWPRec rank-16 | 0.687 | 0.691 | 0.194 | 0.887 | 0.905 |
| GRU4Rec | 0.070 | 0.068 | 0.041 | 0.109 | 0.116 |
| SASRec | 0.290 | 0.291 | 0.070 | 0.362 | 0.371 |
| SASRecProfile | 0.296 | 0.298 | 0.105 | 0.405 | 0.416 |
| HSTU | 0.471 | 0.495 | 0.048 | 0.544 | 0.561 |
Milliseconds, median of 200 timed steps after 25 warm-up steps, one user at a time.
Keep-the-pad is the slowest model here at every history length —
2.3×
slower per step than a cached SASRec and
7.7×
slower than GRU4Rec. Writing to the notepad is a small matrix update, and a matrix update costs more
than appending one row to a cache. The memory story and the speed story point in opposite directions,
and both are in the table.
One more honest detail: at these sizes the GPU spends most of each step on fixed per-call overhead
rather than on arithmetic, so SASRec's step time is flat from 10 clicks to 500 even though it is
attending over fifty times more of them. HSTU's creeps up about
5% over the same
range. Do not read this table as evidence that attention is free at long histories — read it as
evidence that a single user's step is too small to saturate this GPU.
The comparison we are not hiding
Against attention, keep-the-pad's memory wins past 33 clicks (SASRec) and 26 clicks (HSTU), and the gap widens without limit because one side grows and the other does not.
Against a cached GRU, keep-the-pad loses on memory at every length: 256 bytes versus 33,024, a factor of 129. GRU4Rec's state is constant too, so this never turns around.
Keep-the-pad is also the slowest per step of the six, by roughly 2.3× against SASRec.
This measures bytes and milliseconds, not ranking quality. It says nothing about whether any of these models recommends well. That argument lives in the other pages.
Which weights we used
| Model | Weights | Note |
|---|---|---|
| FWPRec dense | real MIND checkpoint | seed 42, keep-the-pad |
| FWPRec rank-16 | real MIND checkpoint | seed 42, rank-16 notepad |
| GRU4Rec | real MIND checkpoint | seed 42, matched baselines |
| SASRecProfile | real MIND checkpoint, position table widened | seed 42, matched baselines |
| SASRec | fresh weights, real config | no MIND SASRec was ever trained here |
| HSTU | real MIND checkpoint, position table widened | seed 42, matched baselines |
What a model stores per user, and how long one step takes, are set by its shape and its settings — how many layers, how wide, what the memory is made of. They are not set by the values it learned during training. So a missing checkpoint is not a hole in this result. We still used the real ones wherever they exist, because it keeps the provenance clean. Two of them were trained with a 50-click window, so their position tables were copied into wider ones to reach 500; the extra rows are fresh. SASRec has no trained MIND run in this repo at all, so it was built from the repo's SASRec config with the same table sizes as the trained baselines.
How we know the cache is real
The shared SASRec code in this repo has no step-by-step path — it re-reads the whole history every
time. The earlier cost audit reported a blank for its per-user memory for exactly that reason.
So this lab had to write the missing incremental path: keep each layer's keys and values, add one
column per new click, attend against the stored columns instead of recomputing them.
A cache you wrote yourself is easy to fool, so the test suite checks it against the shared code:
run the cached path click by click, run the original batch path on the same history, and require the
final scores to agree. They do, for SASRec, SASRecProfile and HSTU. The bytes on the chart are the
bytes that path actually holds.
The same test suite pins this package against the earlier cost audit: it rebuilds the three
fixed-size states here and asserts they still come out at 33,024, 16,640 and 256 bytes — the numbers
that audit published — so the copied measurement code cannot quietly drift away from it.
(The dense notepad's live streaming state is 33,024 bytes; the rank-16 one is 16,656, which is the
published 16,640 plus two 8-byte slot counters the earlier audit's snapshot did not carry.)
Isolated suite experiments/isolated/mind_kv_crossover_lab on damlr-w03 (RTX 4080 SUPER, driver 580.126.20, torch 2.13.0+cu130). Inference only — nothing was trained, and no ranking metric was computed. One user at a time, 50 candidates, seed 42; median and 95th percentile over 200 timed steps after 25 warm-up steps. History lengths 10 to 500 are synthetic click streams, because MIND-small's own histories are capped at 50; the quantities being measured depend on model shape, not on which items appear. Bytes count tensor storage only, not Python object overhead or allocator slack. One machine, one seed, one batch size. Do not read the latency column as a throughput claim — a real server batches users, and this did not.