Skip to content

qwen3_5: run prompts that share prefixes once per prefix - #99

Draft
twu3202 wants to merge 3 commits into
ThinkFlowLab:mainfrom
twu3202:prefix-shared-executor
Draft

twu3202 wants to merge 3 commits into
ThinkFlowLab:mainfrom
twu3202:prefix-shared-executor

Conversation

@twu3202

@twu3202 twu3202 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

For #85, the shared executor step of the plan (§3; PR 3 in §7), as revised in this update. It builds on #97 and #98 and stays a draft until they are merged and the backend path is settled with #96. This PR's own change is the last commit, 3601009. No worker change.

Model::forward_shared runs prompts that share token prefixes: a request prefix, then groups with their own prefix and branches. Each prefix runs once, and each branch continues from the state after it, with positions and the causal mask continuing too. It returns one final hidden state per branch, in order.

  • State: per linear-attention layer, the float32 recurrent state and the last three conv inputs; per full-attention layer, the prefix keys and values, followed by the current branch's rows. A request snapshot and a group snapshot are separate buffers. Like the scratch, they persist across calls and grow only for a longer prompt. Their contents are valid only within one call, which runs eagerly and synchronizes its stream before returning, also on error.
  • Split points: each prefix ends at its last multiple of 64 tokens, and its remaining tokens run with every group or branch after it. With a split inside a chunk, the gated delta rule rounds differently: up to 1.25e-2 relative L2 in the final hidden state on 4B and 1.20e-2 on 9B against forward_fixed, in an earlier version of the tests below that also ran exact splits. The result would also depend on the prompt's siblings, since they set the common prefix. Rounding down avoids both, at the cost of up to 63 extra tokens per branch.
  • GEMMs: the shared runs use the fixed-algorithm handle from qwen3_5: add a GEMM handle with one algorithm per weight shape #98 (reference M 64), so each result equals forward_fixed, a plain pass over the full prompt with the same handle, bit for bit, however the prompts are grouped. It differs from forward, which picks algorithms per row count, by rounding. forward, forward_multimodal and CUDA Graph replay are unchanged: the layer loop takes the GEMM handle and an optional span, and without a span runs as before.

docs/architecture.md, the runtime README and the backend README now describe the entry point, its state and its extra memory.

Test Plan

System1-Omni Version / Commit: 3601009 on #98 (f095f07), on main 47eff9c. The tests and the plain-forward comparison ran on this code. The timing ran earlier, before the last review fixes (comments, tests, documentation, and a stricter split-K check in #98) and before the rebase onto #64, which moved embedding and scratch setup into helpers that this PR now uses. A span queues the same kernels in both.

CPU: the five Rust steps in CI (format, Clippy, frontend tests, workspace tests, release build), mkdocs build --strict, and a new test of how prompts split into runs.

GPU, one RTX 6000 Ada (sm_89), on the Cua-S1 4B and Open-Jev-9B exports, with random token ids:

  • forward_shared against forward_fixed on every full prompt: request prefixes of 0, 1, 63, 64, 65, 300 and 1,000 tokens, groups with 0, 1, 70 and 128 prefix tokens, and branches of 1 to 200 tokens;
  • state isolation: the same request twice, after a longer request that grows the buffers, and with groups and branches reversed; forward before, between and after the shared runs; invalid requests (no groups, an empty group or branch, out-of-vocabulary ids anywhere) refused, after which the model still gives the same results;
  • the same with CUA_S1_GRAPH=1 on 9B, where the plain forward between shared runs replays a CUDA Graph;
  • the plain forward on this branch against main, Cua-S1 and Open-Jev-9B workers side by side;
  • timing of M1-like layouts on 9B.

Test Result

CPU. All six commands pass on macOS, including the new split test.

forward_shared against forward_fixed. Every branch is bit-identical, at every request prefix length and group shape above, on 4B and 9B, and on 9B with CUA_S1_GRAPH=1. Against forward, the difference is that of the GEMM algorithms alone: up to 1.84e-2 (4B) and 1.53e-2 (9B) relative L2 on these random prompts. That is above the plan's 1e-2 hidden-state gate and inside #92's native vs reference spread (p99 0.023), so the update proposes how to treat that comparison.

State isolation. Every check passes, and the model gives the same results after the refused requests. All of the invalid requests fail validation before any GPU work, so the error path's synchronization isn't exercised by these tests.

Plain forward unchanged: Cua-S1 567 of 567 byte-identical (134 of them forward passes) in eager and CUDA Graph modes, and Open-Jev-9B 253 of 253 identical apart from timing, against main 47eff9c.

Timing (9B, random tokens, one warm call each, one request prefix and 38-token branches):

Request prefix Tokens rerun per branch Branches forward_shared One forward per branch
3,072 0 255 6.13 s 121.28 s
3,100 28 255 6.73 s 128.01 s
3,135 63 255 7.23 s 128.69 s
1,024 0 32 0.85 s 5.26 s
1,050 26 32 0.87 s 5.31 s
256 0 255 5.31 s 14.87 s

A single pass with the fixed GEMMs takes 3% less time at 64 tokens, 8% more at 1,024 and 18% more at 3,072 than forward; the prefix pass carries that cost once per request. These are synthetic layouts with random tokens. M1's real 3,093-token prefix would rerun 21 tokens per branch. The plan's measurement on real Open-Jev requests comes with the worker PR.

Demo / evidence

No demo: no worker uses the entry point yet. Test output and scripts are kept locally.

Self-review

  • I have reviewed the full diff and addressed the issues I found.
  • I have checked that the change follows the project's architecture and stays focused on the stated purpose.
  • I have run the checks appropriate to this change and reported commands, results, and anything I could not verify above.
  • I have checked that the PR description, documentation, and any accuracy or performance claims match the implementation and available evidence.

Add CUDA operations that continue a sequence after a shared prefix,
for request-local prefix reuse (ThinkFlowLab#85): the Gated DeltaNet conv with the
inputs of the three positions before it, the chunked prefill from and
to a float32 state, gated attention for queries after cached keys, and
a pitched device-to-device row copy. The existing conv, prefill and
gated attention are these operations without history, state or cached
positions. Bump the ABI to 6 and add the Rust bindings and GPU tests
against the unsplit calls.
cuBLASLt's heuristic picks an algorithm per M, so a row's result can
change with the number of rows in the call, and a prefix and its branch
run separately round differently from one pass over the same tokens.
Add cs1_gemm_create_fixed: one algorithm per weight shape (N, K, ldy),
the heuristic's first choice at a reference M among algorithms without
split-K, for every M. The default handle is unchanged. Bump the ABI to 7
and add a GPU test that each projection's rows match across M.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant