Skip to content

serve: a prompt's last call runs its last id as the call's last row on the Qwen3.8 seat - #1025

Merged
juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:dgx-spark-q38-next-prefill-pass
Oct 30, 2026
Merged

juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:dgx-spark-q38-next-prefill-pass

Conversation

@juliankang4

Copy link
Copy Markdown
Collaborator

Qwen3.8의 프롬프트 마지막 호출이 마지막 id를 마지막 행으로 함께 처리해 prefill과 TTFT가 약 2% 빨라집니다.

What and why

The server fed a prompt less its last id, then ran that id as a separate step. Engine::prefill_next now hands the engine the prompt's last call and that step as one call; its default is the same two calls as before. The Qwen3.8 seat overrides it: the last id runs as the last row of the call's last batch, whose head gives the first token, and the end checkpoint is sealed before it.

When that id completes a delta chunk, the call keeps the chunk out of the lane, so a delta carry now holds up to 16 rows (DELTA_CARRY_ROWS) and the next step folds it. A 4,096-id prompt takes this path too. The carry is 33,152 B larger per GDN layer, 1,193,472 B over Qwen3.8's 36 layers, which moves the placement pins and fits 28 checkpoints in the host budget where 29 fit.

A multi-pass call, a last batch that would hold the id alone, and the MTP draft on more than one slot keep the old two calls.

Release notes: Changed: Qwen3.8 prefill and time to first token are about 2% faster on a 4,096-token prompt.

Code shape

  • No second copy. The closing call's checkpoint seal shares seal_end_mark_with (crates/gpu/src/arch/qwen3moe/body38.rs:3320) with the end mark's.
  • Model code holds only architecture facts. The Qwen3.8 override sits in its own seat; the common Engine trait gained one method whose default is the old two calls.
  • Dead code is gone. fold_closes and its carry-header read are removed.
  • No silent failure. A kernel launch with a short carry is a named error.
  • Comments state what is true now.
  • No relaxed gate. Each re-pinned value carries PIN(2026-10-30): and its derivation; no band or tolerance moved.
  • cargo fmt and cargo clippy are clean on what I touched; the lint count did not go up.

Duplication found or removed (path:line): none left; the copy of the end mark's seal was merged into seal_end_mark_with.

What I ran

Speed, bloomery-serve on Qwen3.8-Flash-Next-FP4-Local: a 4,096-id prompt, 128 generated tokens, 8 windows, three rounds after one warm-up, draft off and MTP on. Base 5c9f5d78f against head bab953d55, both built from source.

Metric Base Head Difference Ranges overlap
draft off / code-fn, prefill_tok_s 2773.19 2829.86 +2.044% no
draft off / code-fn, ttft_p95_ms 1482.53 1453.00 +1.992% no
draft off / code-fn, decode_tok_s 31.77 31.57 -0.628% yes
MTP / code-fn, prefill_tok_s 2609.75 2668.42 +2.248% no
MTP / code-fn, ttft_p95_ms 1574.24 1540.65 +2.134% no
MTP / code-fn, decode_tok_s 59.38 58.85 -0.884% yes
MTP / prose-en, prefill_tok_s 2652.48 2707.01 +2.056% no
MTP / prose-en, ttft_p95_ms 1549.68 1517.91 +2.050% no
MTP / prose-en, decode_tok_s 42.26 42.73 +1.105% no

Prefill and TTFT gain in every arm with the ranges apart. Decode shows no resolved change. The server log shows 1,193,472 B more resident memory on head, the larger carry. In the MTP arm on code-fn the greedy ids first differ at token index 12 of request 1 (6563 on base, 1715 on head), since the first token now comes from the last row of the batch instead of the separate step.

Kernel: gate_linear clause 16 holds a kept last chunk, the calls after it and a step on the 16-row carry to the one-call result.

