Skip to content

Make Qwen3 device top-k selection exact#787

Merged
high-cloud merged 1 commit into
hw-native-sys:mainfrom
zmnobug:topk-sampling-operator
Jul 21, 2026
Merged

Make Qwen3 device top-k selection exact#787
high-cloud merged 1 commit into
hw-native-sys:mainfrom
zmnobug:topk-sampling-operator

Conversation

@zmnobug

@zmnobug zmnobug commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow up on #769 by replacing the Qwen3-14B per-chunk Top-4 approximation with a provably exact full-vocabulary Top-32 selector.

What Changed

  • Split the 151936 real-vocabulary logits into 74 full 2048-token groups and one 384-token tail group.
  • Compute an exact Top-32 for each group with sort32 and mrgsort.
  • Merge the resulting 75 x 32 = 2400 candidates in a 4096-entry padded buffer and select the exact global Top-32.
  • Keep padded-vocabulary entries excluded with valid-shape masking and minimum-value fill.
  • Change the golden implementation to compare directly against torch.topk(logits[:, :REAL_VOCAB], 32).
  • Add adversarial fixtures with all Top-32 values concentrated in one 512-token region and distributed across 32 regions.

If an entry is not in its group's Top-32, at least 32 entries in that group rank ahead of it, so it cannot belong to the global Top-32. Therefore the union of exact group Top-32 sets contains the exact global Top-32.

Validation

  • A2/A3 NPU golden comparison: passed for values and token IDs.
  • Greedy selection_k=1 regression: passed.
  • Standalone selector, 100 rounds: min 427.1 us, median 427.8 us, mean 428.0 us, max 429.9 us.
  • Serving sampling tests after rebase: 29 passed.
  • Shared-L3 batch-16 prefill/decode warmup: passed.
  • Qwen3-14B 20-token top-k generation: 0.769 s E2E, 26.01 tok/s.

Paired serving PR: hw-native-sys/pypto-serving#77

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b0d487fb-713f-4027-bb78-5f38a5cf10da

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Qwen3-14B top-k selection now uses vocabulary groups instead of chunks, handles a vocabulary tail, merges grouped candidates through staged sorting, and derives final indices from selected positions. Test fixtures cover active batches, while the golden reference directly applies torch.topk.

Changes

Qwen3 grouped top-k selection

Layer / File(s) Summary
Grouped candidate construction
models/qwen3/14b/topk_select.py
Adds group and candidate-padding constants, introduces _topk_group_pairs, handles full groups and the vocabulary tail, and rewrites final candidate merging in topk_select_fwd.
Active-batch fixtures and reference validation
models/qwen3/14b/topk_select.py
Expands batch-specific logits, adjusts active-batch initialization, and simplifies the golden result to direct torch.topk over the real vocabulary.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Poem

A rabbit hops through groups of scores,
Sorting bright trails across the floors.
The tail is padded, candidates align,
topk checks the answer line by line.
Carrots cheer the new top-k design!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: making Qwen3 device top-k selection exact.
Description check ✅ Passed The description is clearly aligned with the changeset and details the exact top-k selector update and validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request refactors the top-k selection logic in topk_select.py to use a grouped top-k approach with a new helper function _topk_group_pairs, while simplifying the golden reference implementation. The review feedback highlights two key issues: first, the tail group logic should be wrapped in a compile-time check (if TOPK_GROUP_TAIL != 0:) to prevent out-of-bounds slicing when TOPK_GROUP_TAIL is zero; second, an explicit assertion assert VOCAB_CHUNK == 512 should be added to prevent silent correctness bugs since the sorting pipeline currently assumes this specific chunk size.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread models/qwen3/14b/topk_select.py Outdated
Comment on lines +185 to +211
tail_start = TOPK_NUM_FULL_GROUPS * TOPK_GROUP_WIDTH
tail_scores_raw = logits[
b : b + 1, tail_start : tail_start + VOCAB_CHUNK
]
tail_scores = pl.fillpad(
pl.set_validshape(tail_scores_raw, 1, TOPK_GROUP_TAIL),
pad_value=pl.PadValue.min,
)
tail_indices = pl.arange(
pl.cast(tail_start, target_type=pl.UINT32),
[1, VOCAB_CHUNK],
dtype=pl.UINT32,
)
tail_pairs = pl.sort32(tail_scores, tail_indices)
tail_pairs = pl.mrgsort(tail_pairs, block_len=64)
tail_pairs = pl.mrgsort(tail_pairs, block_len=256)
tail_pairs = tail_pairs[:, 0 : 2 * TOPK]
tail_vals = pl.gather(tail_pairs, mask_pattern=pl.tile.MaskPattern.P0101)
tail_ids = pl.gather(
tail_pairs,
mask_pattern=pl.tile.MaskPattern.P1010,
output_dtype=pl.INT32,
)
tail_offset = TOPK_NUM_FULL_GROUPS * TOPK
for k in pl.range(TOPK):
pl.write(candidate_vals, [0, tail_offset + k], pl.read(tail_vals, [0, k]))
pl.write(candidate_ids, [0, tail_offset + k], pl.read(tail_ids, [0, k]))

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.

