Skip to content

[None][feat] Improve cute dsl radix top-k - #15756

Merged
yuxianq merged 137 commits into
NVIDIA:mainfrom
limin2021:cute-dsl-radix-top-improve
Jul 29, 2026
Merged

[None][feat] Improve cute dsl radix top-k#15756
yuxianq merged 137 commits into
NVIDIA:mainfrom
limin2021:cute-dsl-radix-top-improve

Conversation

@limin2021

@limin2021 limin2021 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added support for request-level row ordering in top-k decode.
    • Introduced a new load-balancing path for handling mixed long/short requests more efficiently.
    • Expanded top-k coverage to support a wider range of batch and selection sizes.
  • Bug Fixes

    • Improved correctness for sorted-request and load-balanced decode paths.
    • Fixed histogram resets and boundary handling to avoid incorrect top-k results.
    • Relaxed some kernel constraints to support broader valid configurations.

Description

This PR adds a radix-filter single-pass multi-CTA (cluster-DSMEM) top-k path
for the Blackwell (SM100) indexer top-k kernels, plus the overflow-policy
framework and dispatch heuristics needed to make it the default, and a large
round of correctness/perf/refactor cleanup of the existing radix top-k code.

What's new

1. Radix-filter single-pass multi-CTA kernel (cluster DSMEM).
A cluster of CTAs cooperates on one row within a single kernel launch: each CTA
histograms its chunk, then peer histograms are merged via distributed shared
memory (mapa.shared::cluster + ld.shared::cluster) into a separate merge
buffer — no GMEM intermediate state, no second merge launch. Output offsets are
computed with a DSMEM exclusive prefix scan. Covers both decode and prefill.

2. Overflow-policy framework (overflow_policy):

  • REREAD (new default) — optimistic SMEM candidate buffer, re-scan the chunk on
    overflow (exact).
  • TRUNCATE — drop overflow candidates (lossy, zero extra memory).
  • GMEM_SPILL — spill overflow to a GMEM scratch buffer (exact).
  • REREAD_ALWAYS — always re-scan (no SMEM candidate buffer).
    Default changed GMEM_SPILL -> REREAD (exact and no scratch allocation).

3. Dispatch heuristics in cute_dsl_indexer_topk_decode:

  • Filter↔select routing (_radix_select_preferred): small-N bf16/fp16 route to
    radix-SELECT SP; gated on num_rows <= num_sms so large-batch falls through to
    the faster filter single-CTA.
  • cluster_size auto-tuning (auto_cluster_size / auto_cluster_size_truncate):
    policy-aware 2-ladder (peak-by-N ∧ occupancy-by-num_rows), REREAD variant hedges
    the large-occupancy re-scan cliff.

4. Correctness fix.
DSMEM cluster-arrive publish sites use a release-fence cluster_arrive() (not
relaxed) so a peer's ld.shared::cluster sees the published histogram; relaxed
arrive risked stale reads. WAR/liveness sites stay relaxed (peer reads already
drained).

5. Perf & cleanup.
Occupancy-aware SMEM sizing (replaces hardcoded smem_input_size), tightened
barriers/fences in the refine loop, cache_smem_values flag, device-queried
num_sms (drops hardcoded 148), and extensive dead-code/refactor removal.

Notes

  • Heuristics (_radix_select_preferred, auto_cluster_size*) were tuned on a
    B200 randn / fixed-length-seqlen sweep; the docstrings carry that caveat.
  • No public API signature changes.

Test Coverage

  • tests/unittest/_torch/thop/parallel/test_indexer_topk.py — exercises
    cute_dsl_indexer_topk_decode / prefill across the single-CTA, 2-pass multi-CTA,
    and new single-pass multi-CTA paths (radix-filter prefix added to prefill tests).
  • tests/unittest/_torch/thop/parallel/test_cute_dsl_topk_overflow_policy.py (new) —
    correctness of REREAD / TRUNCATE / GMEM_SPILL / REREAD_ALWAYS, including the
    multi-CTA short-row degrade boundary.

Performance

Measured on B200 (SM100). GPU-kernel time via CUPTI, cold L2 (8 GB flush before each launch, flush kernel excluded), randn fixed-length inputs, median of 40 iters. Repro scripts + full data attached under ab_pr15756_vs_main/.

1. Decode — this PR vs main baseline (same cute_dsl_indexer_topk_decode op, each version's default dispatch)

The PR makes radix-filter single-pass multi-CTA the default and adds overflow policies. Speedup = baseline_us / PR_us.

Geomean: bf16 1.36x, fp32 1.11x.

Representative configs (top_k=2048):

dtype N batch main (us) PR (us) speedup
bf16 65536 1 31.1 13.4 2.33x
bf16 131072 1 28.4 13.1 2.17x
bf16 32768 1 25.4 12.2 2.08x
bf16 65536 64 31.9 21.7 1.47x
bf16 32768 256 45.2 34.3 1.32x
fp32 131072 1 29.3 17.7 1.66x
fp32 65536 1 24.9 15.6 1.60x
fp32 65536 64 30.1 25.3 1.19x
fp32 32768 256 34.5 33.1 1.04x
Full decode data (60 configs)
dtype N batch top_k main (us) PR (us) speedup
bf16 8192 1 512 9.14 7.47 1.22x
bf16 8192 1 2048 9.12 7.62 1.20x
bf16 8192 64 512 9.15 7.66 1.19x
bf16 8192 64 2048 9.28 7.73 1.20x
bf16 8192 256 512 14.19 11.22 1.26x
bf16 8192 256 2048 14.42 11.43 1.26x
bf16 16384 1 512 13.46 10.33 1.30x
bf16 16384 1 2048 13.89 10.97 1.27x
bf16 16384 64 512 13.68 10.69 1.28x
bf16 16384 64 2048 14.24 11.30 1.26x
bf16 16384 256 512 22.77 17.64 1.29x
bf16 16384 256 2048 23.40 18.15 1.29x
bf16 32768 1 512 15.36 9.96 1.54x
bf16 32768 1 2048 25.42 12.25 2.08x
bf16 32768 64 512 16.09 13.21 1.22x
bf16 32768 64 2048 26.18 18.01 1.45x
bf16 32768 256 512 22.30 22.62 0.99x
bf16 32768 256 2048 45.25 34.32 1.32x
bf16 65536 1 512 20.01 11.47 1.74x
bf16 65536 1 2048 31.15 13.36 2.33x
bf16 65536 64 512 20.50 16.23 1.26x
bf16 65536 64 2048 31.89 21.74 1.47x
bf16 65536 256 512 40.39 40.39 1.00x
bf16 65536 256 2048 83.57 69.89 1.20x
bf16 131072 1 512 23.73 13.33 1.78x
bf16 131072 1 2048 28.41 13.11 2.17x
bf16 131072 64 512 38.95 25.13 1.55x
bf16 131072 64 2048 39.62 25.24 1.57x
bf16 131072 256 512 74.97 72.25 1.04x
bf16 131072 256 2048 75.43 72.70 1.04x
fp32 8192 1 512 8.91 7.71 1.16x
fp32 8192 1 2048 9.34 8.95 1.04x
fp32 8192 64 512 9.93 9.38 1.06x
fp32 8192 64 2048 10.26 9.98 1.03x
fp32 8192 256 512 13.99 12.90 1.08x
fp32 8192 256 2048 14.68 13.77 1.07x
fp32 16384 1 512 10.29 10.62 0.97x
fp32 16384 1 2048 13.01 12.16 1.07x
fp32 16384 64 512 12.11 11.63 1.04x
fp32 16384 64 2048 14.05 13.28 1.06x
fp32 16384 256 512 18.55 18.42 1.01x
fp32 16384 256 2048 22.78 22.80 1.00x
fp32 32768 1 512 15.66 13.91 1.13x
fp32 32768 1 2048 19.12 16.23 1.18x
fp32 32768 64 512 16.79 18.59 0.90x
fp32 32768 64 2048 19.38 19.95 0.97x
fp32 32768 256 512 28.75 27.75 1.04x
fp32 32768 256 2048 34.49 33.11 1.04x
fp32 65536 1 512 20.74 15.19 1.37x
fp32 65536 1 2048 24.91 15.58 1.60x
fp32 65536 64 512 27.52 23.74 1.16x
fp32 65536 64 2048 30.05 25.26 1.19x
fp32 65536 256 512 51.12 49.26 1.04x
fp32 65536 256 2048 56.04 54.28 1.03x
fp32 131072 1 512 19.92 16.67 1.19x
fp32 131072 1 2048 29.32 17.67 1.66x
fp32 131072 64 512 37.95 29.71 1.28x
fp32 131072 64 2048 47.09 34.52 1.36x
fp32 131072 256 512 83.63 82.46 1.01x
fp32 131072 256 2048 101.31 97.68 1.04x

2. Prefill — new DSL interface vs CUDA indexer_topk_prefill (fp32, overflow_policy=REREAD default)

Prefill is new in this PR (no main baseline). num_rows = packed query-token count (min(chunk_query_len, 2^31/num_cols), chunk cap indexer_max_chunk_size=32768); the packed production workload is num_rows 8K–32K. cpp/dsl = cpp_us / dsl_us. cpp is fp32-only.

Realistic regime (num_rows 8192–32768) geomean cpp/dsl = 1.25x.

Representative configs:

N (ctx) num_rows top_k cpp (us) DSL (us) speedup
4096 8192 2048 130.5 103.6 1.26x
32768 8192 2048 582.8 583.2 1.00x
65536 8192 2048 1108.1 976.4 1.13x
131072 8192 2048 2115.5 1849.4 1.14x
65536 32768 2048 4978.6 3863.7 1.29x
131072 32768 2048 8679.6 7663.8 1.13x
262144 8192 512 3486.5 2918.1 1.19x
Full prefill fp32 data, num_rows>=8192 (realistic regime)
N num_rows top_k cpp (us) DSL (us) speedup
4096 8192 512 127.98 137.42 0.93x
4096 16384 512 438.57 267.95 1.64x
4096 32768 512 1406.87 529.10 2.66x
4096 8192 1024 127.71 137.39 0.93x
4096 16384 1024 438.64 268.02 1.64x
4096 32768 1024 1408.69 529.79 2.66x
4096 8192 2048 130.50 103.63 1.26x
4096 16384 2048 446.01 199.62 2.23x
4096 32768 2048 1422.18 392.60 3.62x
8192 8192 512 187.31 189.37 0.99x
8192 16384 512 551.55 369.33 1.49x
8192 32768 512 1615.38 729.30 2.21x
8192 8192 1024 199.57 222.24 0.90x
8192 16384 1024 575.06 434.68 1.32x
8192 32768 1024 1657.17 860.34 1.93x
8192 8192 2048 199.88 215.56 0.93x
8192 16384 2048 576.85 420.94 1.37x
8192 32768 2048 1661.92 832.61 2.00x
16384 8192 512 290.10 293.23 0.99x
16384 16384 512 752.16 571.77 1.32x
16384 32768 512 2004.84 1129.44 1.78x
16384 8192 1024 318.05 324.56 0.98x
16384 16384 1024 805.67 634.32 1.27x
16384 32768 1024 2102.86 1256.01 1.67x
16384 8192 2048 345.87 390.11 0.89x
16384 16384 2048 857.67 763.96 1.12x
16384 32768 2048 2193.41 1516.08 1.45x
32768 8192 512 476.35 471.41 1.01x
32768 16384 512 1112.48 918.20 1.21x
32768 32768 512 2693.71 1812.95 1.49x
32768 8192 1024 519.53 511.84 1.02x
32768 16384 1024 1202.58 999.49 1.20x
32768 32768 1024 2884.94 1978.29 1.46x
32768 8192 2048 582.84 583.21 1.00x
32768 16384 2048 1319.03 1140.11 1.16x
32768 32768 2048 3082.26 2259.61 1.36x
65536 8192 512 962.00 899.23 1.07x
65536 16384 512 2042.30 1759.42 1.16x
65536 32768 512 4374.04 3584.37 1.22x
65536 8192 1024 1037.60 893.75 1.16x
65536 16384 1024 2187.68 1746.62 1.25x
65536 32768 1024 4654.68 3574.71 1.30x
65536 8192 2048 1108.10 976.36 1.13x
65536 16384 2048 2337.08 1909.74 1.22x
65536 32768 2048 4978.58 3863.74 1.29x
131072 8192 512 1819.01 1471.46 1.24x
131072 16384 512 3763.71 2956.47 1.27x
131072 32768 512 7807.97 6146.03 1.27x
131072 8192 1024 1914.48 1799.16 1.06x
131072 16384 1024 3939.95 3603.29 1.09x
131072 32768 1024 8121.20 7455.66 1.09x
131072 8192 2048 2115.54 1849.37 1.14x
131072 16384 2048 4290.65 3712.84 1.16x
131072 32768 2048 8679.60 7663.78 1.13x
262144 8192 512 3486.48 2918.15 1.19x
262144 16384 512 7171.99 6004.46 1.19x
262144 8192 1024 3588.73 2918.14 1.23x
262144 16384 1024 7332.81 6002.96 1.22x
262144 8192 2048 3840.08 9313.66 0.41x
262144 16384 2048 7726.38 18920.26 0.41x

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • If PR introduces API changes, an appropriate PR label is added - either api-compatible or api-breaking. For api-breaking, include BREAKING in the PR title.

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

GitHub Bot Help

To see a list of available CI bot commands, please comment /bot help.

limin2021 added 30 commits May 20, 2026 08:29
Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…invariant

- Replace from_dlpack/static-shape compile with make_fake_compact_tensor +
  sym_int for batch/num_tokens dims, keeping (dtype, top_k, next_n) as the
  cache key. Reduces unique compile entries from 810 to 27 across the bench
  sweep; correctness verified (no OOB writes from cache reuse with wrong
  shape) via 288-config pytest + cross-impl A/B match.

- Fix test_gvr_topk_decode: (1) pre_idx_count now uses top_k (matches CUDA
  dispatch precondition preIdxCount == topK at heuristic_topk.cuh:810);
  (2) tie-aware reference now masks logits to per-row effective_len
  = seq_len - next_n + 1, avoiding false negatives when next_n > 1 makes
  the kernel skip the last next_n-1 columns.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…imit / mask guard)

Four small mechanical alignments — each isolated, removes only redundant
work the CUDA reference does not do. Correctness verified: 288/288 in
test_gvr_topk_decode.py. Perf delta within measurement noise (~0.15us
estimated, 21us baseline DSL — under the ~0.5us spread floor) but the
changes match heuristic_topk.cuh semantics 1:1 and pave the way for
later batches.

- block_count_ge: drop the trailing barrier (gvr_topk_decode.py:422 ->
  removed). CUDA blockCountGE (heuristic_topk.cuh:441) returns without
  a sync because callers already insert their own __syncthreads after
  their tid==0 post-processing. The previous DSL trailing barrier was
  redundant (tid==0 reads its own write in-thread, no sync needed).

- Phase 4 snap_limit: change from cand_count>128 ? cand_count/4 : 32
  to cand_count (matches heuristic_topk.cuh:985). The older bound
  silently accepted a non-converged threshold in ~0.09 % of adversarial
  distributions; correctness improvement only, common case still
  converges in 1-3 iters.

- Phase 4 block_min/max: every thread now recomputes block_min/max from
  the warp-staged smem slots into local registers (matches
  heuristic_topk.cuh:891-898). Replaces the prior `tid==0 writes
  s_thr[1]/s_thr[2] then broadcast via __syncthreads` pattern, saving
  one block barrier in Phase 4.

- Phase 4 Pass 1/Pass 2 writeback: wrap popc + atomicAdd + shuffle in
  `if mask != 0` warp-uniform guard (mirrors heuristic_topk.cuh:1020,
  1045). Skips the atomic round-trip when no lane in the warp emits,
  most impactful for Pass 2 where only K-th-rank ties emit.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…le + early break)

Convert the Phase 2 secant refinement loop and the Phase 3 retry-shrink
loop from Python-unrolled `for in range(N)` (every body wrapped in an
`if not done:` guard) to runtime `while` with the convergence condition
in the loop predicate. This matches CUDA's pattern at heuristic_topk.cuh:
683 (Phase 2) and :769 (Phase 3 retry).

Previously, after the kernel converged at iteration k, the remaining
N-k unrolled bodies still each issued an LDS+ICMP+branch guard. With
secant typically converging at iter 3 of 15 and retry-shrink usually 0
of 10, this saved ~12 + ~10 = ~22 wasted guard sites per kernel call.

Tradeoff: lose Python-time const-fold of `if it == 0: f = min(f, 0.5)`,
which now becomes a runtime compare. CUDA does the same runtime compare
(heuristic_topk.cuh:698-699), so this is alignment not regression.

Measured impact (median config bf16 K=1024 N=32768 BS=1 next_n=2,
same-process A/B vs CUDA GVR, 5 reps alternating order):
  DSL_us  21.01 -> 20.28  (-0.73 us, -3.5%)
  C/G     0.869 -> 0.903  (+3.4 percentage points)
Above the ~1.5% bench_kineto spread floor. 288/288 tests pass.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Two SASS-alignment changes verified against the CUDA reference at the
median config (bf16 K=1024 N=32768 BS=1 next_n=2):

1. cute.make_ptr(..., cute.AddressSpace.gmem, ...) at the two 128-bit
   vec-load sites in block_count_ge and phase3_collect_candidates.
   Default AddressSpace.generic lowered to SASS LD.E.128; explicit gmem
   hint flips to LDG.E.128 (matches CUDA __ldg path, minus .CONSTANT
   which still requires CopyG2ROp+invariant).

2. phase1_preidx_stats: replace the runtime `while i < pre_idx_count`
   strided loop with `range_constexpr(pre_idx_count // num_threads)`.
   pre_idx.shape[1] is a compile-time constant (top_k baked into JIT
   cache key); supported top_k in {512, 1024, 2048} are all multiples
   of num_threads (512), so n_iters ∈ {1, 2, 4} unrolls cleanly. cute
   emits straight-line code (no BRA / ISETP / counter update) and
   issues both preIdx LDG.E and input LDG.E.U16 back-to-back, enabling
   LSU pipelining (in flight ILP). Mirrors what nvcc/ptxas does for
   the equivalent CUDA loop via auto-partial-unroll.

Bench (same-process A/B, 5 repeats × 100 iters, kineto + L2 flush):
   Before: C/G = 0.903 (DSL 10.7% slow)  -- post-Batch 2 baseline
   After:  C/G = 0.922 (DSL  8.5% slow)
   Δ = +1.9pp

Resource use after changes:
   regs/thread:  34 -> 39  (still 3 blocks/SM, occupancy unchanged 75%)
   dynamic smem: unchanged (~44 KB)
   total SASS instructions: 2935 -> 2944 (codegen ripple, mostly
   FMNMX3 +6; loop overhead ISETP/BRA -6/-2/-3 offset by +25 IMAD)

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…tail)

Replaces the runtime `while i + (vec_w - 1) < N` vec loop in
block_count_ge with a 4-way unrolled fast path + 1-way tail. The fast
path issues 4 independent LDG.E.128 per round (separate fragments so
cute schedules them concurrently), mirroring what nvcc/ptxas does for
the equivalent CUDA loop via auto-partial-unroll.

SASS verification at median config (bf16 K=1024 N=32768 BS=1 nn=2):
- 4 LDG.E.128 per inline at addresses base / base+0x2000 / +0x4000 /
  +0x6000 — exact match to CUDA's LDG.E.128.CONSTANT pattern (minus
  the CONSTANT cache hint, which still requires CopyG2ROp+invariant).
- Total LDG.E.128 count: 5 -> 21 (4 inlines * 4 + 4 tails + 1 phase3).
- Cute software-pipelines: 3 LDGs issued back-to-back, then consume of
  iter 0 starts while iter 3's LDG is issued in parallel. All 4 are
  in flight before HBM responds (latency ~600 cy >> 23 inst slots).

Resource impact:
- Regs/thread: 39 -> 39  (cute reuses fragment regs across loop body;
  Phase 4 likely remains the kernel-wide peak)
- Dynamic smem: unchanged (~44 KB)
- Static SASS size: 2944 -> 3672 inst (+25%)  -- code bloat acceptable,
  well within icache; Block Limit Reg = 3 unchanged at occupancy=75%.

Bench results (kineto, L2 flush, n_iters=30):

Median config (bf16 K=1024 N=32768 BS=1 nn=2), same-process A/B:
   Before this commit:  C/G = 0.922  (DSL  8.5% slow)
   After this commit:   C/G = 0.976  (DSL  2.4% slow)
   Delta:               +5.4pp

Full sweep (804 configs = 3 dtype * 3 top_k * 6 N * 5 BS * 3 next_n):
   Median  C/G: 0.860 (baseline post-Batch-2) -> 0.988 (now)
   Geomean C/G:           0.869 -> 0.999  (parity with CUDA)
   DSL faster:             17% ->  46%
   Within 5%:              13% ->  34%
   Within 10%:             28% ->  55%

By dtype: bf16 1.000, fp16 1.022, fp32 0.951 (fp32 has slightly less
runway since vec_w=4 vs 8 for bf16/fp16).
By N: gap remains at large N (>=64K: median ~0.87-0.90), where the
LSU-pipelining win is already saturated and other phases dominate.

The single-config worst slowdowns observed (C/G ~0.4) are concentrated
in nn=3 + small-mid N (4-32K) + BS>=64 configs whose CUDA-side numbers
also moved 5-15x between runs -- short-runtime measurement noise, not
real regressions.

This commit completes the SASS-alignment campaign objective (gap < 5%
on median config). Remaining ~10-13% at very large N is deferred.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Three new switches gate the block_count_ge vec-load fast path:

  enable_unroll_4 (default True): 4-way unrolled fast path
  enable_unroll_2 (default by dtype): 2-way cascade between fast and tail
  use_strided_layout (default by dtype): True → single make_ptr +
      (UNROLL, vec_w) strided layout (cute emits 4 LDG.E.128 sharing
      base reg with +0x2000/+0x4000/+0x6000 imm offsets, matching the
      CUDA SASS pattern). False → 4 separate make_ptr calls (matches
      the prior b459a8f commit style with 4 independent base regs).

