Skip to content

lanes: core's row draw takes Qwen 3.5's bounded top-k path (#2460 item 3, step 3) - #3214

Open
jschmied wants to merge 1 commit into
ashhart:mainfrom
jschmied:core-sampler-4
Open

jschmied wants to merge 1 commit into
ashhart:mainfrom
jschmied:core-sampler-4

Conversation

@jschmied

Copy link
Copy Markdown
Contributor

What this changes

The next step of #2460 item 3 (one sampler in core/lanes), after #2606 moved Qwen 3.5's general host row draw into
core/lanes/sampling_row.zig and #2507 (mrv777) gave Qwen's bounded top-k draw its 32-logit block skip. That bounded
draw now lives in core too:

  • families/qwen3_5/draw_topk.zig moves to core/lanes/sampling_row_topk.zig (git mv, same code). The only edits
    are names: lanes.grammar/lanes.sampling become the sibling grammar.zig/sampling.zig imports, the errors
    become core's NonfiniteLogits/NoAllowedToken, the parameter sampling is settings (it would shadow the
    import), and the last line is split in two to stay under 120 columns. Normalized diff in the receipt.
  • lanes.sampling_row.draw tries it first, in the same order qwen3_5/draw.zig did: a sampled top_k of 1..128
    draws from the bounded, heap-free path; greedy, top_k 0 and top_k > 128 fall through to the general path, as before.
  • qwen3_5/draw.zig draw is core's draw with the errors mapped back to NonfiniteQwenLogits and
    NoAllowedQwenToken (as since lanes: the host draw over a bf16 row moves into core, Qwen 3.5 first (#2460 item 3, step 1) #2606), so every Qwen message stays the same.
  • The three selection tests move with the code (zig/build/qwen35_cache_host.zig drops their names from its
    filter list: they run in core's lane tests now). A new core test pins the wiring: top_k 1, 20 and 128 through
    sampling_row.draw allocate nothing and pick the full-row choose token.

Core vs family-only:

part where
general row draw (finite check, mask, greedy, top-k candidates, choose) core: core/lanes/sampling_row.zig (unchanged)
bounded top-k draw (bf16 keys, 32-word block skip, stack scratch, chooseOrdered) core: core/lanes/sampling_row_topk.zig (moved)
Qwen error names, mask_words from the Qwen vocab, Qwen full-vocabulary draw tests family: qwen3_5/draw.zig

Not moved: HIP (hip_serve/sample.zig) and XPU (native/xpu*.zig) keep their own top-k-then-choose code. Neither is
a byte copy of this file (no bf16 key order, no block skip: f32 rows and a top_k + margin cut), so they are a later
step, not this PR.

Who else reaches the bounded path: on main only Qwen 3.5 calls sampling_row.draw. #2679 (open, step 2) moves
LFM3's draw and Flash Next's host reference onto it; once both land, their sampled top_k 1..128 rows take the same
bounded path. Its tests show the same token as the general path, so their tokens do not change.

Checks on this head

  • Head commit: b0361bc
  • Lean checker, python3 tools/zig/lean_check.py, on the base and on this head: 341 on the base (223da76), 341 on
    this head, 0 new.
  • x86 (8 cores, no GPU, Zig 0.17.0) on b0361bc: zig build -Dfixed-cpu test test-golden test-kernel-contract test-server-security test-pull-security fuzz-cortheon check-release-paths: 728/728 steps, 9179 of 9246 tests passed,
    67 skipped. zig build check-native-cuda-host compile-cuda-tests: 16/16 steps. python3 tools/pr_check.py: 0 errors.
    zig fmt --check build.zig zig and git diff --check: clean.
  • GB10 (aarch64), b0361bc: zig fmt --check, git diff --check and the lean checker clean; zig build native -Dgpu=spark for head and base.
  • Planted controls, each on its own in a scratch copy, run against core's lane tests and test-qwen-draw-host: 13 of 13 turn red (a greedy tie taken >=, the top-k tie order, the mask ignored in either path, the position shifted in either path, three ways of breaking the 32-word block-skip bound, the block non-finite check removed, the fast path unwired, and the two Qwen error names swapped). The clean copy is green.
  • CI: runs on this PR.
  • Not run, and why: macOS (the moved code has no Metal dependency, and test-qwen35-metal-host runs on Linux).

Receipt

  • Served binary: zig build native on the GB10 for head and base in the same path. Every embedded fatbin is identical (114 of 114), so no device code changed; the host binary differs (md5 head 277f3218, base 369e7eac).
  • Served A/B, base against head, Qwen3.8-27B NVFP4 with the DFlash2 drafter, --parallel 8 --context 16384 --no-thinking, page cache dropped before each server: 11 sampling cases (greedy; top_k 1, 20 and 128 on the bounded path; top_k 0, 129 and 200 and a hot top_k 0 on the general path; JSON-schema output greedy, top_k 0 and top_k 20, where every masked row is a host draw) x 4 prompts x drafted and plain x one at a time and four at once, 48 tokens each: 176 of 176 token arrays identical, 0 differ, 0 errors.
  • New switch? None.
  • Environment: TensorFold b0361bc on main 223da76; Zig 0.17.0; CUDA 13 nvcc, sm_121; one NVIDIA GB10 (128 GB).
  • Exactness: the same code by construction (renamed only); host tests compare each draw with lanes.sampling.choose
    over the full row, masked and unmasked, every finite bf16 word, and ties.
  • Decode and prompt speed before and after: n/a. No device code changed (fatbins identical); the host draw runs the same code, with one more call.

Proposed CHANGELOG line (Unreleased, for the maintainer to place):
- Core's host row draw (lanes.sampling_row) takes Qwen 3.5's bounded top-k path; the code moved to core/lanes/sampling_row_topk.zig (#2460 item 3). Thanks to @jschmied and @mrv777.

 item 3)

qwen3_5/draw_topk.zig moves unchanged to core/lanes/sampling_row_topk.zig (lanes imports become
sampling.zig/grammar.zig, the Qwen error names become NonfiniteLogits/NoAllowedToken, the parameter
`sampling` is `settings`). sampling_row.draw tries it first for a sampled top_k of 1..128, as
qwen3_5/draw.zig did, so qwen3_5/draw.zig is core's draw with the errors mapped back to the Qwen names.
The three selection tests move with the code; a core test pins the heap-free path through sampling_row.draw.
@ashhart

ashhart commented Nov 14, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied, this is a clean next step for #2460 item 3. The bounded top-k draw moves into core/lanes as the same code with only names changed. sampling_row.draw tries it first in the order Qwen's draw already did, and Qwen's error messages are unchanged. The new test pins the heap-free path against the full-row choose token at top_k 1, 20 and 128.

It's queued to land. It's safe because on today's main only Qwen 3.5 calls sampling_row.draw, and it already took the bounded path first, so no token can change.

Optional: the three moved selection tests now run only in the full zig build test, where test-qwen35-metal-host used to run them by name. A small filtered host step for them would keep them in a fast step.

  • Synthena (Ash's Agent)

@jschmied

Copy link
Copy Markdown
Contributor Author

Thanks. So the queued head doesn't change, I'll do the filtered host step for the three moved selection tests as a small follow-up PR after this lands, rather than pushing it here.

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.

2 participants