Repository navigation
perf: sampler loop-back masters per node (fixes Whisper), KV plan cache, page-locked index arrays, in-graph decode knobs - #396
Conversation
…per-request frames
…ts wait for slot init
# Conflicts: # docs/environment_variables.rst # mstar/worker/worker.py
…o perf/host-overhead-3
|
Re-merged on top of #380's re-merge (69abae2) and checked on H200s on 2026-10-12: 0.8B gate clean (parity 17 / 17 / 16 of 64 at N=1/8/32, fixed-8, stop-sequence check, prefill padding, long prompts, abort storm, bad requests all as before), 0.8B cells 8947 / 12995 / 15731 tok/s at c32 / c64 / c128 (the clean-instance range of the round-3 table), 9B TP2 gate clean with c8 / c32 at 2029 / 5637. Whisper on this head vs main, each alone on its own node, same day: 5.78 / 4.39 / 3.26 vs 4.03 / 4.12 / 3.20 req/s at c1 / c4 / c16, so the fix holds after the re-merge (c1 first-token p90 102 ms vs main's 427 that day). Both trees ran well under their 10-09 numbers at c16 that afternoon, with the response landing in one piece, which looks like the cluster on the eve of its maintenance window rather than either tree; worth a quiet-day re-check. |
|
Default path and the Whisper fix look good (tested on h200s) Needs a rebase onto #380
LGTM otherwise |
…sharing of plan output state to avoid races
stephen-dwq
left a comment
There was a problem hiding this comment.
INGRAPH_DECODE_TOKENS=1 has captured graphs reading freed memory because of reallocation of last_token.master after it has been captured in a graph. It must therefore not grow after anyone captures it.
We can resolve by either removing the INGRAPH_DECODE_TOKENS option or by pinning a really big last_token.master and raising an exception if we ever have to assign past the really big limit.
… hook to point dummy rows at designated trash slot; trash slot is now slot 0 universally
… plan _setup_plan_states read indptrs.views even when the cached plan supplied decode_pages/decode_offsets, which built the views every step and defeated the lazy builder. test_views_built_after_the_next_preplan_keep_their_step caught it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main carries #380, the squash of perf/host-overhead-2. This branch had merged -2 at d2137a5, so conflicts were re-merged against that commit as the base rather than the pre-squash merge base. - sampler/utils.py: kept this branch's slot-0 trash row (rows = capacity + 1) and took main's host-master lock, gpu-thread device grow and `keep` mask; main's grow arithmetic moved onto the +1 row layout. - qwen3_5 preprocess: main's routed-row mixing plus the rope knob gate. - knobs/docs: dropped MSTAR_TP_EARLY_SPEC (N+2 speculation was reverted) and MSTAR_GC_FREEZE (always on in main). - removed test_worker_launch_signal.py and test_tp_early_spec_decision.py, whose knobs main removed; cpu-core.txt is the union of both lists. - test_sampler_trash_row: device masters now grow on the next sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps off one host copy The Higgs LLM decode step adopts the hooks qwen3_5's decode already uses (mstar-project#368, mstar-project#396): the sampled token stays in the sampler's slot master and is read back in `preprocess` (device loop-back), the client's copy rides inline off the stop check's host rows, the forward returns row-addressed outputs (one clone and one D2H per step), and `check_stop_batched` checks the batch off one `tolist`. The decode loop now routes one signal, `text_inputs`, to itself and to the client, so a decode step stores no tensors at all. At c=32 the worker was CPU-bound (event_sync ~0, ~5.2 ms/iteration for ~15 rows, a third of it per-row stop checks, output staging and routing). H200, LibriSpeech test-clean, 2 rounds interleaved with the previous head: c=32 warm 84.4/89.0 -> 95.0/97.2 req/s, c=32 x600 87.2/93.4 -> 99.0/97.6, c=8 52.1/54.5 -> 53.5/53.6, c=1 15.0/15.7 -> 15.4/15.6 (flat). WER 2.649/2.673% -> 2.649/2.680% (x600); c=1 transcripts identical, c=32 594-595/600 identical vs the previous head (run-to-run of each arm 593-598). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Third pass over the host side, on top of #380. Smaller than the first two rounds: the remaining per-step Python is
spread thin, so this one is a handful of targeted changes behind knobs, plus one fix the cross-model check turned up,
each gated the same way as before (greedy parity vs HF at N=1/8/32, fixed-8, the stop-sequence check).
every request's last token for every node, and on an eager decode step the write behind it builds its slot index with
torch.tensor(..., device=cuda): a synchronous host-to-device copy per step. Whisper's decoder samples eagerly andreads its tokens on the host, so it paid a stream sync per decode step for nothing (Qwen3.5's captured decode never
did). A submodule now declares
reads_device_loopback(the Qwen3.5 LLM node does) and the engine enables thesampler's masters for that node alone. Whisper numbers below.
MSTAR_KV_PLAN_CACHE, default off). When a captured decode step hasthe same rows as the last one and only that step's commit touched the streams, the KV manager advances lengths by
one, refreshes the rows that crossed a page boundary and builds FlashInfer's index arrays with numpy instead of
walking every stream. Anything else that touches a stream (resets, removes, reloads, offloads, rollbacks, releases,
forks, a dropped pre-plan, a commit of a cached row in another batch) invalidates it; another request's prefill does
not.
=2runs both plans every step and logs any difference; the gate ran that way.Numbers: neutral on 0.8B at c32/c64/c128 (three fresh instances per side on one H200: 8921 / 13024 / 15912 vs 8911 /
13036 / 15504 tok/s for the best clean pair), and the kv plan phase itself does not move (the batch changes every step or
two under that load, so there is little for a cache to reuse). Kept as an opt-in with the verify mode.
MSTAR_KV_PINNED_INDPTRS, default off: measured neutral). FlashInfer'splancopies themwith
non_blocking=True, which from pageable memory is a staged copy, and it makes the indices copy blockingoutright; the decode wrapper now copies the indices straight into its captured buffer from the ring.
Numbers: neutral to slightly negative (12551 / 15273 vs 12366 / 14903 tok/s at c64 / c128 on one pair; the event
bookkeeping per take costs about what the staged copies did), so it is off.
MSTAR_INGRAPH_DECODE_TOKENS,MSTAR_INGRAPH_DECODE_ROPE, both off). Round2 reverted this because outputs broke; the cause was the piecewise capture calling the region with the slot's
static inputs and no step, so the in-graph reads were never recorded. Fixed, the tokens half gates clean; the rope
half lowers batch-vs-alone agreement and both together trip a device assert at bs 64, so they stay off.
MSTAR_KV_CHAIN_LAZY_STEPS, default 1 = as before). Measured a wash; kept as a knob.PiecewiseCudaGraphRunner._call_inputs).Not in this PR, written up in the report: speculation reuse (analysed), the stop check in Rust, the TP2 c8
follower waits (located, trace in notes/tp_c8_trace.md), startup compile time (measured: CuTe DSL kernels compile in
every process, no file cache in nvidia-cutlass-dsl 4.8).
Cross-model check (klone H200, main vs #368 vs #380 vs this branch, each server alone unless noted; full tables in the
round-3 report):
19.0 s (vLLM-Omni 20.5 s). The 2x on video came with worker: device loop-back, rows-only steps, leaner API delivery (round 2) #380 and holds here.
5.6 req/s at c32 on 26 requests, a gap that predates these branches.
5.5; a two-instance re-check of main vs this branch on a quiet GPU gives c1 5.4 vs 4.7, c4 5.6-6.1 vs 4.7-5.5, c16 6.1-7.2
vs 4.9-5.2. Whisper is a large audio prefill plus a few dozen output tokens, so per-request fixed cost dominates and the
branches added one: the sampler's eager-step master write (above). With the fix, fresh instances alternating with main:
main 5.5 / 4.0 / 4.3 then 5.6 / 6.2 / 7.1 req/s at c1 / c4 / c16, this branch 5.3 / 5.4 / 7.2 then 5.2 / 5.7 / 6.1; the
c16 loss is gone and so is the c1 first-token tail (p90 114-142 ms vs 286-345 before, main 104). Gate on the fixed
tree clean.
on klone); Orpheus: gated checkpoint.
Qwen3.5 head-to-head with #380 (same GPU, fresh server instances): equal at every concurrency on 0.8B and 4B, and on
9B TP2 c8 with long cells. Against the other systems on the same GPUs, same day (output tok/s at c8 / c32 / c64 / c128,
256-token requests, best of two fresh instances for mstar):
So ahead of vLLM at every size (40 % on 0.8B, 1-16 % on the others) and within -7..+12 % of SGLang. One thing worth knowing for anyone benchmarking this: a server instance comes up in one of two states
that last its whole life, where the worker's main thread loses either ~0.06 or ~0.16 ms per step between iterations
(about 7 % of 0.8B tok/s at c32, nothing at c64+). It is not tied to the branch (main, #368, #380 and this branch all
show both states), so compare state-matched instances or several instances per side; tools/phase_idle.py in the
perf notes measures it.