Dtype-aware defaults (validated via per-config A/B testing on B200):

  bf16 / fp16: enable_unroll_2=True, use_strided_layout=True
      Strided cascade gives clean wins: cascade flips DSL from CUDA
      parity to consistently faster on small-N where the 4-way fast
      path doesn't fully cover N, and the medium 2-way path keeps two
      LDG.E.128 in flight. Strided layout keeps the SASS shared-base
      pattern that nvcc/ptxas auto-partial-unroll also produces.

  fp32: enable_unroll_2=False, use_strided_layout=False
      For fp32 (vec_w=4) the strided layout pushes regs 38 → 40 and
      regresses fp32 large-grid configs by 30-60pp (worst observed:
      K=1024 BS=128 nn=2 → 0.753 vs 1.364 with separate-ptrs). The
      cascade similarly hurts in 12% of fp32 configs. Separate-ptrs
      4-way unroll alone is the sweet spot.

Cache key includes the three switches so different settings produce
separate compiled kernels.

Full sweep results (804 configs, n_iters=30 kineto, L2 flush):

                       baseline   cascade-all   dtype-policy
  Median C/G:           0.988      1.011        1.006
  Geomean C/G:          0.999      1.047        1.038
  DSL faster %:           46%        54%          52%
  Within 10%:             55%        65%          65%

By dtype:
  bf16:  1.000 -> 1.043 (cascade wins preserved)
  fp16:  1.022 -> 1.038
  fp32:  0.951 -> 0.960 (anom K=1024 BS=128 fixed: 0.610 -> 1.038)

By N (the original "large-N gap"):
  N=8192:    1.097 -> 1.172 (+7pp, cascade hides medium-path remainder)
  N=65536:   0.897 -> 0.928 (+3pp)
  N=131072:  0.866 -> 0.923 (+6pp)

Remaining slow configs (fp32 K=2048 + BS>=64) were already <0.7 in
the baseline -- this commit doesn't introduce new regressions there.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Adds two new switches to the DSL GVR kernel:

  enable_phase3_unroll (default True): master gate for phase3_collect
      unrolling. When ON, the inner enable_unroll_4 / enable_unroll_2
      switches independently control 4-way fast and 2-way medium paths
      in phase3 (same semantics as block_count_ge). When OFF, only the
      tail 1-way loop runs.

  use_constant_hint (default False): True → CopyG2ROp(invariant=True)
      → SASS LDG.E.*.CONSTANT (read-only data cache, matches CUDA
      __ldg). Default False because cute's invariant lowering triggers
      aggressive rematerialization in LLVM/NVPTX (+272 inst, 4 spills,
      net -7pp geomean), outweighing the cache hint benefit.

Phase3_collect is now a 3-tier cascade (4-way fast + 2-way medium +
1-way tail) mirroring block_count_ge. The cascade gives:
  N>=65K:  +5-7%  (large-N main path, LSU pipelining wins)
  N<=32K:  -1-3%  (unroll setup overhead exceeds benefit at small N)
  Median geomean: +2.2pp from phase3 unroll alone

Resource analysis (bf16/fp16/fp32 x phase3 ON/OFF):
  REG/thread:
    bf16: 39 -> 39 (no change, cute reuses fragments)
    fp16: 39 -> 39 (no change)
    fp32: 38 -> 40 (+2, separate-ptrs path)
  Static SASS:
    bf16: 3936 -> 4368 (+11%)
    fp16: 4096 -> 4512 (+10%)
    fp32: 3368 -> 3480 (+3%)
  Theoretical Occupancy: 75% all configs (smem-limited to 3 blocks/SM,
    binding limit unaffected by phase3 unroll). Phase3 unroll has
    *zero* occupancy cost.

Wrapper signature gains both switches; cache key includes them so
different settings produce separate compiled kernels. A small helper
method _make_load_copy_atom() factors out the CopyG2ROp/Universal
selection to avoid Python if-else NameError inside @cute.jit scope.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…uristic

Phase 2 (block_count_ge) and Phase 3 (phase3_collect_candidates) replace
the manual `while + range_constexpr(UNROLL)` fast/medium-cascade unrolling
with a single `for k in cutlass.range(big_iters, unroll=4)` loop. LLVM's
loop unroll pass + GVN/CSE folds the 4 derived vec loads into the
CUDA-style shared base + immediate offsets pattern, emitting 4 back-to-back
LDG.E.128 [base+0x2000/0x4000/0x6000] instructions.

Add `min_blocks_per_mp` field on `GvrTopKKernel` and a 3-tier shape-aware
heuristic in the host wrapper:
  * n_vec_iters < 4         -> 0 (no launch_bounds, natural ptxas allocation)
  * num_rows <= 148 (B200 SMs) -> 1 (allow many regs, 4xLDG fold survives)
  * else                       -> 3 (keep 3 CTA/SM occupancy, ~42 reg cap)

The heuristic lifts fp32 K=512 large-N out of its regression zone (worst
case C/G 0.62 -> 1.07 at K=512 N=131072 BS=16 nn=2). Cache key extended so
each min_blocks value gets its own compiled kernel.

Random sweep vs phase3_unroll baseline (804 configs):
  geomean 1.060 -> 1.149, faster%-than-CUDA 62% -> 92%, losses 304 -> 63.

CUDA Graph: heuristic reads `logits.shape` (host int, no GPU sync) so
capture is safe; per-graph capture selects the right kernel per shape.
For dynamic-shape single-graph use, caller can pin `min_blocks_per_mp=3`
to disable the heuristic.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Three new kernel knobs on GvrTopKKernel + gvr_topk_decode host wrapper:

  * use_256bit_load (default False): emit LDG.E.256 (8 fp32 / 16 bf16-fp16
    elements per LDG) instead of LDG.E.128. Address alignment hint is
    raised from 16 to 32 bytes. Phase 2/3 unroll factor is dtype-aware:
    fp32 keeps unroll=4 (no cvt-to-fp32 overhead); bf16/fp16 drops to
    unroll=2 to limit the cvt register pressure that otherwise spills
    under min_blocks=3.

  * num_threads_per_block (default 512): configurable per-instance.
    BLOCK_SIZE / WARP_SIZE / NUM_WARPS are moved from module-level to
    GvrTopKKernel instance attrs (self.WARP_SIZE, self.num_threads,
    self.num_warps). Phase 1 preIdx loop gains an else branch for the
    K < num_threads case (e.g. num_threads=1024 with K=512): only the
    first K threads load a preIdx, others keep reduction-identity
    values which the warp/block reduces naturally absorb.

  * vec_bits / vec_align_bytes derived from use_256bit_load; cache key
    extended with use_256bit_load + num_threads_per_block.

Heuristic uses the resolved num_threads_per_block (not a hardcoded 512)
when computing n_vec_iters.

Tests parametrize use_256bit_load and num_threads_per_block; pytest
runs 288/288 PASS at use_256bit_load=True and at num_threads=1024.

Synth bench on BS<=128:
  - 128-bit + heuristic baseline: gm=1.131, faster%=99%, lose=9
  - 256-bit + heuristic        : gm=1.121 (fp32 wins +3pp; bf16/fp16
    flat-to-negative due to cvt-to-fp32 reg pressure spills under mb=3)

Random sweep on BS up to 128: 256-bit shows niche win on
(fp32, num_rows<=148, large N); should be opt-in.

Synth data generator (multi-BS) and bench script env vars
(DSL_USE_256BIT/DSL_MIN_BLOCKS/DSL_NUM_THREADS) live in the gvr-topk-opt
workspace and are not part of this commit.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…nobs

Add a fourth perf knob `enable_warp_parallel_reduce` to GvrTopKKernel +
gvr_topk_decode and replace the four `tid==0` serial loops over
num_warps slots with warp-parallel reduce/scan in warp 0:

  * Phase 1 block aggregate (4-way reduce):
        min/max/sum_f32/sum_i32 -> 4x warp_reduce in warp 0.
  * Phase 2 / blockCountGE total (1-way reduce):
        sum_i32 -> warp_reduce_sum_i32.
  * Phase 3 collect block prefix sum (exclusive scan):
        Hillis-Steele inclusive scan via block_scan.warp_scan, then
        exclusive = inclusive - val; total = inclusive at last lane.
  * Phase 2 secant aggregate (3-way reduce):
        packed sum_i32 + min_f32 + max_f32, with bound update on lane 0.

Default is False since at num_threads=512 (num_warps=16) the per-warp
ILP loss exceeds the serial-loop savings (~2pp regression on synth).
At num_threads=1024 (num_warps=32) the switch is essential -- without
it 1024 regresses vs baseline (gm 1.131 -> 1.123); with it 1024 wins
(gm -> 1.154 on synth BS<=128). Pair as
`enable_warp_parallel_reduce = (num_threads_per_block >= 1024)`.

Phase 1 also gains an `active_preidx_warps` optimization: when
`pre_idx_count < num_threads` (e.g. K=512 with num_threads=1024) only
the first ceil(K/32) warps have real data, so the warp_reduce + smem
write step is now gated to those warps. Saves ~30 cy/dummy-warp; the
full barrier afterwards still keeps all 1024 threads aligned for
Phase 2. The constexpr is clamped to num_warps so the K>num_threads
case (K=2048 with num_threads=512) doesn't index past the smem
buffers, and the same value drives both the warp_reduce gate and the
Site-1 block aggregate's smem read range.

Remove two now-dead switches:
  * `enable_unroll_2` -- only referenced in the commented-out manual
    2-way medium path that the `cutlass.range(unroll=4)` rewrite
    replaced.
  * `use_strided_layout` -- only referenced in the commented-out manual
    4-way strided-layout path, also replaced.

Cache key drops the two dead entries and gains
`enable_warp_parallel_reduce`. The cleanup is a no-op functionally
(the dead values were ignored by the active code paths) but removes
two cache-bucket dimensions.

Test parametrize expanded to 4-way matrix:
  next_n in {1, 2} (was {1, 2, 3, 4} -- trimmed to keep walltime)
  use_256bit_load in {False, True}
  num_threads_per_block in {512, 1024}
  enable_warp_parallel_reduce in {False, True}
1152 / 1152 PASS in 20:22.

Synth bench (BS<=128, threads=512 baseline -> threads=1024+wpON):
  geomean 1.131 -> 1.154  (+2.3pp)
  fp32 geomean 1.127 -> 1.177  (+5.0pp; up to +28pp at fp32 K=2048
                                N=131072 -- 1.50x vs CUDA)

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Mirrors the kFTarget=kK alignment for K=512/1024 (all dtypes) from CUDA
PR NVIDIA#14413 on the DSL GVR Top-K kernel so the DSL Phase-2 secant
behavior matches the new CUDA reference. Old pre-NVIDIA#14413 values kept as
inline comments for easy rollback.

Verified: 768/768 pytest configs pass for K=512/1024 across all dtypes,
N, next_n, use_256bit_load, num_threads_per_block, and
enable_warp_parallel_reduce.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Move module-level MAX_REFINE_ITERS / FLT_MAX / NEG_FLT_MAX into
instance attributes so all kernel-wide knobs live in one place.
Inline NUM_BINS_DEFAULT (2048) directly into the GvrParams table
since it was only used in three K=2048 entries. Drop dead
MAX_CANDIDATES.

Pure refactor — values, control flow, and DSL IR are unchanged.
Also removes the previously-commented-out A/B layout/unroll dead
code in block_count_ge.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Mirrors heuristicTopKDecode.cu PR NVIDIA#14219 cr-aware branch in the DSL
GVR Top-K kernel. compress_ratio=1 (default) preserves DSv3.2 behavior
exactly; compress_ratio=4 enables the DSv4 (overlap-compressor) indexer
path:
  * pre_idx_offset = 0 (vs (row % next_n) + 1 for cr=1) — in compressed-
    index space, new entries append at the end so prev-step indices
    remain valid as-is.
  * N = actual_kv_len / cr — logits/preIdx live in compressed-token-
    index space when cr > 1.

GvrParams TABLE is also keyed by (dtype, K, cr) so V3.2 and V4 use
their respectively tuned kFTarget values:
  cr=1 (V3.2): kFTarget = 384 (K=512) / 2560 (K=1024), pre-NVIDIA#14413.
  cr=4 (V4):   kFTarget = kK   = 512 (K=512) / 1024 (K=1024), PR NVIDIA#14413.
  K=2048: identical across cr (V4 doesn't natively use K=2048).

Cache key includes compress_ratio so different cr settings compile
separate kernels. assert restricts compress_ratio in {1, 4}.

Verified: 1152/1152 pytest configs pass on cr=1 default path.
Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…3 mb

Two paired host-wrapper heuristic refinements:

1. enable_warp_parallel_reduce: bool → Optional[bool] = None, default
   auto-coupled to num_threads_per_block: enabled iff threads == 1024 (32
   warps, where serial tid==0 cost dominates). At threads == 512 (16
   warps) the warp-parallel path measured a ~2pp synth regression so it
   stays off. Cache key sees the concrete bool. Explicit True/False still
   overrides for A/B testing.

2. tier-3 (large grid + large N) min_blocks_per_mp hardcoded "= 3"
   replaced by a (T, dtype) lookup:
     T == 1024 or dtype == fp32 → mb=2
     T == 512  and dtype in (bf16, fp16) → mb=3
   Derived from BS{256,384,512} × N{16K,32K,65K} × all 9 (dtype, K) sweep
   (gvr-topk-opt/sweep_tv_mb_kineto/mb_sweep.png). Old mb=3 default
   regressed by 25-37% on (T=512 + fp32 + large N/BS) configs because
   cap=42 starves the 4-LDG-inflight ILP (fp32 vec_w=4 × unroll-4 needs
   50+ regs). bf16/fp16 keep mb=3 since cvt-to-fp32 ILP fits in 40 regs
   and the extra CTA/SM (3 vs 2) hides cvt latency.

Pure default-policy change — no behavioral effect when caller passes
explicit values. Verified: pytest smoke 4/4 on cr=1 default path.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Two more host-wrapper Optional[*]=None defaults so callers no longer
need to pick threads/vec-bits per shape:

  num_threads_per_block (None default):
    1024 iff num_rows <= num_sms (1 CTA/SM bound) AND N >= 65536
    (so each of the 1024 threads has meaningful vec-loop work).
    Otherwise 512.

  use_256bit_load (None default):
    True iff dtype == fp32 AND N >= 16384.
    Half-prec (bf16/fp16) cvt-to-fp32 doubles fragment reg footprint
    and regresses 5-11% at K=512/1024; LDG already saturates at 128b
    anyway. fp32 N=8K dips 5-8% with 256b at small grid so the N
    threshold excludes that single tier.

Cache key sees concrete values; (None, X) and (None, Y) hash apart.
Explicit values still override for A/B testing.

Derivation: sweep BS{1,4,16,64,128,256,384,512} x N{4K..131K} x all 9
(dtype, K), gvr-topk-opt/sweep_tv_kineto/auto_speedup.csv. Net vs
baseline (T=512, V=128):
  - median speedup vs CUDA  1.09x -> 1.10x
  - mean   speedup vs CUDA  1.11x -> 1.15x  (+3.8pp)
  - max    speedup vs CUDA  1.45x -> 1.52x
  - 21 of 22 sp<1 configs were already sp<1 in baseline (BS=384 grid
    quirk, unrelated to this change). 1 new config introduces a 0.8pp
    sp<1 dip (within bench noise).

Pure default-policy change. Verified: 4-case auto-path smoke + pytest
smoke 4/4 on cr=1 default.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Decode runs under CUDA graph, where the (T, V) heuristic baked in at
capture time is reused across all replays. The capture-time
logits.shape[1] is typically much smaller than peak runtime N, so the
captured kernel misses the large-N (T=1024, V=256) path. Add an
optional max_seq_len hint so the caller (e.g. dsa.py) can pass the
peak compressed-N for the model; the heuristic then tunes the captured
kernel for the peak.

Usage guidance baked into the docstring + inline comment:
  * CUDA Graph mode: CALLER MUST PASS max_seq_len.
  * Eager mode: leave max_seq_len=None (heuristic adapts per call).

Rules with max_seq_len:
  * T=1024 threshold becomes dtype-aware to avoid half-prec K=512/1024
    small-N replay regression (14-16% when forced T=1024 at small N):
      fp32 -> 65536 (small-N replay 1-9% loss, net win)
      half -> 131072 (only forced at very large peak)
  * V=256 still gated by fp32 + N >= 16384.

Without max_seq_len, dtype-split is NOT applied because per-call
adaptive decisions never force T=1024 onto small N — heuristic only
fires for N >= 65536 by definition — so the half-prec N=65K-128K
+4-6% T=1024 win is preserved.

Cache key sees concrete (T, V), so different max_seq_len hints compile
distinct kernels. Pure default-policy extension. Verified with 4-case
auto smoke (no hint / fp32+131K / bf16+131K / bf16+200K).

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
The min_blocks_per_mp tier heuristic was still computing n_vec_iters
from logits.shape[1] (capture-time N). In graph mode with max_seq_len
hint, this would stick small-capture-N calls in tier-0 (mb=0) and
miss the tier-3 occupancy choice for large-N replays — same pitfall
the (T, V) heuristic was fixed against in the previous commit.

Switch to N_dec (= max_seq_len if provided, else logits.shape[1]) so
the tier classification is consistent with how T/V are picked.

Smoke 4/4.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Add a wave-fit branch in the fp32 tier-3 path: when num_rows ∈ (296, 444]
(i.e. fits 1 wave at 3 CTAs/SM but needs partial 2nd wave at 2 CTAs/SM
with num_sms=148), pick mb=3 instead of mb=2. This recovers ~15% perf
on fp32 BS=384 across (K, N) — verified against CUDA which already uses
__launch_bounds__(BS, 3) for this exact reason.

Math:
  mb=2 cap → 2 CTAs/SM × 148 SMs = 296 CTAs in 1 wave.
  mb=3 cap → 3 CTAs/SM × 148 SMs = 444 CTAs in 1 wave.
For BS=384 (× next_n=1):
  mb=2: 384 / 296 = 1.30 waves → tail wave wastes ~70% SMs.
  mb=3: 384 / 444 = 0.86 waves → 1 wave fits, max SM utilization.

Verified perf gains (fp32 T=512, both V=128 and V=256 default paths):
  fp32 K=512  N=4K-32K BS=384: +11-23%
  fp32 K=1024 N=4K-32K BS=384: +16-19%
  fp32 K=2048 N=8K-32K BS=384: +5-9%

Other BS unaffected:
  BS ≤ 296 (192, 256): mb=2 already fits 1 wave → rule keeps mb=2 (no change)
  BS > 444 (512): both need >1 wave → rule keeps mb=2 (ILP > occupancy)

Half-prec heuristic unchanged (already uses mb=3 in tier-3 via the
dtype-split path from a prior commit).

Bench artifacts: gvr-topk-opt/auto_full_bench/fp32_bs384_cluster/
(mb sweep CSV + NCU reports + drivers). Smoke: pytest 4/4 + spot tests
across BS={256, 384, 512}.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Functional change: limit wave-fit mb=3 branch to N <= 32768. Beyond
that threshold the kernel becomes bandwidth-bound and mb=3's 3-way L2
sharing causes contention; mb=2's lower occupancy gives each CTA more
bandwidth and wins +21-30% at fp32 K=512 N=65K BS=384.

The full wave-fit rule for fp32 tier-3 is now:
  if 2*num_sms < num_rows <= 3*num_sms and N_dec <= 32768:
      mb = 3
  else:
      mb = 2

Also cleans up file comments: remove obsolete TODO list, trim refs to
specific CUDA line numbers, simplify class docstring.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Registers torch.ops.trtllm.cute_dsl_gvr_topk_decode as the production
entry point for the cuTe DSL GVR Top-K decode kernel (Blackwell SM100).
Op writes values + indices into caller-allocated buffers (mutates_args
style), matching the existing cute_dsl_indexer_topk_decode pattern so
the DSA indexer pipeline can drop it in.

CuteDSLGvrTopKDecodeRunner takes ownership of the JIT compile cache and
the auto-heuristic for T (threads/block), V (vec-load width),
min_blocks_per_mp and enable_warp_parallel_reduce. The previous
module-level wrapper in gvr_topk_decode.py is removed; standalone
bench / A-B testing with the full tuning knob set lives in
tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py.

Tests:
- tests/unittest/.../test_cute_dsl_gvr_topk_decode.py: production
  correctness sweep via the op (dtype x K x N x next_n x batch_size x
  compress_ratio) with vectorized tie-aware + strict sort+allclose check.
- tests/scripts/.../run_gvr_topk.py: dual-mode driver -- pytest sweep
  over T/V/wp knobs and standalone CLI for single-case verification.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
The DSA indexer pipeline (dsa.py) and CUDA indexer_topk_decode op both
only read top-K indices from the kernel output; the value buffer is
caller-allocated scratch that's never consumed. Add a kernel-level
return_output_values switch so the cuTe DSL kernel can elide all
STG.value stores when the caller doesn't need them.

Kernel (gvr_topk_decode.py):
- GvrTopKKernel gains return_output_values: bool = True.
- All 9 STG.value sites + the output_values_row slice are gated under
  cutlass.const_expr(self.return_output_values), letting cute.compile
  eliminate the dead writes when False.

Op (cute_dsl_custom_ops.py):
- CuteDSLGvrTopKDecodeRunner adds return_output_values to the compile
  cache key + _compile signature; forward() hardcodes False, drops the
  output_values arg, and passes None for the value-output slot at
  launch (mirrors the optional-fake-tensor pattern at
  CuteDSLTopKDecodeMultiCTARunner._compile).
- trtllm::cute_dsl_gvr_topk_decode op signature drops output_values;
  mutates_args is now ("output_indices",), aligning with CUDA's
  indexer_topk_decode which also only exposes indices.

Tests:
- tests/unittest/.../test_cute_dsl_gvr_topk_decode.py drops the
  output_values buffer alloc + op kwarg (all 144 cases pass with the
  sort+allclose strict check).
- tests/scripts/.../run_gvr_topk.py wrapper exposes
  return_output_values as a knob so the standalone driver can still
  capture written values; the _compile cache + cute.compile
  out_values_fake placeholder are conditional on the flag.

SASS verification at bf16 K=1024 N=8K BS=384 confirms 88 STG.E.U16
writes are eliminated (kernel cubin -6KB, total SASS -416 lines).
On B200 SXM5 + synth_data, v5 (return_output_values=False) gives a
median 1.2% latency improvement over v4 with sp<1 configs nearly
halved (52 -> 27).

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
* Modernize type hints: replace ``typing.Tuple[...]`` with the built-in
  ``tuple[...]`` (Python 3.10+) and drop the ``from typing import Tuple``
  import in both ``_make_inputs`` / ``_tie_aware_correct`` helpers.
* Fix the ``pre_idx[..., 0]`` argmax invariant for next_n > 1: argmax
  must come from the kernel's effective scan range
  ``[0, N - next_n + 1)``, not full ``[0, N)``. With the prior full-N
  argmax, an index landing in the ``[N_eff, N)`` tail could violate the
  CUDA-side ``preIdxCount == topK`` dispatch precondition (kernel still
  produced correct top-K because pre_idx is only a Phase-1 hint, but
  the test was technically exercising the kernel under invalid input).

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
The kernel derives batch_size as logits.shape[0] / next_n implicitly,
then sizes pre_idx / seq_lens / output_indices accordingly. If the
divisibility breaks, the failure modes are either an OOB write or a
ZeroDivisionError raised from deep inside the JIT-compiled kernel —
neither is actionable for callers. Add an upfront check in the op
body so the contract violation surfaces with a clear message.

Other invariants (top_k in {512,1024,2048}, compress_ratio in {1,4},
logits dtype) are already enforced by GvrTopKKernel.__init__ via
GvrParams.get and the dtype switch.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
* Drop the single-use N_cols local in CuteDSLGvrTopKDecodeRunner.forward;
  fold it directly into the N_dec ternary for less noise.
* Add the input shape / dtype / knob signature to the info_once dedup
  key. Without the signature the first call's log message hid every
  subsequent shape from production diagnostics; now each new shape
  emits a single log line on its first run.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Both _tie_aware_check (unittest) and _tie_aware_correct (run_gvr_topk
standalone driver) previously assumed the reference scan range was
``N - next_n + ofs + 1`` per row. That matches the kernel for cr=1
and for cr>=2 with next_n in {1, 2}, but breaks for cr>=2 with
next_n>=3 because floor-division by cr makes per-row N_eff vary
within a group in ways the simple closed form can't express.

Switch both reference helpers to mirror the kernel's exact formula:

    actual_kv_len = seq_lens[row // next_n] - next_n + (row % next_n) + 1
    N_eff = actual_kv_len // compress_ratio  # cr=1 is identity

This requires the helpers to take ``seq_lens`` (and compress_ratio for
the standalone driver) so the reference can compute per-row N_eff
exactly as the kernel does. With this, any (next_n, cr) combo is
testable without the floor-division mismatch.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
- Document 32B alignment caller contract when use_256bit_load=True
  (no runtime assert; matches existing DSL op convention — see PR
  reply for risk-model rationale)
- Clarify return_output_values policy: op hardcodes False (matches
  CUDA indexer_topk_decode); kernel retains True branch for future
  caller flexibility
- Add preidx_hit_rate parametrize axis to op unittest (0.0
  worst-case + 0.5 realistic, matching V3.2/V4 Pro production
  preIdx∩topK overlap); test matrix 144 → 288
- Add 'Not in CI' header docstring to standalone driver
  run_gvr_topk.py explaining the trtllm-runtime-free design
- Add --num_sms CLI to standalone driver for heuristic-edge debug

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
Bug fixes:
- phase4_histogram_snap: add missing cute.arch.barrier() between the
  smem_hist[0..NW-1] read loop (recomputing block_min/block_max from
  warp-staged cmax slots) and the histogram-zeroing write loop. Without
  this, warp-0 thread-0 could finish the unrolled read and start
  zeroing smem_hist[0] while warp-N was still reading the staged cmax
  → squashed bmax_r → all candidates land in bin 0 → wrong K-th
  threshold. Hit-rate-dependent; covered by existing tests once they
  span realistic preIdx hit rates (NVIDIA#4 from prior review).
- Add runtime 32B alignment assert on logits.data_ptr() when the
  use_256bit_load heuristic fires (LDG.E.256 path). Catches view-with-
  unaligned-offset misuse with a clear error rather than silent
  miscompiled addresses / faults.

Code hygiene:
- Fix stale "unrolled for 64 times" comment in the block_min/max
  recompute loop; it unrolls num_warps times (16 or 32).
- Fix phase1_preidx_stats s_thr docstring: [3] [threshold, val_lo,
  val_hi]; pmax_saved lives in the separate s_thr_extra allocation.
- Document the (lge << 16) | lgt packing's <2^16 per-warp count
  precondition (currently holds via cand_count ≤ kC ≤ 6144 in
  GvrParams; future kC bump past 65536 would silently corrupt).

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
s_thr_extra was a [1]-wide fp32 smem buffer written twice in
phase1_preidx_stats (parallel + serial paths) to "save pmax", but
never read anywhere in the kernel — leftover from an earlier design
that was refactored. The same value is already in s_thr[2] at the
point of the redundant write; subsequent secant updates of s_thr[2]
don't need the original pmax.

Drops: smem allocation, phase1_preidx_stats parameter + call-site
arg, and 2 write statements.

Reported by @yuxianq in PR review.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
New kernel GvrTopKClusterKernel + driver run_gvr_topk_cluster.py, forked
from V5 GvrTopKKernel. Adds an SM_100 thread-block-cluster path so the
dominant Phase 2 secant-search work can be parallelised across multiple
CTAs that share a single row.

Architecture (cluster_size = 2 default):

  Grid       = (num_rows * cluster_size, 1, 1)
  Cluster    = (cluster_size, 1, 1)
  cta_in_cluster = cute.arch.block_idx_in_cluster()
  cluster_id (= row) = bidx // cluster_size
  Row slice per CTA: [cta_in_cluster*N/cs, (cta_in_cluster+1)*N/cs);
                     last CTA absorbs the N mod cs remainder.

  Phase 1 (preIdx stats):     every CTA scans pre_idx independently --
                              same input + deterministic math gives
                              identical s_thr / s_iscalars on every
                              CTA, no DSMEM broadcast required.
  Phase 2 (secant search):    every CTA scans its row slice; per-iter
                              block_count_ge ends with an all-reduce of
                              per-CTA cand_count through DSMEM
                              (s_cluster_partial slot at the same SMEM
                              offset in every CTA; mapa.shared::cluster
                              + ld.shared::cluster.u32 from each peer).
                              The cluster total lands in s_iscalars[0]
                              on every CTA, and each CTA's tid==0 runs
                              the same secant update against it,
                              keeping s_thr in sync across the cluster
                              without an explicit broadcast.
  Cluster handoff:            one final cluster_arrive_relaxed +
                              cluster_wait. From here on only the
                              leader (cta_in_cluster == 0) participates;
                              peers fall through to the kernel epilogue.
                              The leader resets slice to [0, N), re-runs
                              block_count_ge with do_cluster_aggregation
                              = False on the full row to refresh
                              smem_ptcnt (Phase 2's last block_count_ge
                              cached slice-local counts, which would
                              give wrong Phase 3 prefix sums), then
                              proceeds.
  Phase 3 (collect):          leader-only, full row.
  Phase 4 (histogram snap):   leader-only.

  cluster_size == 1 is a degenerate fast path -- DSMEM, cluster
  barriers, and the leader-only handoff all compile out via const_expr
  guards, so the kernel is byte-for-byte equivalent to V5 in that case.

DSMEM primitives (inline PTX, adapted from
single_pass_multi_cta_radix_topk_cluster.py):
  mapa.shared::cluster.u32       translate a local SMEM ptr into a
                                 peer CTA's address-space view.
  ld.shared::cluster.{u32,f32}   load int32 / fp32 from peer SMEM.

The MVP does not yet parallelise Phase 3/4 across the cluster (Phase
3+4 are leader-only and rescan the full row). That leaves the gather
optimisation -- "each CTA collects candidates from its slice, leader
DSMEM-merges into its smem_keys, then runs Phase 4" -- as the next
step. Estimated speedup of the current MVP at N=131K BS=64 is about
~30% (Phase 2 halves; Phase 3/4 unchanged; +2us for the leader's
full-row smem_ptcnt refresh).

Validation: 72 pytest cases pass on
  dtype  in {fp32, bf16, fp16}
  K      in {512, 1024, 2048}
  N      in {4096, 65536}
  BS     in {1, 32}
  cluster_size in {1, 2}
All assertions use the tie-aware sorted-value comparator already used
by the V5 driver, so candidates that differ only at threshold ties
(common with random data) are still accepted.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…input size

TRUNCATE retains at most filtered_topk_smem_input_size threshold-bin
candidates. When top_k exceeds that size (fp32 large_occupancy: S=8192,
top_k up to 16384), a dense threshold bin can leave fewer than top_k
valid candidates, and the unfilled s_indices tail is written back as
undefined/duplicate indices -- violating the kernel's no-duplicate
contract. This configuration is under-determined, so reject it at
construction and direct callers to REREAD or GMEM_SPILL.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…ants

The CuTe DSL radix-filter decode top-k (cute_dsl_indexer_topk_decode)
JIT-compiles per compile-key on first touch (~seconds). num_cols is fixed
at indexer_max_seq_len for a deployment, but cluster_size =
auto_cluster_size(num_cols, num_rows) steps across num_rows occupancy bands.
CUDA-graph warmup only exercises cuda_graph_batch_sizes; eager decode iters
(mixed prefill+decode batch, or cuda_graph disabled) whose num_rows lands in
an uncovered band otherwise pay the JIT stall on a live request.

Add warmup_cute_dsl_radix_topk_decode (custom_ops) that issues one decode
per num_rows band so every cluster_size variant compiles during warmup, a
DSA metadata method that supplies the deployment params, and a
ModelEngine.warmup hook that runs it after the DG metadata prewarm. Mirrors
the existing _warmup_dg_paged_mqa_logits_metadata pattern. No-op unless
decode routes to the radix-filter DSL path.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…atch modes

cute_dsl_indexer_topk_decode selects among three dispatch modes via
overlapping booleans whose precedence is only the if/elif ordering
(radix_filter_single_pass_multi_cta > single_pass_multi_cta > 2-pass).
radix_filter_single_pass_multi_cta defaults True, so a caller passing
single_pass_multi_cta=True without also disabling it was silently rerouted
to the radix-filter path.

Rewrite the (stale, two-mode) docstring to describe all three modes and
their precedence, marking radix-filter as the adaptive best-performance
default and the others as legacy A/B-and-fallback overrides. Add an assert
that rejects a conflicting mode-2/3 override when mode 1 is enabled, so the
override fails loudly instead of being silently ignored. No behavior change
for existing callers (none pass the conflicting combination).

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…er cluster decode

In the single-pass multi-CTA radix-filter decode path, a row whose effective
length spans more than one chunk (need_cluster_sync) but is <= top_k has a
cluster-merged histogram total (== eff_len) that never exceeds top_k, so the
radix threshold search never fires and the output is undefined. The single-CTA
path avoids this via its take_trivial shortcut, but the cluster path forces
take_trivial off to keep every CTA cooperating on the barriers.

Fix at the dispatch layer: when eff_len <= top_k there is nothing to filter,
so collapse to the existing solo trivial fast path -- clear need_cluster_sync
(cluster-uniform, so only cta 0 runs and no CTA waits on a barrier) and hand
cta 0 the full row. No kernel change. Add a regression case (fp32 N=32768,
top_k=16384, eff=12000) to test_cute_dsl_topk_decode_high_and_odd_k.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
The comment claimed no explicit CuTe DSL top-k warmup is needed; that is no
longer accurate now that ModelEngine.warmup pre-compiles the radix-filter
cluster_size variants. Remove it (review nit).

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
…k.py

Consolidate the top-k custom-op tests in one file (review nit). Move the four
overflow-policy tests and their unique helpers from
test_cute_dsl_topk_overflow_policy.py into test_indexer_topk.py and delete the
former. The moved tests reuse the generate_seq_lens / compare_top_k_results /
create_random_logits helpers already defined in test_indexer_topk.py instead
of re-importing the source-module copies, removing the duplication. 176 moved
cases pass.

Signed-off-by: Mindy Li <11663212+limin2021@users.noreply.github.com>
@limin2021
limin2021 requested a review from a team as a code owner July 27, 2026 02:10
@limin2021
limin2021 requested review from bo-nv and pcastonguay July 27, 2026 02:10
@limin2021

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@longcheng-nv longcheng-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

All review comments are addressed on this head — approving.

Verified on 6004753:

  • TRUNCATE top_k > SMEM input size (57437ae): re-ran my duplicate-indices repro (fp32 N=32768, top_k=16384, large_occupancy, all-tie + randn, B200). The kernel now rejects the configuration at construction with a clear ValueError, and REREAD remains exact on the same input (0 bad rows). Docstring now states the constraint.
  • Warmup (78751cd): the band-representative decode warmup covers exactly the cluster_size compile-key dimension the CUDA-graph path misses; the dispatch guards in warmup_cute_dsl_radix_topk mirror the runtime routing (heuristic top-k / C++-op cases correctly skipped), and per-band failures are best-effort so a broken bucket can't abort startup.
  • Dispatch modes (6981308): documented precedence + assert on conflicting mode-2/3 overrides, no behavior change for existing callers.
  • Also reviewed the _eff <= top_k cluster fix (a4c3923, cluster-uniform collapse to the solo trivial path) with its regression test, and the test relocation into test_indexer_topk.py.

One optional follow-up (non-blocking): a small negative test asserting the new ValueError for TRUNCATE with top_k > filtered_topk_smem_input_size would pin the contract.

Merge still needs a green CI run on this head.

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61773 [ run ] triggered by Bot. Commit: 6004753 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61773 [ run ] completed with state SUCCESS. Commit: 6004753
/LLM/main/L0_MergeRequest_PR pipeline #49975 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@limin2021

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61845 [ run ] triggered by Bot. Commit: 6004753 Link to invocation

@zongfeijing zongfeijing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61845 [ run ] completed with state FAILURE. Commit: 6004753
/LLM/main/L0_MergeRequest_PR pipeline #50040 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@limin2021

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61894 [ run ] triggered by Bot. Commit: 6004753 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #61894 [ run ] completed with state FAILURE. Commit: 6004753
/LLM/main/L0_MergeRequest_PR pipeline #50085 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@limin2021

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62049 [ run ] triggered by Bot. Commit: 6004753 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62049 [ run ] completed with state SUCCESS. Commit: 6004753
/LLM/main/L0_MergeRequest_PR pipeline #50234 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

@limin2021

Copy link
Copy Markdown
Collaborator Author

/bot run --disable-fail-fast

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62182 [ run ] triggered by Bot. Commit: 6004753 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #62182 [ run ] completed with state SUCCESS. Commit: 6004753
/LLM/main/L0_MergeRequest_PR pipeline #50354 completed with status: 'SUCCESS'

CI Report

Link to invocation

@yuxianq
yuxianq merged commit f5cbe6b into NVIDIA:main Jul 29, 2026
7 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.

10 participants