Repository navigation
attn: skip a plan the wrapper already holds, in the ragged base - #1076
Conversation
NSagan271
left a comment
There was a problem hiding this comment.
LGTM, pending some live testing
Every caller builds cu_seqlens on the host, so the plan cache always reads the layout back.
Gaurav-Shah05
left a comment
There was a problem hiding this comment.
I pushed the two small points on top: the device-layout guard is gone, since every caller builds cu_seqlens on the host, and a minor comment edit.
I tested it with configs/cosmos3_edge.yaml one 832x480, 121-frame, 20-step video at a time, two server sessions per arm, against main at 897eca5:
main |
branch | |
|---|---|---|
| DiT step | 226.3 ms | 225.8 ms |
Every video is identical between the arms.
Live test: #1076 at
|
| model (ragged attention user) | result |
|---|---|
BAGEL, cfg_parallel |
Every phase (solo, concurrent, storm, solo again) was byte-identical across all 4 servers. Repeating a solo request on one server matched 3/6 on every server alike, since the first request takes a different path. |
| Cosmos3-Edge | Solo, and solo again after the storms, 8/8 identical across all servers. Concurrent 4–6/6 vs solo on both arms (batching nondeterminism). |
FLUX.2-klein, attention_backend: flashinfer |
Solo vs solo again 6/6 on every server. Across servers 3 of 6 images differ at ~60 dB on both arms, and main's second server was byte-identical to the PR's second server: cross-process noise. |
| Qwen3-Omni colocated (encoders on ragged attention) | Speech 10/10 on every server. Cross-server text agreement 14–18/20 on both arms alike. Storms clean. |
| Qwen3.5 0.8B (multimodal) and Qwen3-ASR | Greedy text identical across all servers. Storms clean. |
No request failed on any server.
Performance, p50 latency (s), main servers → PR servers
| bench | main | PR |
|---|---|---|
| Omni a2s | 1.59, 1.74 | 1.86, 1.64 |
| Omni a2t | 0.50, 0.48 | 0.51, 0.46 |
| Omni i2t | 2.56, 2.75 | 2.73, 2.64 |
| Qwen3-ASR | 0.20, 0.21 | 0.20, 0.20 |
| BAGEL t2i | 52.9, 53.1 | 53.4, 53.0 |
Qwen3.5 0.8B: worker CPU overhead (text tok/s; 128–384 output tokens with ignore-EOS)
| main | PR | |
|---|---|---|
| t2t c=8 | 3181, 3094 | 3115, 3147 |
| t2t c=32 | 7516, 7320 | 7596, 7445 |
| t2t c=128 | 13227, 12973 | 12328, 13750 |
| i2t c=16 | 5233, 5058 | 5339, 5155 |
| i2t c=64 | 10242, 12358 | 12365, 12677 |
Every pair is inside the run-to-run spread. 640/640 requests at c=128 succeeded on every server, with no errors in any server log.
Not run
- FLUX.2-klein throughput: the benchmark runner has no klein model choice. The seeded klein correctness run above covers the attention path.
- Z-Image: no weights on this machine.
Main brings #981 (per-rid forward failures), #985, #367 and #916 (chunked prefill as a graph Loop), #988 (Qwen3.5 on Blackwell), #977, #857 (stop policies checked in the graph runtime), #852, #1076, #943, #1077 and #938. Resolutions: - Piecewise leases: main's #977 design. A forward replays a lease_before_step region exactly when its label is in ModelInputsFromEngine.piecewise_leases; #859's engine-side filter (SubmoduleManagement.runners_for) and its tests go. The forwards #859 had moved onto runner presence check the lease again: Omnilingual's encoder loop and OmniVoice's backbone (both new here) as main's do, and V-JEPA2 and the FastConformer stream take main's code as it is. - Qwen3.5: main's token_stop (runtime_check) beside #859's DeviceLoopback; main's _splice_row_ids replaces both loop-back paths in preprocess; decode builds its rotary tables in the graph always (#859), text prefill and mixed steps under ingraph_decode_rope (#988), whose knob entry, docstring and docs now say so; main's compile_deterministic and speculative_input_seq_len. - TokenStop carries #859's withhold and #857's runtime_check. - Orpheus: #859's TokenStop with runtime_check, so the runtime derives its EOS policy (the loop cap is the budget, as its check_stop has it), in place of #857's hand-written AR policy; test_stop_policies passes its TokenStop. - Engine: chunking imports beside cudnn_benchmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…generation Conflicts: - attn dense/ragged wrappers, test_dense_attention: main's #1076 landed the same skip-an-unchanged-plan change in the ragged base; took main's. - bagel: kept both sides' new LLMSubmodule methods; the chunked-prefill fork condition uses this branch's guided_rids. - wan22: main moved the dit onto the scaffold's UniPC denoise loop. CFG parallelism now splits the batch's branch stack over the sp group, and the ragged self-attention goes through the scaffold's attn_resource_key with one segment per branch this rank computes (attention_segments); the per-model declare_step and Wan22AttnLayout are gone. Tests ported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follows #963 (merged as bf70108). Measured against #963's head f109674 on B300, then rebased onto main 897eca5 with no change to the patch.
#963 skips FlashInfer's plan when a dense slot's layout hasn't changed, in the dense manager only. This moves the check into
_RaggedPrefillBase._plan, so every manager on that base gets it: ragged self and cross attention on FlashInfer and FA3, and the dense manager, which loses its own copy.What changed
_RaggedPrefillBase._plan(attn/ragged/wrappers.py). A wrapper keeps its last schedule until it's planned again andrunonly reads it, so a layout it already holds returns before any work. The layout it compares against is kept on the wrapper. Wrappers are per (label, slot) and per (bucket, slot, label), so each slot's check is against that slot's own schedule, as Wan2.2 step: one-pass adaLN per frame, fused q/k norm+RoPE, no host syncs; Cosmos3: keep the dense attention plan #963 described, never against the previous step._RaggedManagerBase.clear_preplan->forget_plan). Its H2Ds went on the plan stream and the step that runs instead has no event to wait on, so skipping would read the schedule with nothing ordering it against those copies. This comes from reading the stage order, not from a failure I saw. The block-causal wrapper plans every step and has nothing to forget.DenseAttentionManager._build_plandrops_plannedand always callswrapper.plan. It now builds its two pinned layouts per step instead of only on a change, about 6-9 µs on this box.TODO: cache the latest plan statein_plan_layoutsis gone.Performance
This doesn't move a served number, and the gate shows that. Captured steps plan on the plan thread, off the critical path, and a miss there is 0.2-0.3 ms. Where a skip costs real time is the eager FlashInfer path, and Cosmos3 already had that from #963. So this is about having one check instead of two, and every ragged model getting it, more than about speed.
What a skip saves, host time around
plan(), median of 200 (B300, FlashInfer 0.7.2.post1):autoautoGate on 1x B300 (sm_103, 1100 W cap), torch 2.12.1+cu132, FlashInfer 0.7.2.post1,
-O2,MSTAR_SHM_ARENA=1, server pinned to 8 cores and client to 4, fresh servers alternated with #963's head, one warm Inductor cache, no compaction stalls or recompile lines in any counted session. Step is DiT ms per step: (compute span − other nodes) / forwards. Wan runsconfigs/wan22.yamlpluscompile_dit_sizes: ["832x480x33"]; Cosmos3-Edge runs the stock config. Both video models at 20 steps.Three models needed something beyond their stock config to reach this code at all, the same in both arms:
attention_backend: sdpa, which plans nothing, so they runflashinferhere. Graph mode then needsMSTAR_WORKSPACE_BUFFER_MB=2048: FlashInfer's plan wants 866 MB at bs=8 against the 512 MB default.flashinfer_backend: cudnn. Its plan wants 153 MB on B300 against the 128 MB it declares, and that value has no YAML override.Image concurrency is 8, video and LTX 4. LTX-2.5's c=4 p50 reads about 4% higher here, with throughput down 1%; two servers per arm can't separate that from its own spread (8.87-9.26 s on #963 alone), and a plan there costs 0.09 ms. Wan and Cosmos3 are the controls here: Wan declares no attention resource, and Cosmos3 already had the check. Both arms ran at the same clocks: c=1 at 1485-1552 MHz and 1049-1057 W on Cosmos3, 1522-1755 MHz and 854-903 W on Wan, 2025-2032 MHz on klein, about 1965 MHz on Z-Image, about 1610 MHz on LTX-2.5.
FLUX.2-klein's c=1 p50 reads worse and isn't. The DiT step is the same, but across servers p50 went 0.39-0.43 s on #963 and 0.42-0.47 s here, in both orders of the pair. That gap is in the text encoder, which is bimodal, about 40 or 100 ms per request regardless of prompt, and on this box's powersave E-cores that's a core at 3.2 GHz or at 800 MHz. Two checks:
Turning the skip off doesn't close it, and with the governor out of the way the arms are equal. So it's how often a request lands on a parked core, which varies by server, and not this change.
Correctness
Same-seed outputs, against #963, with #963 against itself as the yardstick:
At concurrency a row's output depends on which rows share its forward, so servers differ with or without this change. That batch makeup is the cause is my reading of those numbers; I didn't log it per step. The lowest figures for klein and LTX-2.5 sit just under #963's own, by 0.8 and 0.7 dB, over a handful of files in a spread that wide.
Tests
ruff check .: clean.python -m pytest test/modular test/distributed test/chatterbox test/kokoro test/omnivoice test/sampling_test mstar/model/*/tests benchmark/worker_phases, CUDA hidden,-p xdist -n 8): Wan2.2 step: one-pass adaLN per frame, fused q/k norm+RoPE, no host syncs; Cosmos3: keep the dense attention plan #963's head 50 failed / 8809 passed / 801 skipped; this branch 51 failed / 8845 passed / 801 skipped. 49 failures are shared by test id; the rest are the eval family below. The fivetest_eval_compare*cases that flipped between trees (they run fake servers under timeouts) all pass on each tree run alone on an idle box.test_piecewise_config_signatureandtest_tp_async_lockstep_simran serially on both and are counted above.test_flex_attention_resource,test_waypoint_componentsandtest_waypoint_ditcrash in native code on this box's CPU on both trees and aren't counted.test_ragged_plan_cache.py, 38 tests on mocked FlashInfer and FA3 wrappers, every manager on this base: reuse, the miss, each slot owning its schedule, captured wrappers, promoted and dropped pre-plans, a plan that raises, a device layout, and a dropped block-causal pre-plan. Mutating the skip, the clear before planning,forget_plan, the device guard andcausalin the key each fails it.test_dense_attention.pyloses its copy of the reuse test.test_ragged_plan_cache,test_ragged_attention,test_ragged_dense_attention,test_ragged_attention_cpu,test_dense_attention,test_attn_backend_auto,test_cross_attention_plan: 184 passed, 25 skipped, 3 failed. The same 3 fail on Wan2.2 step: one-pass adaLN per frame, fused q/k norm+RoPE, no host syncs; Cosmos3: keep the dense attention plan #963's head (test_piecewise_*intest_ragged_attention, an error of 0.03125 against a 0.03 limit).Not in this PR
RaggedBlockCausalWrapperisn't on this base and still plans every step.workspace_mbhas no YAML override, and on B300 graph-mode FlashInfer wants more workspace than some of these models allow (klein 866 MB at bs=8 against the 512 MB default, LTX-2.5 153 MB against its declared 128 MB). So stock LTX-2.5 can't boot onflashinferhere. That's worth its own fix.