high

If TOPK_GROUP_TAIL == 0 (e.g., if REAL_VOCAB is a multiple of TOPK_GROUP_WIDTH), the unconditional execution of the tail group logic would slice logits out of bounds (specifically when REAL_VOCAB == VOCAB), causing compilation or runtime failure. Wrapping the tail group logic in a compile-time check if TOPK_GROUP_TAIL != 0: completely avoids this issue.

                    if TOPK_GROUP_TAIL != 0:
                        tail_start = TOPK_NUM_FULL_GROUPS * TOPK_GROUP_WIDTH
                        tail_scores_raw = logits[
                            b : b + 1, tail_start : tail_start + VOCAB_CHUNK
                        ]
                        tail_scores = pl.fillpad(
                            pl.set_validshape(tail_scores_raw, 1, TOPK_GROUP_TAIL),
                            pad_value=pl.PadValue.min,
                        )
                        tail_indices = pl.arange(
                            pl.cast(tail_start, target_type=pl.UINT32),
                            [1, VOCAB_CHUNK],
                            dtype=pl.UINT32,
                        )
                        tail_pairs = pl.sort32(tail_scores, tail_indices)
                        tail_pairs = pl.mrgsort(tail_pairs, block_len=64)
                        tail_pairs = pl.mrgsort(tail_pairs, block_len=256)
                        tail_pairs = tail_pairs[:, 0 : 2 * TOPK]
                        tail_vals = pl.gather(tail_pairs, mask_pattern=pl.tile.MaskPattern.P0101)
                        tail_ids = pl.gather(
                            tail_pairs,
                            mask_pattern=pl.tile.MaskPattern.P1010,
                            output_dtype=pl.INT32,
                        )
                        tail_offset = TOPK_NUM_FULL_GROUPS * TOPK
                        for k in pl.range(TOPK):
                            pl.write(candidate_vals, [0, tail_offset + k], pl.read(tail_vals, [0, k]))
                            pl.write(candidate_ids, [0, tail_offset + k], pl.read(tail_ids, [0, k]))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bc73c77: the tail-group path is now guarded by the compile-time condition TOPK_GROUP_TAIL != 0, so zero-tail configurations cannot form an out-of-bounds slice. The current Qwen3 exact Top-32 NPU golden test passes.

Comment on lines 38 to 44
assert VOCAB % VOCAB_CHUNK == 0
assert REAL_VOCAB <= VOCAB
assert TOPK <= VOCAB_CHUNK
assert CHUNK_TOPK <= TOPK
assert CHUNK_TOPK <= CHUNK_TOPK_PAD
assert REAL_NUM_VOCAB_CHUNKS * CHUNK_TOPK <= CANDIDATE_PAD
assert TOPK_GROUP_WIDTH % VOCAB_CHUNK == 0
assert TOPK_GROUP_TAIL == REAL_VOCAB_TAIL
assert TOPK_NUM_GROUPS * TOPK <= TOPK_CANDIDATE_PAD
assert NUM_VOCAB_CHUNKS <= GREEDY_CHUNK_PAD

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.

medium

The mrgsort pipeline in both the greedy path (lines 96-97) and the tail path (lines 199-200) only goes up to block_len=256, which can only fully sort up to 512 elements. If VOCAB_CHUNK is configured to any other value (e.g., 1024 or 2048) in T.vocab_chunk, the sorting will be incomplete, leading to silent correctness bugs. Adding an explicit assertion ensures that any future configuration changes are caught immediately.