just gate-gpu-linear
a prompt of 32 keeping its last chunk: o same=196608/196608 max_ulp=0 (the call's), lanes same=3145728/3145728 max_ulp=0 PASS
a step on the kept carry of 16 rows: o same=6144/6144 max_ulp=0 lane 2 same=786432/786432 max_ulp=0 PASS
prompt launcher refuses a carry one short at m=16: "enqueue_delta_prompt: carry.len() 132611 < 132612" PASS
gpu-gate.sh: gate_linear ran 16 s (exit 0)

Model: the closing call against the one call, on Qwen3.8-Flash-Next-FP4-Local. Before this change the 32-id case ended with the last ubatch 1 rows (folds false); after it:

just gate-gpu-qwen4exp-mtp-input
(cl) 21 ids: the closing call whole = the one call's argmax and logits true, its steps' bits true (held true)
(cl) the last id rides the ubatch walk at 21, 32 and 5 ids from a reset (the seat steps it otherwise): [true, true, false] PASS
(cl) a drafted run whose prompt call closes: the draft rejoins at 20 true
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out

Placement pins and the end-to-end gate:

just gate-qwen4exp-meta
just gate-gpu-qwen4exp-e2e
gate-qwen4exp-meta rc: lib 0 reserve 0 meta 0 mtp 0 host 0 bp 0
structure layers=4 selecting at [1, 3]; store bytes 22521384 (want 22521384, derived) PASS
clauses: 27 ran, 9 left to the real tier
gpu-gate.sh: gate_qwen4exp_e2e ran 75 s (exit 0)

The carry-size derivation in gate_qwen4exp_e2e also matches the store on the real tier: structure layers=48 ... store bytes 237766352 (want 237766352, derived) PASS.

Hardware (if the change is for a machine)

Card Driver CUDA
NVIDIA GB10 595.84 13.3
NVIDIA GeForce RTX 5090 595.99.02 13.2

The speed A/B ran on the GB10; the end-to-end gate ran on the RTX 5090.

The kernel and model gates ran on DGX Spark machines of the same kind as the A/B.

AI help

AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.

…n the Qwen3.8 seat

The server fed a prompt less its last id, then stepped that id. `Engine::prefill_next` hands the engine the last call and that step as one call; its default is the two calls as before. The Qwen3.8 seat overrides it where the last id rides the ubatch walk (`GpuModel::closes_by_ubatch`): the body's closing call (`Body38::set_close`) runs the id as the last row of the last ubatch, whose head gives the argmax, and seals the end checkpoint before it, each delta carry given back that row.

When that id completes a delta chunk, the walk's last unit keeps the chunk out of the lane (`enqueue_delta_prompt`'s `keep`), so a delta carry now holds up to a whole chunk, 16 rows (`DELTA_CARRY_ROWS`), and the next step folds it. A 4,096-id prompt now folds too. The carry is 33,152 B more a GDN layer, 1,193,472 B over Qwen3.8's 36, which moves the placement pins and fits 28 checkpoints in the host budget where 29 fit.

A call of passes, a last ubatch that would hold the id alone, and the MTP draft on more than one slot keep the call less the id and the id's step: a slot change clears the draft's waiting row in the ubatch arena. Under steps the closing call now feeds its last id after the end mark. gate_linear's clause 16 holds a kept call and the calls after it to the one call bit for bit, and a step on the kept carry to the one-lane rule; the checkpoint folder gate's (cl) clause holds the closing call to the one call's first token, the cut call to the whole call and the step from the end mark to the former cut.
@github-actions github-actions Bot added gpu The card engine: crates/gpu*, crates/app host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja needs-gpu Changes code a card must check, and carries no gate run on a card placement Placement planning: crates/placement serve The HTTP server: crates/serve tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo labels Oct 30, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: CLEAN: No validated must-fix items remain.

Non-blocking

⚠ Complete the performance evidence: qwen38.rs:3324. The PR supplies gate output, a hardware table and timing summaries, but omits the nvidia-smi line and exact timed A/B commands with output required by CONTRIBUTING.md. Attach these for both measured commits to support the changed prompt dispatch's speed claim.

Checked

Duplicates: dryer reports zero qualifying pairs in its searched crates; GPU crates were excluded from that search.

Complexity: diagnostics flag new gate helpers and closing-checkpoint branches. Complexity alone does not establish a defect; the kernel's added keep condition does not warrant a split.

Silent failure: closing-call and checkpoint-seal errors propagate; the prompt launcher checks buffer sizes.

Tests, static inspection: keep_clauses checks kept output, state and carry; close_case checks rollback equivalence; the new serve test asserts that prefill_next receives the whole prompt.

Gates: changed carry and placement pins include dated derivations.

Escape catalog: C1–C4 do not match the cited Rust dispatch path.

Notes

Static validation only; builds and hardware gates were not rerun. gpu.json records evidence: false for the incomplete evidence.

Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_linear.rs, crates/gpu-gates/src/bin/gate_qwen38_serve.rs, crates/gpu-gates/src/bin/gate_qwen4exp_e2e.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@juliankang4

Copy link
Copy Markdown
Collaborator Author

The review is done and the contribution check passed, so I'm merging this.

@juliankang4
juliankang4 merged commit 30b1ec4 into midagedev:main Oct 30, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gpu The card engine: crates/gpu*, crates/app host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja needs-gpu Changes code a card must check, and carries no gate run on a card placement Placement planning: crates/placement serve The HTTP server: crates/serve tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant