Skip to content

qwen3_5 cuda: a chat's next turn resumes at the previous turn's history (message starts for the planned cache) - #1335

Open
gilby wants to merge 1 commit into
ashhart:mainfrom
gilby:qwen-cuda-turn-starts
Open

gilby wants to merge 1 commit into
ashhart:mainfrom
gilby:qwen-cuda-turn-starts

Conversation

@gilby

@gilby gilby commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1334 (draft until the arithmetic question there is settled).

A CUDA family that also cuts its cold prompt passes at request chunk starts declares request_starts; for such a family the native host advertises its grid as prefill_step, so the chat server sends message starts and the planned prompt cache keeps and resumes at them. Qwen 27B and its MoE sibling opt in: cuda_prefill.zig already cuts at stream.chunks (nextCut) and resumes only where a cold pass cuts (coldCut). GLM's CUDA prefill does not read the request's chunks, so it is unchanged.

Changes

  • native/cuda_snaps.zig: requestStep(F) (the family's snap_grid when it declares request_starts, else 0), with a test.
  • native/cuda.zig: .prefill_step = snaps.requestStep(F) in the served info.
  • qwen3_5/cuda_native.zig, cuda_native_moe.zig: request_starts = true.

Receipts (one DGX Spark, GB10 128 GB, driver 580, CUDA 13.0; TensorFold/Qwen3.8-27B-MLX-4bit @ 22d8d538 + z-lab/Qwen3.8-27B-DFlash2, --parallel 4 --context 262144; main and this branch alternated in one session)

  • Multi-turn chat, first-token time per turn (s), six turns, ~200-token greedy replies, thinking off:

    1 2 3 4 5 6
    ~1.8K chat, main 1.47 1.58 1.75 1.96 2.17 2.28
    ~1.8K chat, this branch 1.74 0.24 0.67 0.23 0.69 0.23
    ~19.5K chat, main 16.68 3.00 3.21 3.31 3.56 4.61
    ~19.5K chat, this branch 17.68 0.28 1.30 0.28 1.33 0.28

    Every reply with the cache on equals the reply with --prompt-cache-gib 0, on both builds.

  • bench_concurrent --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serial, two runs each: 0 unequal, 0 failed. Code 68.8 / 69.0 tok/s at 1 stream and 186.3 / 186.3 at 4; chat 50.0 / 49.9 and 141.4 / 140.7.

  • bench_cache --require-cold --require-reuse does not qualify on either build (a raw prompt has no message starts; see Qwen 27B on native CUDA: a chat's next turn never resumes at the previous turn, only on the 4,096-row grid #1334).

  • zig build test: 500/500 steps; zig fmt clean; lean_check.py adds no problems over main.

Arithmetic

A cold pass now also cuts at message starts, so multi-message prompts' outputs differ bitwise from main's (0 of 12 fresh replies matched), each exact under the contract; a first turn costs ~6% more. Qwen27 on Metal already cuts at request starts.

jschmied added a commit to jschmied/TensorFold that referenced this pull request Oct 26, 2026
…arts and requests that name none

The server sends message starts once ashhart#1335 lands, so one conversation's turns can
name chunk starts while another's (or an older server's) name none. Each case
writes a state, restarts and reads it back from disk with the other kind of
request: a state written with starts on the grid serves a request that names
none; one at 1000, or at 1024 with its chunks below cut at 300, serves none; a
grid state serves named starts that hold the grid's below it and its own
position, and no others. Every resume is a fresh pass's bytes.
jschmied added a commit to jschmied/TensorFold that referenced this pull request Oct 26, 2026
…arts and requests that name none

The server sends message starts once ashhart#1335 lands, so one conversation's turns can
name chunk starts while another's (or an older server's) name none. Each case
writes a state, restarts and reads it back from disk with the other kind of
request: a state written with starts on the grid serves a request that names
none; one at 1000, or at 1024 with its chunks below cut at 300, serves none; a
grid state serves named starts that hold the grid's below it and its own
position, and no others. Every resume is a fresh pass's bytes.
@ashhart

ashhart commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Thanks for tracing #1334 so clearly; the later-turn numbers are a big improvement. Resumed equals fresh, and
concurrent equals solo, on your receipts.

The difficulty is that this is on by default and changes the arithmetic of multi-message prompts. Fresh replies
differ from main bit for bit (0 of 12), and first turns are 6 to 18 % slower. Our rule is that anything new stays
off by default unless it is a speedup with identical tokens. Could you put request_starts behind an opt-in? That
way we can measure it, and decide in #1334 whether CUDA chats should cut at message starts by default. Two
interactions to know about. #1357's TF_QWEN_KV_SHARE applies only to requests without starts, so with this change
it would stop resuming chats. #1358 adds a line beside yours in cuda_native.zig and cuda_native_moe.zig.

A small one: the commit uses gilby@users.noreply.github.com, while your landed commits use
880013+gilby@users.noreply.github.com.

jschmied added a commit to jschmied/TensorFold that referenced this pull request Oct 27, 2026
…arts and requests that name none

The server sends message starts once ashhart#1335 lands, so one conversation's turns can
name chunk starts while another's (or an older server's) name none. Each case
writes a state, restarts and reads it back from disk with the other kind of
request: a state written with starts on the grid serves a request that names
none; one at 1000, or at 1024 with its chunks below cut at 300, serves none; a
grid state serves named starts that hold the grid's below it and its own
position, and no others. Every resume is a fresh pass's bytes.
jschmied added a commit to jschmied/TensorFold that referenced this pull request Oct 27, 2026
…arts and requests that name none

The server sends message starts once ashhart#1335 lands, so one conversation's turns can
name chunk starts while another's (or an older server's) name none. Each case
writes a state, restarts and reads it back from disk with the other kind of
request: a state written with starts on the grid serves a request that names
none; one at 1000, or at 1024 with its chunks below cut at 300, serves none; a
grid state serves named starts that hold the grid's below it and its own
position, and no others. Every resume is a fresh pass's bytes.
ashhart pushed a commit that referenced this pull request Oct 28, 2026
…arts and requests that name none

The server sends message starts once #1335 lands, so one conversation's turns can
name chunk starts while another's (or an older server's) name none. Each case
writes a state, restarts and reads it back from disk with the other kind of
request: a state written with starts on the grid serves a request that names
none; one at 1000, or at 1024 with its chunks below cut at 300, serves none; a
grid state serves named starts that hold the grid's below it and its own
position, and no others. Every resume is a fresh pass's bytes.
@ashhart

ashhart commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Hi @gilby, thanks again for this, and for the clear trace in #1334. Resuming a chat at the previous turn's history is something we want on CUDA. Following up on the earlier note, two things are needed before we can land it in 1.2.0:

  1. Please make request_starts opt-in, for example an environment variable or server flag that is off by default. With it off, Qwen 27B on CUDA should behave exactly as it does on main. As it stands, the change is on by default, fresh multi-message replies differ bit for bit from main's, and first turns are slower. Our rule is that anything new stays off by default unless it is a speedup with identical tokens. With an opt-in we can measure it, and settle in Qwen 27B on native CUDA: a chat's next turn never resumes at the previous turn, only on the 4,096-row grid #1334 whether CUDA chats should cut at message starts by default. When you add it, please check that a request carrying message starts still resumes with TF_QWEN_KV_SHARE (cuda qwen3_5: the prompt cache keeps KV pages on the device (TF_QWEN_KV_SHARE, #1156) #1357) set.

  2. Please re-author the commit as 880013+gilby@users.noreply.github.com, the address your landed commits use. The current one is gilby@users.noreply.github.com.

The branch also no longer merges cleanly with main. cuda_native.zig, cuda_native_moe.zig and cuda_snaps.zig now have neighbouring lines (kept_disk and a test import). Keeping both sides resolves it. Thank you!

@ashhart

ashhart commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Hi @gilby, a friendly check on this one. The main thing from our 27 Oct note is still open: put request_starts
behind an opt-in that's off by default, working with TF_QWEN_KV_SHARE. If it stays quiet, it will count as stale
on 26 Nov, 30 days after our ask, and close under our stale rule. A push before then keeps it open.

@ashhart

ashhart commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Hi @gilby, checking in on this one. It's still the fix #1334 needs on CUDA. The one open point is putting
request_starts behind an opt-in that's off by default and works with TF_QWEN_KV_SHARE. If you're short on time, we're
happy to draft the opt-in on top of your commits and keep you as the author; say so here.

@gilby

gilby commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Yes, please draft the opt-in on top of my commits and keep me as the author — that's the fastest path to landing, and it matches what you described: request_starts opt-in, off by default, working with TF_QWEN_KV_SHARE. I'll review your draft as soon as it's up and answer any questions from the diff.

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby, we'll do that. We'll put the opt-in on top of your commit, off by default, rebased onto current main, with you kept as the author, and check it with TF_QWEN_KV_SHARE set. Then we'll rerun your six-turn tables on a GB10 against main and post them here for you to review.

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

@gilby, the opt-in is drafted as you asked, as #2176: your commit with your authorship, rebased onto main, then TF_QWEN_REQUEST_STARTS=1 behind it, off by default. Beside shared KV it's refused by name for now, because exact cuts would need partial-page snapshots, and that's its own step. Please have a look. Our six-turn GB10 tables come next, and they'll go there. I've left your branch here untouched.

@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

/atc exchange ffa9ed5b69d38e0c68c164e676f9db273cfb2b1a2fd1e6115ea7c8779e82d6e8

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.

Qwen 27B on native CUDA: a chat's next turn never resumes at the previous turn, only on the 4,096-row grid

2 participants