Suggested change
assert VOCAB % VOCAB_CHUNK == 0
assert REAL_VOCAB <= VOCAB
assert TOPK <= VOCAB_CHUNK
assert CHUNK_TOPK <= TOPK
assert CHUNK_TOPK <= CHUNK_TOPK_PAD
assert REAL_NUM_VOCAB_CHUNKS * CHUNK_TOPK <= CANDIDATE_PAD
assert TOPK_GROUP_WIDTH % VOCAB_CHUNK == 0
assert TOPK_GROUP_TAIL == REAL_VOCAB_TAIL
assert TOPK_NUM_GROUPS * TOPK <= TOPK_CANDIDATE_PAD
assert NUM_VOCAB_CHUNKS <= GREEDY_CHUNK_PAD
assert VOCAB % VOCAB_CHUNK == 0
assert REAL_VOCAB <= VOCAB
assert TOPK <= VOCAB_CHUNK
assert VOCAB_CHUNK == 512
assert TOPK_GROUP_WIDTH % VOCAB_CHUNK == 0
assert TOPK_GROUP_TAIL == REAL_VOCAB_TAIL
assert TOPK_NUM_GROUPS * TOPK <= TOPK_CANDIDATE_PAD
assert NUM_VOCAB_CHUNKS <= GREEDY_CHUNK_PAD
References
  1. When assuming a single-tile coverage (e.g., T_PAD <= MM_ROW_TILE) in a kernel, make this invariant explicit with a module-level assertion (e.g., assert T_PAD == MM_ROW_TILE) to prevent silent correctness issues if configurations change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bc73c77: added assert VOCAB_CHUNK == 512 to make the sort-pipeline invariant explicit and fail fast if the tiling configuration changes.

@zmnobug
zmnobug force-pushed the topk-sampling-operator branch from 4f6f119 to f44ddf9 Compare July 16, 2026 03:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
models/qwen3/14b/topk_select.py (1)

264-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Distributed fixture doesn't actually cross the half0/half1 merge boundary.

spread_ids uses stride VOCAB_CHUNK (512), so with TOPK=32 the max index is ~15,879 — entirely inside groups 0-7 (candidate slots 0-255), which all land in half0 (slots 0-2047). This fixture never places a top-32 value in half1 (groups 64+, tail) or near the 2048-slot boundary, so it can't exercise the cross-half merge that is central to this PR's exactness guarantee.

Consider spreading across the full REAL_VOCAB range instead, e.g.:

🧪 Proposed fix to actually exercise the half0/half1 boundary
-            spread_ids = torch.arange(TOPK, dtype=torch.long) * VOCAB_CHUNK + 7
+            spread_ids = torch.arange(TOPK, dtype=torch.long) * (REAL_VOCAB // TOPK) + 7
             logits[1, spread_ids] = torch.arange(TOPK, 0, -1, dtype=torch.float32) + 1000.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@models/qwen3/14b/topk_select.py` around lines 264 - 266, Update the BATCH > 1
fixture in the distributed top-k test around spread_ids so selected logits span
the full REAL_VOCAB range, including indices near and beyond the 2048-slot
half0/half1 boundary. Preserve the descending score ordering and TOPK
cardinality while ensuring the generated candidates exercise the cross-half
merge path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@models/qwen3/14b/topk_select.py`:
- Around line 264-266: Update the BATCH > 1 fixture in the distributed top-k
test around spread_ids so selected logits span the full REAL_VOCAB range,
including indices near and beyond the 2048-slot half0/half1 boundary. Preserve
the descending score ordering and TOPK cardinality while ensuring the generated
candidates exercise the cross-half merge path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 77dc0714-7f8b-487a-a464-0852fba7c65c

📥 Commits

Reviewing files that changed from the base of the PR and between bd6c48b and 4f6f119.

📒 Files selected for processing (1)
  • models/qwen3/14b/topk_select.py

@zmnobug
zmnobug force-pushed the topk-sampling-operator branch 2 times, most recently from ed45657 to bc73c77 Compare July 16, 2026 03:52
@zmnobug

zmnobug commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the latest review feedback in bc73c77. In addition to the tail guard and 512-chunk invariant, the distributed Top-32 fixture now spans the full real vocabulary and places candidates in both halves of the final merge. The updated fixture passes exact NPU comparison against torch.topk.

@zmnobug
zmnobug force-pushed the topk-sampling-operator branch 2 times, most recently from 8c6d271 to 9fdc34a Compare July 20, 2026 03:37
@high-cloud
high-cloud merged commit 0999dba into hw-native-sys:main Jul 21, 2026
8 checks passed
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