Repository navigation
[KV cache] Fix multi-label plans under the host index ring and plan cache - #446
Conversation
Both paths are opt-in, off by default (MSTAR_KV_PINNED_INDPTRS, MSTAR_KV_PLAN_CACHE). The index ring recycled its 8 host buffers by take count and each plan label takes 4, so a plan of 3 labels overwrote its first label's arrays before they were read (IndexError in KVManager.plan). The ring now holds one buffer set per plan: KVManager.plan calls begin_plan, every take of the plan gets its own buffer, and a set is reused two plans later after the event recorded on its stream, as a single-label plan already was. A cached decode plan sized each label's qo_indptr from the real rows of all labels (main + cfg_img gave [0, 1, 2] per label). It now counts that label's own rows, and a hit requires each label's padding to be the step's, since it rebuilds every label's padding from all padding rows. Found by an independent Codex audit of mstar-project#396.
The docstrings said default 1; the code and docs/environment_variables.rst say 0.
…how many there are
|
/review |
Automated reviewThis PR changes Findings: none. I traced the ring's lifetime through On the description: the claim that Advisory, not a merge gate. Rules: |
Live test: [KV cache] Fix multi-label plans under the host index ring and plan cacheWorks as far as served traffic reaches it: with 1x H200, Claims
PerformanceQwen3.5 0.8B,
No failed requests; inside main's restart spread. Host load 22–35. Commands
Other
|
merceod
left a comment
There was a problem hiding this comment.
Ran 3c03bed (and 446m = main 84fd4a6 + this PR, since the head sits 23 commits behind main) against main on a klone H200 (clock-limited node, 1560 MHz under load).
- BAGEL think mode, MSTAR_KV_PLAN_CACHE=2 with the ring on, seeds 100 to 102, one request alone then two at once. Main logs "plan cache disagrees with the full plan for 2 segments" once, the head and 446m log none, 0 IndexError and 0 tracebacks in all 6 launches, and the images hash the same on every arm and in modes 0, 1 and 2 (mode 1 serves the cached plan). Thinking text identical between main and 446m. Same as the live test.
- Qwen3.5-0.8B at c=32 in a quiet block, 3 launches per cell. Knobs on, 446m 3019 out tok/s [2955, 3395] vs main 3096 [2913, 3119], knobs off 2814 vs 3083, ring alone and cache alone on main 3270 and 3233, all inside the launch spread, so the knobs cost nothing measurable on either tree and the PR changes nothing there. Mode 2 on Qwen3.5, 0 mismatch lines on both. Greedy outputs agree within the launch-to-launch floor of this stack (54 to 72 of 112 across every pair, the same tree and knobs pair agrees on 60).
- An earlier, busier block had main with the knobs on at 2009 vs 2534 tok/s with two "plan_executor timed out waiting for batch N commit" warnings. That turned out to be a one-time first big batched prefill that passed 10 s in those two launches (19.4 and 11.3 s), not the knobs. 0 warnings in the other 43 Qwen3.5 launches, knobs on or off.
- 26/26 new tests, 18 fail on main (15 multi-label, 3 ring). cpu-core failing set equals main's, ruff clean.
- Reading agrees. Every consumer of a plan's ring buffers runs inside the same plan call on the stream begin_plan recorded on, preplans run under the plan stream, and promotion issues no copies, so a set is reused two plans later with its event waited on. n_rows counts each label's own rows plus padding, the strict zip cannot raise, and no shipped model declares three separate labels in one KV step, so the IndexError stays a CPU test, as you say.
One nit, inline. The kv_pinned_indptrs docstring still says the cost is event bookkeeping per take, it is one event per plan now.
Approving.
|
|
||
| def kv_pinned_indptrs() -> bool: | ||
| """``MSTAR_KV_PINNED_INDPTRS`` (default 1): the KV plan writes FlashInfer's | ||
| """``MSTAR_KV_PINNED_INDPTRS`` (default 0): the KV plan writes FlashInfer's |
There was a problem hiding this comment.
The text below still says the cost is "event bookkeeping per take". With this PR the ring records one event per plan, so the per-take wording is stale.
There was a problem hiding this comment.
Fixed: the docstring and its docs/environment_variables.rst row now say the cost is one CUDA event per plan, which a buffer set waits on before it is rewritten.
|
Thanks for the runs.
Fixed in |
Fixes #445
What does this PR do?
Found while auditing the opt-in KV host optimizations from #396 (both off by default). Either one, turned on, breaks plans with more than one label:
MSTAR_KV_PINNED_INDPTRS=1). A 3-label plan overwrote its first label's arrays andKVManager.plan()raisedIndexError. The ring now keeps one buffer set per plan.MSTAR_KV_PLAN_CACHE=1|2). A cached decode step counted query rows across labels (main+cfg_imggave[0,1,2]per label). It now counts each label's own rows.Single-label plans are byte-identical, with no timing change. A small padding guard and a docstring fix are separable (see "What changed"). #438 keeps BAGEL
image_genon one combined plan label, so it reaches neither bug.Schematic of
PinnedIndexRingand the plan cache's per-labelqo_indptr; both paths are opt-in and off by default.How was it tested?
On ffc8a13 (head), CI's install, GPU hidden:
cpu-core.txt1308 passed, 55 skipped; the two KV test files 26 passed;ruff check .clean. 15 of the 23 tests in the newtest/modular/test_kv_multi_label_plans.pyfail on main. With the GPU visible they pass too. On an H200, BAGEL think-mode decode logs the plan-cache mismatch on main and not here.Not tested
CUDA async-copy lifetime of the ring buffers (every consumer's copy issued before its set is reused, across preplan, promotion, discard and inline plans) was checked by source tracing and with mocked events, not by observing real async copies.
What changed
Index ring (
MSTAR_KV_PINNED_INDPTRS=1). Each plan label takes 4 of the ring's 8 host buffers, so a 3-label plan overwrote its first label's arrays andKVManager.plan()raisedIndexError. The ring now keeps one buffer set per plan, so every array lives until the plan after next, as single-label plans already did.Plan cache (
MSTAR_KV_PLAN_CACHE=1|2). A cached decode step counted query rows across labels (main+cfg_imggave[0,1,2]per label). It now counts each label's own rows. On an H200, BAGEL think-mode decode logs the mismatch on main and not here.kv/plan.py:PinnedIndexRingholdsdepth=2buffer sets, one per plan.begin_plan()records the finished plan's event on its stream and waits on the event of the set it reuses;take()fills the next buffer of the current set, growing the set past four takes if needed. One event record per plan instead of per take, so a single-label plan does no more work than before.kv/manager.py:plan()callsbegin_plan()after the preplan promotion return (a promoted plan reuses its staged arrays)._fast_decode_planuseslen(rows.rows) + n_pad._derive_rows: a hit now requires each label's padding rows to be the step's padding rows, row for row (request, label, span), not just as many. In a combined label, a later source label's real rows can follow an earlier one's padding, so a count alone let real rows pass as padding. A hit rebuilds each label's padding from all of the step's padding rows, so padding under only some labels gave the other labels extra views (verification mode flagged it; no shipped model lays padding out that way). One condition; easy to drop if you prefer the strict scope._fast_decode_plan): a step is also planned from the streams when its combined labels differ from the cached step's, or when its padding rows change label. The cached views place each label's rows for one grouping and one padding order, so either change gave a combined label the wrong rows in mode 1 (mode 2 caught it).mstar/utils/knobs.pyanddocs/environment_variables.rst: the two knobs' docstrings said "default 1" (the code says 0), and the ring's cost now reads one CUDA event per plan instead of event bookkeeping per take. Docs only.Tests and evidence
test/modular/test_kv_multi_label_plans.py(incpu-core.txt, about 0.3 s): the two audit regressions; cached against full plans for 1, 2 and 3 labels and a mixed set, each with the ring on and off; padded multi-label steps under verification mode; padding under some labels only; one plan's arrays intact while the next is built. 15 of its 23 tests fail on main, including combined labels with padding, padding that changes label between steps, and a step that starts combining labels.test_kv_pinned_index_ring.pyupdated to per-plan rotation.cpu-core.txt1308 passed, 55 skipped; the two KV test files 26 passed;ruff check .clean.plan()timing at 32 rows is unchanged within noise.bagel_single_gpu.yaml,MSTAR_KV_PLAN_CACHE=2 MSTAR_KV_PINNED_INDPTRS=1), three think-mode text-to-image requests (one alone, two concurrent): main logs "the plan cache disagrees with the full plan for 2 segments"; this branch logs none and has no tracebacks, and the images hash the same on both (verification mode serves the full plan).Checklist
ruff check .passesAI assistance: written with AI coding agent