Repository navigation
Long rows keep fp32 attention in linear memory: the small family's 32k-64k states no longer run out of GPU memory - #202
Merged
Conversation
…k-64k states no longer run out of GPU memory transformers asks SDPA for grouped-query attention on an unmasked call (every long state through kev.shared_prefix); only flash (no fp32) and math take enable_gqa, so the fp32 state pass fell back to math and its L x L scores. Inside kev.predictors.long_row_kernels an fp32 call repeats keys/values instead and runs the memory-efficient kernel in fp32; bf16 calls (Kev-27B) and rows under ROW_PASS_TOKENS are unchanged. Measured with scripts/long_state_memory.py (runs/long-state-*). Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ng-row routing test reads in order Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…,200 of 1,200 records; 720 short records bit for bit with main) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
jaredpalmer
force-pushed
the
eval/long-state-memory
branch
from
October 1, 2026 03:18
05d97fc to
16995ea
Compare
jaredpalmer
added a commit
that referenced
this pull request
Oct 1, 2026
…loading #200, prefix cache make_room + out-of-memory retry fix #201, fp32 long-row attention #202) The skills pinned 2ea5660. 71d4829 is main after #200, #201 and #202: the server's prefix cache now drops the entries a batch will evict before its pass (PrefixCache.make_room), so a new near-limit state no longer sits next to the old one, and the out-of-memory retry also counts this batch's own hits as held states. Checked on an H200 deploy of Kev-27B (app kev-ref71d4829-27b, stopped; runs/kev-deploy-71d4829/deploy_check.json): semif-v1 development over --remote vs runs/rel27-public max |dp| 0.0122, 0 flips (252 rows); the 70,065-token state gets the 422; two ~60k-token states answered (8.1-10.1 s new, 0.28 s repeated), the second evicting the first cleanly (cached_states 1, oom_retries 0). SKILL.md's long-document latencies updated. Spend <= ~$0.45. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
jaredpalmer
added a commit
that referenced
this pull request
Oct 1, 2026
…loading #200, prefix cache make_room + out-of-memory retry fix #201, fp32 long-row attention #202) (#204) The skills pinned 2ea5660. 71d4829 is main after #200, #201 and #202: the server's prefix cache now drops the entries a batch will evict before its pass (PrefixCache.make_room), so a new near-limit state no longer sits next to the old one, and the out-of-memory retry also counts this batch's own hits as held states. Checked on an H200 deploy of Kev-27B (app kev-ref71d4829-27b, stopped; runs/kev-deploy-71d4829/deploy_check.json): semif-v1 development over --remote vs runs/rel27-public max |dp| 0.0122, 0 flips (252 rows); the 70,065-token state gets the 422; two ~60k-token states answered (8.1-10.1 s new, 0.28 s repeated), the second evicting the first cleanly (cached_states 1, oom_retries 0). SKILL.md's long-document latencies updated. Spend <= ~$0.45. Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The small Kevs (0.8B, 4B, 9B) cannot score long documents on a GPU. The 2026-09-30 family longdoc reads (
/bench/fam-*-longdoctestonkev-runs) all stopped withOutOfMemoryError. Kev-4B and both 9B versions failed at the first 32k record, and Kev-0.8B at the first 64k record even on an H200. Kev-27B scores the same records without trouble, and agents-ood / guardrails-ood development states (up to 51k / 39k tokens) are in the same range, so this blocks every long read of the small family.Why fp32 fell to the L x L kernel
#149 already handles long rows. A record whose longest row is over
ROW_PASS_TOKENS(16,384) runs its state once throughkev.shared_prefixunder SDPA's flash and memory-efficient kernels, and is labelledkernels: efficient. For Kev-27B (bf16) that means flash. The small family is evaluated in fp32, and in fp32 the state pass never reached an efficient kernel:kev.shared_prefixpasses no mask for an unpadded state, so SDPA can run it causal.sdpa_attention_forwardsees no mask and asks SDPA for grouped-query attention (enable_gqa=True,use_gqa_in_sdpa). Grouped-query attention means 16 query heads share 4 key/value heads.enable_gqa. Flash has no fp32, so SDPA quietly uses math, which materialises a 16 x L x L fp32 score matrix: 69 GB at 32k and 275 GB at 64k, per attention layer.The profile of one 4k pass shows this (
runs/long-state-4b-h100/report.json["kernels_4k"]). Under #149's context the state pass rancutlass_80_simt_sgemm+cunn_SoftMaxForward, the math kernel. Only the masked branch pass ranfmha_cutlassF_f32(memory-efficient). One consequence: every row #149 labelledefficienton an fp32 backbone that did fit had its state attention computed by math.The change
kev.predictors.long_row_kernels()is #149's context with one addition. While the context is active, an fp32 SDPA call repeats its keys and values per query head instead of asking forenable_gqa. transformers already does exactly this whenever a mask is given. With the repeat, the memory-efficient kernel takes the call: fp32 inputs and output, and memory linear in L. Mechanically, it swapstransformers.integrations.sdpa_attention.use_gqa_in_sdpafor the duration of the context and restores it afterwards. A bf16 / fp16 call keepsenable_gqaand flash, so Kev-27B's long rows take exactly the path they took before. Nothing else changes:kernels: efficienton long rows andlong_rowsin report.json.The repeated K/V cost 1 GiB each at 64k for Kev-9B.
Measurements
scripts/long_state_memory.py(new) loads a checkpoint the wayLocalPredictordoes (fp32, TF32 off) and runs the longest longdoc-v1 development record per bucket under three settings:math(the global exact policy alone),legacy(#149's context as it was), andlong(this PR). Peak is GPU memory over the loaded weights.At 16k, math's 33.6 GiB is two 16 x 15,231² fp32 matrices (scores and softmax). That matches the cause above.
Parity against math where math fits: the 8k and 16k buckets, cuad and synthetic records, every question. "rows" is the row form that records under the threshold actually get.
So the fp32 memory-efficient kernel moves answers by at most 6.4e-4 with no flips in 238 questions. That is the same order as what #149's branch pass already moved (legacy column), and smaller than the gap between the shared prefix and the row form, which comes from fla's TF32 DeltaNet kernels. The bar I set before measuring was "no flips, max |dp| within the TF32 DeltaNet noise (~3e-3, AGENTS.md)". The H200 runs repeat the H100 values exactly where they overlap (same sm80 kernel).
Reports:
runs/long-state-{4b,9b}-{h100,h200}/report.json(numbers and kernel names only, no document text).End to end
Kev-4B on longdoc-v1 development (1,200 records, 4,654 questions, states 4k-64k), H100,
kev.benchmarkthroughmodal_app.py::benchmarks,runs/longmem-kev-4b-longdoc-dev-r2/report.json:longdoc-v1/cuad/development/32k/0000withOutOfMemoryError: Tried to allocate 55.00 GiB. That is one 16 x 30k² fp32 score matrix, the cause above, reproduced on the development partition.kernels: efficient(long_rows.threshold16,384). Those are exactly the 32k and 64k buckets; under the Qwen3.5 tokenizer no 16k-bucket row crosses the threshold.Clean overall: acc 0.887, Brier 0.180, ECE 0.066 (n = 4,654). These are report-only probe numbers, not a release claim. The 16k bucket is slower than 32k because it is on the exact row form (the state once per question); the long buckets run the state once.
Short rows are bit for bit. The main read's 720 records before the OOM (every 4k / 8k / 16k record) have logits identical to this branch's in all 720 records (
predictions.jsonlcompared field for field).What this changes for existing reads
A long row short enough to have fit in math before (just over 16,384 tokens; none in longdoc-v1) now takes the memory-efficient kernel for its state too, so it moves by up to the amounts above. Pair those rows only with reads from the same side of this PR; AGENTS.md says so next to the long-row rule. Kev-27B's reads and all short rows are unchanged.
Not in this PR
kev.servewithKEV_DTYPE=fp32on CUDA has the same grouped-query fallback on long states. It serves bf16 by default, and the fp32 server is not on any read path, so I left it alone.kernels: efficientlabel still does not say which efficient kernel ran (flash for bf16, memory-efficient for fp32).report.json["environment"]["dtype"]already tells them apart.Test plan
tests/test_unit.py::test_long_row_kernels_repeat_fp32_keys_instead_of_grouped_attention: inside the context an unmasked fp32 call repeats K/V and a bf16 call keepsenable_gqa; outside it nothing changes; the patch is restored; the outputs match.tests/test_unit.py::test_local_predictor_scores_long_rows_through_the_shared_prefix, extended: the context is entered only for a long row on CUDA (not on CPU, not under the threshold), and the labels are unchanged.tests/test_model.py::test_long_rows_run_fp32_attention_in_linear_memoryon Modal H100 (modal_app.py::gpu_tests): Kev-0.8B, a 22.5k-token state. The long pass stays under 1/4 of one fp32 L x L score matrix per head, the math pass exceeds it, the answers match within 2e-3, and the argmax is the same.test_unit,test_conventionsand the rest of the CI list pass (one flaky torchruntest_resume_is_bit_identicalcase passed on rerun; it also passes on main).