Repository navigation
qwen36 tier: re-plan the VRAM residents from the prompt's own prefill routing (QT_PREFILL_REPLAN=1) - #1769
Merged
Conversation
kreuzzelg
force-pushed
the
qwen36-prefill-replan
branch
from
October 1, 2026 20:01
d7f8f2b to
8b85c98
Compare
kreuzzelg
force-pushed
the
qwen36-prefill-replan
branch
2 times, most recently
from
October 3, 2026 08:57
eee3c83 to
8edba54
Compare
…r's own buffers Every expert of a model has the same geometry, so a tier swap can write the newcomer into the victim's device buffers instead of cudaFree + cudaMalloc, each of which synchronises the device under the async expert groups. Same steps as the upload (H2D, the int4 offset-to-signed pass, the scales), into the buffers already there; a failure half-way leaves the content undefined and the caller frees the tensor. Optional in the DLL loader (RESOLVE_OPT): a backend that predates it answers 0 and the caller uploads as before. The fake backend implements it and can be told to refuse, so the fallback is testable without a card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ting The warmstart fills VRAM from the heat file, i.e. from what earlier prompts routed to, and the LFRU tick corrects that one expert per sixteen tokens and device. qt_replan(layer, counts, cap) takes one layer's routing counts over this prompt's rows and swaps residents the prompt never routed to for its most-routed non-residents of that layer: budget-neutral, the victim-first swap the tick uses, in strict count order, only while the newcomer's count beats the victim's. What the upload queue takes starts at once, the rest is pending and drains on later calls and four per token on the decode ticks, so nothing blocks. layer < 0 takes the whole table and plans per device. The swap writes the newcomer into the victim's buffers when the backend has coli_cuda_tensor_overwrite, and frees-then-uploads otherwise. Evicted slots are reported through qt_evicted_take so the engine can rebuild their RAM copies ahead of the miss path. qt_stats_mark snapshots the counters at the prefill/decode boundary and qt_stats then prints the decode-only hit rate, the number a re-plan moves; the swap line says how many were in place. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ill, layer by layer After each prefill layer's routing loop moe() hands that layer's counts over the prompt rows to qt_replan, so the swaps of layer L upload while layers L+1.. still compute; QT_PREFILL_REPLAN_MAX caps them per layer (24: offline at B=96 that reaches the uncapped plan's hit rate with two thirds of the swaps). At the start of every step the experts the tier evicted get their RAM int8 copies back, in parallel, before the layers run: on the int4 container the warmstart freed them, and left to the miss path 2,000 victims cost ~7 ms/token over the next 300 tokens in slot_ensure_int8. The prefill/decode boundary is marked for the decode-only hit rate. Measured on the 3070 (35B per-row int4, heat file from six other prompts, four held-out prompts, 300 tokens): decode VRAM hit rate 55-63 -> 73-75 % with the trunk on the CPU, 42-50 -> 60-67 % with the trunk in VRAM, MoE phase -2 to -5 ms/token, whole token -8..-10 % with the trunk in VRAM on three prompts of four; TTFT +0.1..1.1 s for the 960 swaps' uploads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kreuzzelg
force-pushed
the
qwen36-prefill-replan
branch
from
October 3, 2026 20:58
8edba54 to
dac90d7
Compare
…ow-the-prompt section Fake backend, in make check: counts order the plan and only a newcomer that beats its victim is swapped; the cap holds; a layer plan leaves other layers alone while the whole-table form trades across layers; swaps are budget- neutral and in place (the newcomer takes the victim's tensor pointers, no new upload); evicted experts are reported once, with their layer; with the overwrite refused the swap still lands by free-then-upload. Docs: the offline table that motivates it, the mechanism, the measured runs, the TTFT price and its follow-up, the two knobs in ENVIRONMENT.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…verwrite 47 mandatory + 10 optional, 57 exports: the new entry point is optional on purpose, a DLL predating it leaves the pointer NULL and the tier frees and uploads as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…im on every libc qsort is not stable and glibc's and msvcrt's disagree on ties: two never- routed residents with equal heat came out in a different order on the Windows runner, and the test's "4 in, 0 out" saw 1 go instead. The slot index breaks the last tie in both comparators; the plan is now the same on every platform. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
kreuzzelg
force-pushed
the
qwen36-prefill-replan
branch
from
October 3, 2026 20:59
dac90d7 to
e4a2e3c
Compare
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 warmstart fills VRAM from the heat file, i.e. from what earlier prompts routed to, and the LFRU tick then corrects one expert per sixteen tokens and device. For a prompt on a new topic that is thousands of tokens of catching up. Measured on the 35B at 37 % residency: a heat file from six other prompts gives a fresh prompt 55-63 % decode hits, a cold start 38 %, a heat file from the same prompt 89-91 % -- the heat warmstart is an oracle for a repeated prompt and a stranger's guess for a new one.
The prefill knows better, because its routing is this prompt's routing. Offline (14 route traces, four held-out prompts), a static set of B experts per layer chosen from the prompt's own prefill counts covers its decode routing far better than the heat file at the same budget: at B = 96 of 256 (the 8 GB card's share) 0.741 against 0.593, oracle 0.890; at B = 32, 0.421 against 0.217. Mixing in heat does not help. This is the prefill-as-probe idea from #1078 without the Atlas: the prompt's own counts are finer than any cluster, and there is nothing pre-fitted to leak the evaluation set (the concern raised there).
What the PR does
Tier.
qt_replan(layer, counts, cap): given one layer's routing counts over the prompt rows, residents the prompt never routed to are swapped, budget-neutral, for the prompt's most-routed non-residents of that layer -- the same victim-first swap the LFRU tick uses, in strict count order, only while the newcomer's count beats the victim's. As many swaps as the upload queue takes start at once; the rest are pending and drain on later calls and, four per token, on the decode ticks. Nothing blocks.qt_evicted_takereports evicted experts, andqt_stats_markletsqt_statsprint the hit rate counted from the first decode token (decode VRAM hit rate, the number that moves).Engine.
QT_PREFILL_REPLAN=1(opt-in): after each prefill layer's routing loopmoe()hands that layer's counts toqt_replan, so the swaps of layer L upload while layers L+1.. still compute.QT_PREFILL_REPLAN_MAXcaps swaps per layer (default 24, see below). At the start of every step the engine rebuilds the RAM int8 copies of the experts the tier evicted, in parallel, before the layers run: on the int4 container the warmstart freed them, and left to the miss path 2,000 victims cost ~7 ms/token over the following 300 tokens (slot_ensure_int8inside the decode loop). Routing is untouched: placement never changes which experts run.Backend.
coli_cuda_tensor_overwrite: every expert of a model has the same geometry, so a swap writes the newcomer into the victim's device buffers instead ofcudaFree+cudaMalloc, each of which synchronises the device under the async expert groups. Optional in the DLL loader (RESOLVE_OPT); a backend without the entry point, or a failure half-way, falls back to free-then-upload. The fake backend implements it and can refuse it, so the fallback is tested.Measured (RTX 3070 alone, per-row int4 35B, 200-250-token prompts, 300 decoded tokens, heat file from six other prompts, cap 24 = 960 swaps per prompt)
Hit rate +12 to +22 points, as the offline table predicted; MoE phase -2 to -5 ms/token; with the trunk in VRAM that is 8-10 % of the token on three prompts of four (the fourth sits in the ±2 ms run-to-run noise). With the trunk on the CPU the saving disappears in the 50 ms the dense trunk costs there.
The price, honestly. The swaps upload during the prefill, and each one is six synchronous host-to-device copies (three matrices, three scale vectors), about 1 ms on this box. Uncapped (~1,500 swaps) that was +1.3-1.8 s of TTFT for the same hit rate; capped at 24 per layer (960 swaps) it is +0.1-1.1 s, paid back by the decode after 100-250 tokens. The in-place overwrite did not change that number -- the cost was never the allocation, it is the copies serialising with the prefill's own expert groups. Offline at B = 96: cap 12 per layer (480 swaps) reaches 0.709 of the uncapped 0.741, cap 24 reaches 0.741, a minimum-count threshold buys nothing -- hence the default. The follow-up that would take the TTFT price away is a pinned, asynchronous staging path on a non-blocking stream, or one contiguous device block per expert; both are backend work I would rather do as their own PR.
Two things I measured on the way and did not ship: draining the swaps on the decode ticks instead of the prefill (48 per token) made decode slower by 7 ms/token while they drained, because each swap was then a
cudaFree/cudaMallocunder the async groups -- that is why the tick drains four and why the overwrite exists; and without the evicted-expert rebuild the hit-rate gain was eaten whole byslot_ensure_int8in the miss path.Verified
tests/test_qwen36_tier_replan.c(fake backend, inmake check): counts order the plan and only a newcomer that beats its victim is swapped; a cap holds; a layer plan leaves other layers alone while the whole-table form trades across layers; swaps are budget-neutral and in place (the newcomer takes the victim's tensor pointers, no new upload); evicted experts are reported once, with their layer; with overwrite refused the swap still lands by free-then-upload;qt_stats_marksnapshots.withdraw,fill_wait,invariants,int8_engine, ...) unchanged and green; CPU-only build (make qwen36) carries the inline stubs;colibri(the DLL loader) builds.QT_PREFILL_REPLAN=0against=1: the tokens are not byte-identical (the eight pairs above diverge between byte 8 and byte 767 of the text). That is the non-identity any change of residency has today -- a different heat file, an LFRU swap -- because a resident expert (GPU kernel) and a CPU miss (int8 slot) do not sum in the same order and a greedy route flips; measured in qwen36: offer the rest of the dense trunk to the VRAM placer (dnout, attnproj, shexp) #1657's thread, two runs of the same configuration with a frozen heat file are byte-identical, two configurations are not. Routing itself is untouched: the re-plan only decides where an expert is computed.Logs, traces (2.6 GB) and the offline scripts are in my workspace; happy to attach any of them.
Thanks to Claude Code for the help along the way.