From 45e16a2de0bb3a2db71fd010c1294b4a4d5c8526 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:06:18 -0700 Subject: [PATCH 001/117] [None][perf] GVR top-k: block-skip for the R0 M-ary count pass and Phase-3 collect Port the block-skip consumer from the skip-finegrain development chain onto the R0 (op#26) architecture, measured-optimal configuration only (grain 32, int16 active list = 16KB SMEM, strided coalesced 3-barrier build, UN=2 software-pipelined compact scan). The active-block list is built once per row at the loosest rung (lossless for every rung count and the collect); phase3's compact stream-write replays the count pass's per-thread walk order so prefix-sum positions stay exact; a list-current flag pairs the two and is cleared on any dense fallback. Misaligned slice starts fall through to the dense path. Contract: workspace/epilogue_topk_interface.md. Correctness: REAL-data smoke (flash 16k/64k/256k/1024k + pro 64k/1024k, dense + skip arms, exactness contract) all pass incl. hit_rate=0.08. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 381 ++++++++++++++++-- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 178 ++++++++ 2 files changed, 535 insertions(+), 24 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 1fe0dfa5eddc..68a264c14323 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -270,6 +270,7 @@ def __init__( p4_tail_fast: Optional[bool] = None, # [p4tt] p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, + enable_block_skip: bool = False, ): # Redundant-warp sync reduction: every warp replays the block # reduce + decision from the same staged SMEM partials in the @@ -423,6 +424,25 @@ def __init__( self.enable_r0 = bool(enable_r0) self.mt_unroll = int(mt_unroll) self.fb_fix = bool(fb_fix) + # enable_block_skip: gate the R0 M-ary count pass and the Phase-3 + # stream-write on per-32-position upper bounds emitted by the + # indexer epilogue (``block_max [num_rows, nb_pad*4]`` fp32, record + # r = exact max of positions [r*32, r*32+32) of the POST-CONVERSION + # stored logits — contract in workspace/epilogue_topk_interface.md). + # Lossless: the active-block list is built at the LOOSEST rung, so a + # skipped block cannot contain any element >= any rung and every + # rung count (and the collect) equals its dense value. The port + # ships the measured-optimal configuration only: grain 32, int16 + # list entries (16KB SMEM, 3 CTA/SM at T512), strided coalesced + # O(1)-barrier build, UN=2 software-pipelined compact scan. + self.enable_block_skip = bool(enable_block_skip) + self.SKIP_BLOCK = 32 + self.SKIP_BLOCK_LOG2 = 5 + self.SKIP_MAX_BLOCKS = 8192 # covers N up to 262144 at grain 32 + self.SKIP_UNROLL = 2 + self.skip_order = "grouped" + if enable_block_skip and num_threads not in (512, 1024): + raise ValueError("enable_block_skip requires num_threads in {512, 1024}") # C7 dispatch (op#26 host policy folded into the ctor; all gated on # enable_r0 so an OFF kernel is byte-identical to the base): # - qfracs default = M2D (0.85, 0.35): dispatch_r0_op26 ships M2D for @@ -1377,6 +1397,105 @@ def block_count_ge( # totals are the answer. smem_ptcnt_multi holds slice-local per-thread # columns (the accepted rung's column seeds Phase 3 per CTA). # ------------------------------------------------------------------ + + # ---- block-skip machinery (enable_block_skip; ported from the + # skip-finegrain development chain, measured-optimal configuration + # only: grain 32, int16 list, grouped strided build, UN=2 scan) ---- + + @cute.jit + def _list_ld(self, smem_active, idx): + return cutlass.Int32(smem_active[idx]) + + @cute.jit + def _list_st(self, smem_active, idx, val): + smem_active[idx] = cutlass.Int16(val) + + @cute.jit + def _block_bound(self, bm_addr, blk_id): + # grain 32: record blk_id IS the exact positional bound of + # [blk_id*32, blk_id*32+32) (indexer TMEM partition contract) — + # one 4B scalar load, no fold. + bm_ptr = cute.make_ptr( + cutlass.Float32, + bm_addr + cutlass.Int64(blk_id) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + return cute.make_tensor(bm_ptr, cute.make_layout((1,)))[0] + + @cute.jit + def _full_build_active( + self, + block_max_row, + slice_start, + slice_end, + threshold, + smem_wcnt, + smem_active, + s_active_cnt, + tidx, + warp_id, + lane, + ): + # Two-phase register-bitmask build. Each thread owns ids_per_thread + # block ids STRIDED (t, t+T, t+2T, ...) so each pass's bound loads + # coalesce warp-wide. Phase A flags+counts with no sync; phase B is + # ONE block-wide exclusive scan over per-thread counts; phase C + # scatters from the bitmask — 3 barriers TOTAL, independent of + # nb_slice. List order is thread-grouped, not ascending: fine, since + # the count scan and the Phase-3 stream-write walk the SAME list by + # position (determinism contract). + ids_per_thread = cutlass.const_expr(self.SKIP_MAX_BLOCKS // self.num_threads) + num_threads = cutlass.const_expr(self.num_threads) + blk_lo = slice_start >> cutlass.Int32(self.SKIP_BLOCK_LOG2) + blk_hi = (slice_end + cutlass.Int32(self.SKIP_BLOCK - 1)) >> cutlass.Int32( + self.SKIP_BLOCK_LOG2 + ) + nb_slice = blk_hi - blk_lo + bm_addr = block_max_row.iterator.toint() + + # Phase A (no sync): flag my ids into a register bitmask. + bmask = cutlass.Int32(0) + cnt = cutlass.Int32(0) + for m in cutlass.range_constexpr(ids_per_thread): + ib = tidx + cutlass.Int32(m * num_threads) + if ib < nb_slice: + bound = self._block_bound(bm_addr, blk_lo + ib) + if bound >= threshold: + bmask = bmask | (cutlass.Int32(1) << cutlass.Int32(m)) + cnt = cnt + cutlass.Int32(1) + + # Phase B: one block-wide exclusive scan over per-thread counts. + tp = cnt + for off_i in cutlass.range_constexpr(5): + off_v = cutlass.const_expr(1 << off_i) + other = cute.arch.shuffle_sync_up(tp, off_v, mask_and_clamp=0) + if lane >= cutlass.Int32(off_v): + tp = tp + other + excl = tp - cnt + warp_total = cute.arch.shuffle_sync(tp, cutlass.Int32(self.WARP_SIZE - 1)) + if lane == 0: + smem_wcnt[warp_id] = warp_total + cute.arch.barrier() + if tidx == 0: + tot = cutlass.Int32(0) + for w in cutlass.range_constexpr(self.num_warps): + cw = smem_wcnt[w] + smem_wcnt[w] = tot + tot = tot + cw + s_active_cnt[0] = tot + cute.arch.barrier() + + # Phase C: scatter from the bitmask at deterministic offsets. + pos_out = smem_wcnt[warp_id] + excl + if bmask != cutlass.Int32(0): + for m in cutlass.range_constexpr(ids_per_thread): + if (bmask & (cutlass.Int32(1) << cutlass.Int32(m))) != cutlass.Int32(0): + ib_c = tidx + cutlass.Int32(m * num_threads) + self._list_st(smem_active, pos_out, blk_lo + ib_c) + pos_out = pos_out + cutlass.Int32(1) + cute.arch.barrier() + @cute.jit def block_count_ge_multi( self, @@ -1393,6 +1512,9 @@ def block_count_ge_multi( warp_id, lane, smem_ptcnt=None, # vseed: last column's per-thread counts land here + block_max_row=None, # block-skip: per-32-position upper bounds + smem_active=None, # block-skip: int16 active list + s_active_cnt=None, # block-skip: [0]=list length, [1]=list-current flag ): M = cutlass.const_expr(self.M_thr) num_threads = cutlass.const_expr(self.num_threads) @@ -1416,7 +1538,97 @@ def block_count_ge_multi( i = slice_start + tidx * cutlass.Int32(vec_w) step = cutlass.Int32(step_elem) - if self.enable_unroll_4: + # ---- block-skip compact iteration (lossless vs the dense path) ---- + # Build the active list at the LOOSEST threshold over all M columns: + # a skipped block bounds every element below min(t_m), so all M + # counts equal their dense values. Slice boundaries must be + # 32-aligned for the list's block<->position mapping to stay inside + # the slice (runtime-uniform guard; misaligned slices fall through + # to the dense path below via skip_ok == 0). + skip_ok = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): + if (slice_start & cutlass.Int32(self.SKIP_BLOCK - 1)) == cutlass.Int32(0): + skip_ok = cutlass.Int32(1) + if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): + if skip_ok == cutlass.Int32(1): + tmin = thr_frag[0] + for m in cutlass.range_constexpr(M): + tmin = _fmin_f32_inline(tmin, thr_frag[m]) + # smem_wcnt_multi's first num_warps slots double as the + # build's warp-total scratch: the build's own barriers + # complete before any count lands there. + self._full_build_active( + block_max_row, + slice_start, + slice_end, + tmin, + smem_wcnt_multi, + smem_active, + s_active_cnt, + tidx, + warp_id, + lane, + ) + if tidx == 0: + s_active_cnt[1] = cutlass.Int32(1) # list-current flag + chunks_per_block = cutlass.const_expr( + self.SKIP_BLOCK // (self.vec_bits // self.dtype.width) + ) + tpb = cutlass.const_expr(chunks_per_block) + blocks_per_iter = cutlass.const_expr(self.num_threads // chunks_per_block) + UN = cutlass.const_expr(self.SKIP_UNROLL) + stride_un = cutlass.const_expr(blocks_per_iter * UN) + my_blk_slot = tidx // cutlass.Int32(tpb) + my_chunk0 = tidx % cutlass.Int32(tpb) + cnt_active = s_active_cnt[0] + frags = [cute.make_fragment((vec_w,), self.dtype) for _ in range(UN)] + li = my_blk_slot + while li < cnt_active: + poss = [] + valids = [] + for u in cutlass.range_constexpr(UN): + lu = li + cutlass.Int32(u * blocks_per_iter) + valid = lu < cnt_active + pos0 = cutlass.Int32(0) + if valid: + blk = self._list_ld(smem_active, lu) + pos0 = blk * cutlass.Int32(self.SKIP_BLOCK) + my_chunk0 * cutlass.Int32( + vec_w + ) + src_ptr_u = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(pos0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + copy_atom, + cute.make_tensor(src_ptr_u, cute.make_layout((vec_w,))), + frags[u], + ) + poss.append(pos0) + valids.append(valid) + for u in cutlass.range_constexpr(UN): + if valids[u]: + pos = poss[u] + if pos + cutlass.Int32(vec_w) <= slice_end: + for j in cutlass.range_constexpr(vec_w): + if cutlass.const_expr(self.dtype == cutlass.Float32): + vj = frags[u][j] + else: + vj = cutlass.Float32(frags[u][j]) + for m in cutlass.range_constexpr(M): + cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vj >= thr_frag[m]) + else: + jj = pos + while jj < slice_end: + vs = self._load_fp32(input_row, jj) + for m in cutlass.range_constexpr(M): + cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vs >= thr_frag[m]) + jj = jj + cutlass.Int32(1) + li = li + cutlass.Int32(stride_un) + + if self.enable_unroll_4 and skip_ok == cutlass.Int32(0): rng_frag = cute.make_fragment((vec_w,), self.dtype) big_iters = cutlass.Int32(0) if slice_end > i + cutlass.Int32(vec_w - 1): @@ -1443,30 +1655,31 @@ def block_count_ge_multi( i = i + big_iters * cutlass.Int32(step_elem) tail_frag = cute.make_fragment((vec_w,), self.dtype) - while i + cutlass.Int32(vec_w - 1) < slice_end: - src_ptr = cute.make_ptr( - self.dtype, - row_addr + cutlass.Int64(i) * cutlass.Int64(elem_bytes), - cute.AddressSpace.gmem, - assumed_align=vec_align, - ) - src = cute.make_tensor(src_ptr, cute.make_layout((vec_w,))) - cute.copy(copy_atom, src, tail_frag) - for j in cutlass.range_constexpr(vec_w): - if cutlass.const_expr(self.dtype == cutlass.Float32): - vj = tail_frag[j] - else: - vj = cutlass.Float32(tail_frag[j]) - for m in cutlass.range_constexpr(M): - cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vj >= thr_frag[m]) - i = i + step + if skip_ok == cutlass.Int32(0): + while i + cutlass.Int32(vec_w - 1) < slice_end: + src_ptr = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(i) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + src = cute.make_tensor(src_ptr, cute.make_layout((vec_w,))) + cute.copy(copy_atom, src, tail_frag) + for j in cutlass.range_constexpr(vec_w): + if cutlass.const_expr(self.dtype == cutlass.Float32): + vj = tail_frag[j] + else: + vj = cutlass.Float32(tail_frag[j]) + for m in cutlass.range_constexpr(M): + cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vj >= thr_frag[m]) + i = i + step - it = n_aligned + tidx - while it < slice_end: - v = self._load_fp32(input_row, it) - for m in cutlass.range_constexpr(M): - cnt_frag[m] = cnt_frag[m] + cutlass.Int32(v >= thr_frag[m]) - it = it + cutlass.Int32(num_threads) + it = n_aligned + tidx + while it < slice_end: + v = self._load_fp32(input_row, it) + for m in cutlass.range_constexpr(M): + cnt_frag[m] = cnt_frag[m] + cutlass.Int32(v >= thr_frag[m]) + it = it + cutlass.Int32(num_threads) for m in cutlass.range_constexpr(M): if cutlass.const_expr(self.r0_vseed and m == self.M_qf): @@ -1813,6 +2026,8 @@ def phase3_collect_candidates( lane, do_cluster_sync, # bool: False = cs=1 / short-row degrade (skip cluster sync) smem_input=None, # optional SMEM-cached slice + smem_active=None, # block-skip: int16 active list (reused, not rebuilt) + s_active_cnt=None, # block-skip: [0]=list length, [1]=list-current flag ): """Retry-shrink (when P2 didn't converge) + prefix sum + stream-write. @@ -1977,6 +2192,72 @@ def phase3_collect_candidates( wc = my_write_pos step = cutlass.Int32(step_elem) + # ---- block-skip compact stream-write ---- + # Reuses the active list left by the R0 compact count pass (same + # ownership walk: my_blk_slot's list slots in ascending order, so + # each thread produces its candidates in the SAME per-thread order + # the count pass counted them — the prefix-sum positions match). + # Only taken when the list is CURRENT (s_active_cnt[1] == 1, set by + # the build; cleared on any dense fallback re-count). + skip_wr = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip and smem_active is not None): + if s_active_cnt[1] == cutlass.Int32(1): + skip_wr = cutlass.Int32(1) + if cutlass.const_expr(self.enable_block_skip and smem_active is not None): + if skip_wr == cutlass.Int32(1): + chunks_per_block_w = cutlass.const_expr( + self.SKIP_BLOCK // (self.vec_bits // self.dtype.width) + ) + blocks_per_iter_w = cutlass.const_expr(self.num_threads // chunks_per_block_w) + my_blk_slot_w = tidx // cutlass.Int32(chunks_per_block_w) + my_chunk0_w = tidx % cutlass.Int32(chunks_per_block_w) + cnt_active_w = s_active_cnt[0] + wfrag = cute.make_fragment((vec_w,), self.dtype) + li_w = my_blk_slot_w + while li_w < cnt_active_w: + blk_w = self._list_ld(smem_active, li_w) + pos0_w = blk_w * cutlass.Int32(self.SKIP_BLOCK) + my_chunk0_w * cutlass.Int32( + vec_w + ) + src_ptr_w = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(pos0_w) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + copy_atom, + cute.make_tensor(src_ptr_w, cute.make_layout((vec_w,))), + wfrag, + ) + if pos0_w + cutlass.Int32(vec_w) <= slice_end: + for j in cutlass.range_constexpr(vec_w): + if cutlass.const_expr(self.dtype == cutlass.Float32): + vj = wfrag[j] + else: + vj = cutlass.Float32(wfrag[j]) + if vj >= thr_final and wc < cutlass.Int32(kCC): + smem_keys[wc] = vj + smem_vals[wc] = pos0_w + cutlass.Int32(j) + wc = wc + cutlass.Int32(1) + else: + jj_w = pos0_w + while jj_w < slice_end: + v_w = self._load_fp32(input_row, jj_w) + if v_w >= thr_final and wc < cutlass.Int32(kCC): + smem_keys[wc] = v_w + smem_vals[wc] = jj_w + wc = wc + cutlass.Int32(1) + jj_w = jj_w + cutlass.Int32(1) + li_w = li_w + cutlass.Int32(blocks_per_iter_w) + + # When the compact write ran, park the dense cursors at the end so + # all three dense loops below (4-way, vec tail, scalar tail) fall + # through without re-indenting them. + if skip_wr == cutlass.Int32(1): + ic = N_local + n_aligned = N_local + # Phase3 unrolling: master gated by self.enable_phase3_unroll. # When OFF, only the tail 1-way loop runs (matches the pre-unroll # state of phase3_collect). When ON, the inner enable_unroll_4 @@ -3717,6 +3998,7 @@ def gvr_topk_kernel( output_values: cute.Tensor, # [numRows, top_k] dtype output_indices: cute.Tensor, # [numRows, top_k] int32 order_row: cute.Tensor, # [batch_size] int32 (or None when seqlen_sorted=False) + block_max: cute.Tensor, # [numRows, nb_pad*4] fp32 (or None: no block-skip) ): """Thin entry: bidx → row_idx → run_one_row. @@ -3768,6 +4050,7 @@ def gvr_topk_kernel( seq_lens, output_values, output_indices, + block_max=block_max, ) @cute.jit @@ -3779,6 +4062,7 @@ def run_one_row( seq_lens: cute.Tensor, # [numRows / next_n] int32 output_values: cute.Tensor, # [numRows, top_k] dtype, optional output_indices: cute.Tensor, # [numRows, top_k] int32 + block_max: cute.Tensor = None, # [numRows, nb_pad*4] fp32 ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -3835,6 +4119,10 @@ def run_one_row( # Slice per-row views. input_row = input_data[row_idx, None] pre_idx_row = pre_idx[pre_idx_row_idx, None] + if cutlass.const_expr(self.enable_block_skip and block_max is not None): + block_max_row = block_max[row_idx, None] + else: + block_max_row = None # When return_output_values=False, ``output_values`` is None at # launch and the gated writes below are compiled out; slicing into # None would crash so we keep the view None as well. @@ -3874,6 +4162,22 @@ def run_one_row( layout=cute.make_ordered_layout((num_threads,), order=(0,)), byte_alignment=128, ) + # block-skip: int16 active list (16KB at 8192 entries) + control + # ([0] list length, [1] list-current flag for the Phase-3 reuse). + if cutlass.const_expr(self.enable_block_skip): + smem_active = smem.allocate_tensor( + element_type=cutlass.Int16, + layout=cute.make_ordered_layout((self.SKIP_MAX_BLOCKS,), order=(0,)), + byte_alignment=128, + ) + s_active_cnt = smem.allocate_tensor( + element_type=cutlass.Int32, + layout=cute.make_ordered_layout((2,), order=(0,)), + byte_alignment=16, + ) + else: + smem_active = None + s_active_cnt = None # warp_counts[NUM_WARPS] int32 (P3 prefix-sum scratch) # p2_warp_redundant parity-banks the Phase-2 staging (a warp one # round ahead writes the other half) — costs num_warps*4 bytes. @@ -4114,6 +4418,9 @@ def run_one_row( tidx, warp_id, lane, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) else: # Short row: only CTA 0 scans the full row; the other @@ -4155,6 +4462,9 @@ def run_one_row( tidx, warp_id, lane, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) else: # cs=1: one CTA per row, no cluster sync. @@ -4193,6 +4503,9 @@ def run_one_row( tidx, warp_id, lane, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) griddepcontrol_launch_dependents() @@ -4234,6 +4547,9 @@ def _run_phases( tidx, warp_id, lane, + block_max_row=None, # block-skip: this row's per-32-position bounds + smem_active=None, + s_active_cnt=None, ): """Run Phase 1-4 + final cluster barrier on a given row slice. @@ -4248,6 +4564,13 @@ def _run_phases( cluster_size = cutlass.const_expr(self.cluster_size) is_leader = cta_in_cluster == cutlass.Int32(0) + # block-skip: the list-current flag starts INVALID every row; only + # the R0 compact pass's build sets it. Ordered ahead of all readers + # by Phase 1's internal barriers. + if cutlass.const_expr(self.enable_block_skip): + if tidx == cutlass.Int32(0): + s_active_cnt[1] = cutlass.Int32(0) + # ---- Phase 1: preIdx Min/Max/Mean ---- self.phase1_preidx_stats( input_row, @@ -4357,6 +4680,9 @@ def _run_phases( warp_id, lane, smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) cute.arch.barrier() if tidx == 0: @@ -4409,6 +4735,9 @@ def _run_phases( # count passes (op#26 efficiency) instead of ~6. done=1 on # accept so Phase 3 skips its retry-shrink. if bc < cutlass.Int32(0): + if cutlass.const_expr(self.enable_block_skip): + if tidx == cutlass.Int32(0): + s_active_cnt[1] = cutlass.Int32(0) if cutlass.const_expr(self.fb_fix): if tidx == cutlass.Int32(0): M = cutlass.const_expr(self.M_thr) @@ -4604,6 +4933,8 @@ def _run_phases( lane, do_cluster_sync=do_cluster_sync, smem_input=smem_input, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) # Cluster handoff #2: leader's DSMEM gather of peer @@ -4752,6 +5083,7 @@ def __call__( output_values: cute.Tensor, # or None. output_indices: cute.Tensor, order_row: cute.Tensor, # or None when seqlen_sorted=False + block_max: cute.Tensor, # or None: block-skip disabled stream, ): num_rows = input_data.shape[0] @@ -4774,6 +5106,7 @@ def __call__( output_values, output_indices, order_row, + block_max, ).launch( grid=(total_ctas, 1, 1), block=(self.num_threads, 1, 1), diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 41cb23260cd4..bd028c07a7ee 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -64,6 +64,7 @@ def _compile( seqlen_sorted: bool = False, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, + enable_block_skip: bool = False, ): """JIT-compile the GVR kernel for a specific knob combination. @@ -122,6 +123,16 @@ def _compile( if seqlen_sorted else None ) + block_max_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Float32, + (n_rows, cute.sym_int()), + stride_order=(1, 0), + assumed_align=16, + ) + if enable_block_skip + else None + ) fake_stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True) kernel = GvrTopKKernel( dtype=cute_dtype, @@ -140,6 +151,7 @@ def _compile( seqlen_sorted=seqlen_sorted, p4_warp_redundant=p4_warp_redundant, p2_warp_redundant=p2_warp_redundant, + enable_block_skip=enable_block_skip, ) return cute.compile( kernel, @@ -149,11 +161,163 @@ def _compile( out_values_fake, out_indices_fake, order_row_fake, + block_max_fake, stream=fake_stream, options="--enable-tvm-ffi", ) +_FLT_MAX = torch.finfo(torch.float32).max +_META_BLOCK = 128 +_META_RECS_PER_BLOCK = 4 # warp-partial records per block (indexer layout) + + +def _row_n_eff( + seq_lens: torch.Tensor, + num_rows: int, + next_n: int, + compress_ratio: int, +) -> torch.Tensor: + """Per-row effective scan length, mirroring the kernel formula.""" + dev = seq_lens.device + rows = torch.arange(num_rows, device=dev) + sl = seq_lens[rows // next_n].to(torch.int64) + actual = sl - next_n + (rows % next_n) + 1 + return actual // compress_ratio + + +def emu_block_max( + logits: torch.Tensor, + seq_lens: torch.Tensor, + next_n: int = 1, + compress_ratio: int = 1, + tail_mode: str = "pad_inf", + records: str = "rotate", +) -> torch.Tensor: + """``[num_rows, nb_pad*4] fp32`` warp-partial upper-bound records. + + tail_mode: + "exact": tight bound — max over valid positions only. + "pad_inf": a partially-valid tail unit is forced to +FLT_MAX, the + worst legal inflation (the indexer masks by request-level + ctx >= N_eff, so tail positions can inflate the bound). + Tests use this to prove inflated bounds only *disable* + skipping, never break correctness. + records: + "rotate": fold-correctness fixture — the 128-block max lands in + ONE slot rotated by blk % 4, the other 3 hold + -FLT_MAX. Any 4 values folding to the block max are + legal; the rotation makes a consumer that drops or + mis-reads partial slots fail the tests. Valid ONLY + for grain-128 consumers (slots are NOT positional). + "positional": production semantics — record r is the exact max of + positions [r*32, r*32+32) (the indexer's TMEM T2R + partition gives warp w of a tile the contiguous + positions [tile*128 + w*32, +32)). Required for + skip_grain=32; also a legal grain-128 input (its + fold is the block max). + """ + assert tail_mode in ("exact", "pad_inf") + assert records in ("rotate", "positional") + R, C = logits.shape + nb = (C + _META_BLOCK - 1) // _META_BLOCK + dev = logits.device + n_eff = _row_n_eff(seq_lens, R, next_n, compress_ratio).unsqueeze(1) + lf = logits.to(torch.float32) + pad = nb * _META_BLOCK - C + if pad: + lf = torch.nn.functional.pad(lf, (0, pad), value=float("-inf")) + pos = torch.arange(nb * _META_BLOCK, device=dev).unsqueeze(0) + masked = torch.where(pos < n_eff, lf, torch.full_like(lf, float("-inf"))) + if records == "positional": + sub = _META_BLOCK // _META_RECS_PER_BLOCK # 32 positions/record + nrec = nb * _META_RECS_PER_BLOCK + rmax = masked.view(R, nrec, sub).amax(-1) + if tail_mode == "pad_inf": + rec_start = torch.arange(nrec, device=dev).unsqueeze(0) * sub + partial = (rec_start < n_eff) & (rec_start + sub > n_eff) + rmax = torch.where(partial, torch.full_like(rmax, _FLT_MAX), rmax) + return rmax.contiguous() + bmax = masked.view(R, nb, _META_BLOCK).amax(-1) + if tail_mode == "pad_inf": + blk_start = torch.arange(nb, device=dev).unsqueeze(0) * _META_BLOCK + partial = (blk_start < n_eff) & (blk_start + _META_BLOCK > n_eff) + bmax = torch.where(partial, torch.full_like(bmax, _FLT_MAX), bmax) + out = torch.full((R, nb, _META_RECS_PER_BLOCK), -_FLT_MAX, dtype=torch.float32, device=dev) + slot = torch.arange(nb, device=dev) % _META_RECS_PER_BLOCK + out[:, torch.arange(nb, device=dev), slot] = bmax + return out.reshape(R, nb * _META_RECS_PER_BLOCK).contiguous() + + +# Rung offsets below each 32-position record's max for the meta-seed +# metadata (L6_geo.25 — offline-validated: 1.00 real scans on synth + +# real captures for both V4 models with the kernel's S=16 x 2-pass +# quantized search). +_META_DELTAS = (0.25, 0.5, 1.0, 2.0, 4.0, 8.0) + + +def emu_block_meta( + logits: torch.Tensor, + seq_lens: torch.Tensor, + compress_ratio: int = 1, + next_n: int = 1, +) -> torch.Tensor: + """Emulate the indexer-side per-32-block rung-count metadata. + + Record r packs, for each rung offset ``delta_j`` in + ``_META_DELTAS``, ``count(v >= record_max - delta_j)`` over + positions ``[32r, 32r+32)`` as a 5-bit saturating field at bits + ``[5j, 5j+5)`` (31 = "31 or 32"; the kernel decodes 31 as the safe + upper bound 32). Positions beyond the row's effective length are + excluded. Layout matches ``block_max``: ``[num_rows, nrec]`` int32 + with ``nrec = ceil(N/128)*4`` (one record per 32 positions). + On the indexer side this is ~L ballots+popcounts per record in the + epilogue, same shape as the +1.3% block_max emission. + """ + assert next_n == 1, "emu_block_meta: next_n == 1 only" + x = logits.float() + R, N = x.shape + nrec = ((N + _META_BLOCK - 1) // _META_BLOCK) * _META_RECS_PER_BLOCK + npad = nrec * 32 + if npad > N: + x = torch.nn.functional.pad(x, (0, npad - N), value=float("-inf")) + n_eff = seq_lens.long() // compress_ratio + ar = torch.arange(npad, device=x.device)[None, :] + x = x.masked_fill(ar >= n_eff[:, None], float("-inf")) + xb = x.view(R, nrec, 32) + m = xb.amax(2, keepdim=True) + meta = torch.zeros(R, nrec, dtype=torch.int32, device=x.device) + for j, d in enumerate(_META_DELTAS): + cj = (xb >= (m - d)).sum(2).clamp(max=31).to(torch.int32) + meta |= cj << (5 * j) + return meta.contiguous() + + +def enc_ordered_f32(t: torch.Tensor) -> torch.Tensor: + """Order-preserving int encoding of fp32 (an involution). + + Stored back in fp32 slots — matches the indexer's encoded-int atomic + min/max. + """ + bits = t.float().contiguous().view(torch.int32) + enc = torch.where(bits >= 0, bits, bits ^ 0x7FFFFFFF) + return enc.view(torch.float32) + + +def hit_agg_identities(num_rows: int, device) -> torch.Tensor: + """Identity-initialized per-row hit aggregate. + + {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} — the required initial state of + the buffer the indexer atomically merges into. + """ + ident = torch.tensor([_FLT_MAX, -_FLT_MAX], dtype=torch.float32, device=device) + enc = enc_ordered_f32(ident) + out = torch.zeros((num_rows, 4), dtype=torch.float32, device=device) + out[:, 0] = enc[0] + out[:, 1] = enc[1] + return out.contiguous() + + def gvr_topk_decode( logits: torch.Tensor, pre_idx: torch.Tensor, @@ -178,6 +342,7 @@ def gvr_topk_decode( order_row: Optional[torch.Tensor] = None, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, + block_max: Optional[torch.Tensor] = None, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -247,6 +412,17 @@ def gvr_topk_decode( cute_dtype = _DTYPE_TORCH_TO_CUTE[logits.dtype] num_rows = logits.shape[0] + enable_block_skip = block_max is not None + if enable_block_skip: + assert ( + block_max.dtype == torch.float32 + and block_max.is_cuda + and block_max.is_contiguous() + and block_max.dim() == 2 + and block_max.shape[0] == num_rows + and block_max.shape[1] % 4 == 0 + ), "block_max must be contiguous CUDA fp32 [num_rows, nb_pad*4]" + if return_output_values: if out_values is None: out_values = torch.empty((num_rows, top_k), dtype=logits.dtype, device=logits.device) @@ -312,6 +488,7 @@ def gvr_topk_decode( seqlen_sorted, p4_warp_redundant, p2_warp_redundant, + enable_block_skip, ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. @@ -324,6 +501,7 @@ def gvr_topk_decode( out_values if return_output_values else None, out_indices, order_row if seqlen_sorted else None, + block_max if enable_block_skip else None, ) if return_output_values: return out_values, out_indices From 7eb1d888d8fb49717ecbf3c53a890e72247247f6 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:07:37 -0700 Subject: [PATCH 002/117] [None][fix] GVR top-k: make block_max a trailing defaulted launch param The block-skip bounds tensor initially landed as a required positional in __call__, breaking every pre-existing compile path that does not pass it (16457's equivalence tests: 'Missing required argument'). Move it after stream with a None default so legacy callers are untouched; the wrapper passes it positionally last (the TVM-FFI env-stream launch takes no runtime stream arg). Verified standalone: main equivalence family 768 passed / 0 failed, launch_autoconfig 4/4, real-data smoke (flash+pro up to 1M) all green. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py | 2 +- tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 68a264c14323..2393a644fb4b 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -5083,8 +5083,8 @@ def __call__( output_values: cute.Tensor, # or None. output_indices: cute.Tensor, order_row: cute.Tensor, # or None when seqlen_sorted=False - block_max: cute.Tensor, # or None: block-skip disabled stream, + block_max: cute.Tensor = None, # block-skip bounds; None = disabled ): num_rows = input_data.shape[0] cluster_size = cutlass.const_expr(self.cluster_size) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index bd028c07a7ee..f2966563d663 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -161,8 +161,8 @@ def _compile( out_values_fake, out_indices_fake, order_row_fake, - block_max_fake, stream=fake_stream, + block_max=block_max_fake, options="--enable-tvm-ffi", ) From dc42f23914bde76285dbc4f4f489f5829d8f70db Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:21:43 -0700 Subject: [PATCH 003/117] [None][fix] GVR block-skip: support unaligned cluster slice starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The active list previously required 32-aligned slice starts (runtime guard falling back to dense), which silently disabled the skip on every cluster slicing whose N/cs is not a multiple of 32 — including the launch policy's cs=8 picks at the 512k/1024k rungs. The list now covers FULL blocks only (first-full-block ceil in the build); the sub-block head region of an unaligned slice is counted by all threads in a strided scalar pass ordered BEFORE the list walk, and Phase 3's compact write replays the same head-then-list per-thread order, so prefix-sum positions stay exact. Boundary blocks shared with a neighbouring CTA appear only in that CTA's head region — no double count, no gap. Verified: skip arms exact at cs in {1,2,4,8} on flash+pro real cells (64k/512k/1024k, unaligned N=262127 and 131075 slices). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 45 ++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 2393a644fb4b..88638c1d377d 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1447,7 +1447,12 @@ def _full_build_active( # position (determinism contract). ids_per_thread = cutlass.const_expr(self.SKIP_MAX_BLOCKS // self.num_threads) num_threads = cutlass.const_expr(self.num_threads) - blk_lo = slice_start >> cutlass.Int32(self.SKIP_BLOCK_LOG2) + # first FULL block: a block straddling slice_start would map list + # positions outside this CTA's slice; the sub-block head region + # [slice_start, blk_lo*32) is scanned separately by the callers. + blk_lo = (slice_start + cutlass.Int32(self.SKIP_BLOCK - 1)) >> cutlass.Int32( + self.SKIP_BLOCK_LOG2 + ) blk_hi = (slice_end + cutlass.Int32(self.SKIP_BLOCK - 1)) >> cutlass.Int32( self.SKIP_BLOCK_LOG2 ) @@ -1541,16 +1546,27 @@ def block_count_ge_multi( # ---- block-skip compact iteration (lossless vs the dense path) ---- # Build the active list at the LOOSEST threshold over all M columns: # a skipped block bounds every element below min(t_m), so all M - # counts equal their dense values. Slice boundaries must be - # 32-aligned for the list's block<->position mapping to stay inside - # the slice (runtime-uniform guard; misaligned slices fall through - # to the dense path below via skip_ok == 0). + # counts equal their dense values. The list covers FULL blocks only; + # the sub-block head region of an unaligned slice start is counted + # here separately (per-thread order contract: head elements FIRST, + # then list entries — Phase 3's compact write replays the same). skip_ok = cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): - if (slice_start & cutlass.Int32(self.SKIP_BLOCK - 1)) == cutlass.Int32(0): - skip_ok = cutlass.Int32(1) + skip_ok = cutlass.Int32(1) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): if skip_ok == cutlass.Int32(1): + head_end = ( + (slice_start + cutlass.Int32(self.SKIP_BLOCK - 1)) + >> cutlass.Int32(self.SKIP_BLOCK_LOG2) + ) << cutlass.Int32(self.SKIP_BLOCK_LOG2) + if head_end > slice_end: + head_end = slice_end + hh = slice_start + tidx + while hh < head_end: + vh = self._load_fp32(input_row, hh) + for m in cutlass.range_constexpr(M): + cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vh >= thr_frag[m]) + hh = hh + cutlass.Int32(num_threads) tmin = thr_frag[0] for m in cutlass.range_constexpr(M): tmin = _fmin_f32_inline(tmin, thr_frag[m]) @@ -2205,6 +2221,21 @@ def phase3_collect_candidates( skip_wr = cutlass.Int32(1) if cutlass.const_expr(self.enable_block_skip and smem_active is not None): if skip_wr == cutlass.Int32(1): + # head region first — same per-thread order as the count pass + head_end_w = ( + (slice_start + cutlass.Int32(self.SKIP_BLOCK - 1)) + >> cutlass.Int32(self.SKIP_BLOCK_LOG2) + ) << cutlass.Int32(self.SKIP_BLOCK_LOG2) + if head_end_w > slice_end: + head_end_w = slice_end + hh_w = slice_start + tidx + while hh_w < head_end_w: + vh_w = self._load_fp32(input_row, hh_w) + if vh_w >= thr_final and wc < cutlass.Int32(kCC): + smem_keys[wc] = vh_w + smem_vals[wc] = hh_w + wc = wc + cutlass.Int32(1) + hh_w = hh_w + cutlass.Int32(num_threads) chunks_per_block_w = cutlass.const_expr( self.SKIP_BLOCK // (self.vec_bits // self.dtype.width) ) From 36a542f12bbae649268413edda5a384998eda480 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:42:20 -0700 Subject: [PATCH 004/117] [None][perf] GVR block-skip: rung-tightening active-list build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The active list was built at the loosest rung over all M columns. On low-hit-rate rows the lowest sample-quantile rung sits far below the K-th value (real pro 1024k: retained fraction 0.63 at that rung vs 0.08 at the final threshold), so the list barely skipped anything. Build now iterates (cs==1): if the list exceeds CAP = 3/4 kC blocks, DROP that rung — dropping is always correct (the rung is merely an unmeasured probe; its partial counts are excluded from the admission argmin and the fallback bracket seeding via a dropped-rung mask) — and rebuild at the next tighter threshold, bounded by M-1 extra ~2-5us builds. At cs>1 per-CTA list lengths differ (the drop decision would diverge across the cluster), so the plain loosest-rung build stays. Real-data 1024k, cs1, warm-L2 directional: pro 28.8 -> 16.4us (skip ratio 1.16x -> 2.05x), flash 18.5 -> 12.8us (1.73x -> 2.49x). Correctness: cs {1,2,4,8} x flash+pro x {64k,512k,1024k} all exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 111 ++++++++++++++---- 1 file changed, 88 insertions(+), 23 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 88638c1d377d..e8db5ecea200 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1567,24 +1567,71 @@ def block_count_ge_multi( for m in cutlass.range_constexpr(M): cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vh >= thr_frag[m]) hh = hh + cutlass.Int32(num_threads) - tmin = thr_frag[0] - for m in cutlass.range_constexpr(M): - tmin = _fmin_f32_inline(tmin, thr_frag[m]) - # smem_wcnt_multi's first num_warps slots double as the - # build's warp-total scratch: the build's own barriers - # complete before any count lands there. - self._full_build_active( - block_max_row, - slice_start, - slice_end, - tmin, - smem_wcnt_multi, - smem_active, - s_active_cnt, - tidx, - warp_id, - lane, - ) + # Rung-tightening build (cs==1): a rung whose active list + # exceeds CAP blocks is provably or near-provably + # unacceptable (count >= list length; on real low-hit-rate + # rows the loosest sample-quantile rung retains 60%+ of the + # blocks and destroys the skip). DROP it — a dropped rung is + # merely an unmeasured probe (recorded in the mask at + # s_active_cnt[2]; classify and the fallback seeding skip + # it) — and rebuild at the next tighter threshold. Bounded + # by M-1 extra builds (~2-5us each at nb=8192). At cs>1 the + # per-CTA list lengths differ, so the drop decision would + # diverge across the cluster: keep the plain loosest-rung + # build there. + CAP_BLOCKS = cutlass.const_expr(3 * self.kC // 4) + if cutlass.const_expr(cluster_size == 1): + build_done = cutlass.Int32(0) + for _attempt in cutlass.range_constexpr(M): + if build_done == cutlass.Int32(0): + dmask = s_active_cnt[2] + tcur = cutlass.Float32(self.FLT_MAX) + mcur = cutlass.Int32(-1) + kept = cutlass.Int32(0) + for m in cutlass.range_constexpr(M): + if ( + dmask & (cutlass.Int32(1) << cutlass.Int32(m)) + ) == cutlass.Int32(0): + kept = kept + cutlass.Int32(1) + if thr_frag[m] < tcur: + tcur = thr_frag[m] + mcur = cutlass.Int32(m) + self._full_build_active( + block_max_row, + slice_start, + slice_end, + tcur, + smem_wcnt_multi, + smem_active, + s_active_cnt, + tidx, + warp_id, + lane, + ) + if s_active_cnt[0] <= cutlass.Int32( + CAP_BLOCKS + ) or kept <= cutlass.Int32(1): + build_done = cutlass.Int32(1) + else: + if tidx == 0: + s_active_cnt[2] = dmask | (cutlass.Int32(1) << mcur) + cute.arch.barrier() + else: + tmin = thr_frag[0] + for m in cutlass.range_constexpr(M): + tmin = _fmin_f32_inline(tmin, thr_frag[m]) + self._full_build_active( + block_max_row, + slice_start, + slice_end, + tmin, + smem_wcnt_multi, + smem_active, + s_active_cnt, + tidx, + warp_id, + lane, + ) if tidx == 0: s_active_cnt[1] = cutlass.Int32(1) # list-current flag chunks_per_block = cutlass.const_expr( @@ -4203,7 +4250,7 @@ def run_one_row( ) s_active_cnt = smem.allocate_tensor( element_type=cutlass.Int32, - layout=cute.make_ordered_layout((2,), order=(0,)), + layout=cute.make_ordered_layout((4,), order=(0,)), byte_alignment=16, ) else: @@ -4601,6 +4648,7 @@ def _run_phases( if cutlass.const_expr(self.enable_block_skip): if tidx == cutlass.Int32(0): s_active_cnt[1] = cutlass.Int32(0) + s_active_cnt[2] = cutlass.Int32(0) # dropped-rung mask # ---- Phase 1: preIdx Min/Max/Mean ---- self.phase1_preidx_stats( @@ -4721,6 +4769,11 @@ def _run_phases( # (Explicit argmin: with r0_vseed the pmean column is not # sorted into the rung order; for sorted rungs this is # equivalent to the old "last m in window" rule.) + # Dropped rungs (block-skip rung tightening) hold PARTIAL + # counts — never admissible. + dmask_c = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip): + dmask_c = s_active_cnt[2] best_m = cutlass.Int32(-1) best_c = cutlass.Int32(2147483647) for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): @@ -4729,6 +4782,8 @@ def _run_phases( cm >= cutlass.Int32(self.top_k) and cm <= cutlass.Int32(self.kC) and cm < best_c + and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) + == cutlass.Int32(0) ): best_m = cutlass.Int32(m) best_c = cm @@ -4776,16 +4831,26 @@ def _run_phases( bhi = v_hi clo = cutlass.Int32(-1) chi = cutlass.Int32(-1) + dmask_f = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip): + dmask_f = s_active_cnt[2] for m in cutlass.range_constexpr(M): cm = s_mt_cnt[m] tm = s_mt_thr[m] - if cm > cutlass.Int32(self.kC) and ( - clo < cutlass.Int32(0) or tm > blo + m_ok = ( + dmask_f & (cutlass.Int32(1) << cutlass.Int32(m)) + ) == cutlass.Int32(0) + if ( + m_ok + and cm > cutlass.Int32(self.kC) + and (clo < cutlass.Int32(0) or tm > blo) ): blo = tm clo = cm - if cm < cutlass.Int32(self.top_k) and ( - chi < cutlass.Int32(0) or tm < bhi + if ( + m_ok + and cm < cutlass.Int32(self.top_k) + and (chi < cutlass.Int32(0) or tm < bhi) ): bhi = tm chi = cm From 086eabab136dff8715d7c63cf750fbb2202d97a4 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:39:14 -0700 Subject: [PATCH 005/117] [None][fix] GVR block-skip: guard list capacity/id width, bound tail loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes for the compact machinery: - skip_ok now also requires nb_slice <= SKIP_MAX_BLOCKS and absolute block id < 32768 (int16 list entries); wider/higher slices fall back to the dense walk losslessly instead of silently truncating counts and the collect (or wrapping ids negative at cluster_size > 1). - Both compact walks vector-load only fully in-bounds chunks; the slice-end straddle goes through the scalar path, so an unaligned row no longer reads past the row/allocation. - Ctor rejects enable_block_skip without enable_r0 (dead 16KB SMEM). - emu_block_max defaults to records='positional' (the shipped kernel is grain 32; 'rotate' is a grain-128 fold fixture) and the wrapper asserts block_max covers every 32-position record of the row. Validated: capacity boundary (8192/8193/9375 blocks), N=1.05M at cs=4/8, unaligned exact-size rows (N=1000/65535/65529), real-data flash/pro 64k/1024k — all exact with planted tail winners. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 70 +++++++++++++------ .../cute_dsl_kernels/top_k/run_gvr_topk.py | 5 +- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index e8db5ecea200..005ff1b82b54 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -443,6 +443,11 @@ def __init__( self.skip_order = "grouped" if enable_block_skip and num_threads not in (512, 1024): raise ValueError("enable_block_skip requires num_threads in {512, 1024}") + if enable_block_skip and not enable_r0: + # The compact machinery hangs off the R0 count pass and the + # phase-3 stream-write; without R0 the 16KB list SMEM would be + # allocated but the skip could never engage. + raise ValueError("enable_block_skip requires enable_r0") # C7 dispatch (op#26 host policy folded into the ctor; all gated on # enable_r0 so an OFF kernel is byte-identical to the base): # - qfracs default = M2D (0.85, 0.35): dispatch_r0_op26 ships M2D for @@ -1553,6 +1558,22 @@ def block_count_ge_multi( skip_ok = cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): skip_ok = cutlass.Int32(1) + # Capacity/id-width guard: the active list holds at most + # SKIP_MAX_BLOCKS local ids and _list_st stores ABSOLUTE block + # ids as int16. A slice over 8192 full blocks (N_local > + # 262144) or reaching absolute id >= 32768 falls back to the + # dense walk (lossless; the list-current flag is never set, so + # phase3 stays dense too). + blk_lo_g = (slice_start + cutlass.Int32(self.SKIP_BLOCK - 1)) >> cutlass.Int32( + self.SKIP_BLOCK_LOG2 + ) + blk_hi_g = (slice_end + cutlass.Int32(self.SKIP_BLOCK - 1)) >> cutlass.Int32( + self.SKIP_BLOCK_LOG2 + ) + if blk_hi_g - blk_lo_g > cutlass.Int32(self.SKIP_MAX_BLOCKS): + skip_ok = cutlass.Int32(0) + if blk_hi_g > cutlass.Int32(32767): + skip_ok = cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): if skip_ok == cutlass.Int32(1): head_end = ( @@ -1658,17 +1679,22 @@ def block_count_ge_multi( pos0 = blk * cutlass.Int32(self.SKIP_BLOCK) + my_chunk0 * cutlass.Int32( vec_w ) - src_ptr_u = cute.make_ptr( - self.dtype, - row_addr + cutlass.Int64(pos0) * cutlass.Int64(elem_bytes), - cute.AddressSpace.gmem, - assumed_align=vec_align, - ) - cute.copy( - copy_atom, - cute.make_tensor(src_ptr_u, cute.make_layout((vec_w,))), - frags[u], - ) + # Vector-load only fully in-bounds chunks; the + # slice-end straddle re-reads scalars below (a + # tail block's chunks would otherwise read past + # the row/allocation when N % 32 != 0). + if pos0 + cutlass.Int32(vec_w) <= slice_end: + src_ptr_u = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(pos0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + copy_atom, + cute.make_tensor(src_ptr_u, cute.make_layout((vec_w,))), + frags[u], + ) poss.append(pos0) valids.append(valid) for u in cutlass.range_constexpr(UN): @@ -2297,18 +2323,18 @@ def phase3_collect_candidates( pos0_w = blk_w * cutlass.Int32(self.SKIP_BLOCK) + my_chunk0_w * cutlass.Int32( vec_w ) - src_ptr_w = cute.make_ptr( - self.dtype, - row_addr + cutlass.Int64(pos0_w) * cutlass.Int64(elem_bytes), - cute.AddressSpace.gmem, - assumed_align=vec_align, - ) - cute.copy( - copy_atom, - cute.make_tensor(src_ptr_w, cute.make_layout((vec_w,))), - wfrag, - ) if pos0_w + cutlass.Int32(vec_w) <= slice_end: + src_ptr_w = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(pos0_w) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + copy_atom, + cute.make_tensor(src_ptr_w, cute.make_layout((vec_w,))), + wfrag, + ) for j in cutlass.range_constexpr(vec_w): if cutlass.const_expr(self.dtype == cutlass.Float32): vj = wfrag[j] diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index f2966563d663..9d908b3c7367 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -192,7 +192,7 @@ def emu_block_max( next_n: int = 1, compress_ratio: int = 1, tail_mode: str = "pad_inf", - records: str = "rotate", + records: str = "positional", ) -> torch.Tensor: """``[num_rows, nb_pad*4] fp32`` warp-partial upper-bound records. @@ -421,7 +421,8 @@ def gvr_topk_decode( and block_max.dim() == 2 and block_max.shape[0] == num_rows and block_max.shape[1] % 4 == 0 - ), "block_max must be contiguous CUDA fp32 [num_rows, nb_pad*4]" + and block_max.shape[1] >= (logits.shape[1] + 31) // 32 + ), "block_max must be contiguous CUDA fp32 [num_rows, nb_pad*4] covering the row" if return_output_values: if out_values is None: From 01bc507f51d265d8531bcaf3eedd93f9ca3dd9d4 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:06:03 -0700 Subject: [PATCH 006/117] [None][perf] GVR block-skip: host dispatch gate below N=200k The compact walk only wins on long rows (cold-L2 protocol: >= 2.18x at N=262k, 4-14% loss at N <= 131k). Gate block_max shape-based (no device sync) behind skip_min_n=200_000: below it the wrapper drops to the dense arms. Protocol after gating: flash/pro 256k/512k cells all 0.99-1.02x, 1024k wins intact (flash BS1 15.63us 2.18x, BS1024 4.18x; pro BS1024 3.15x). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 9d908b3c7367..cbc58b6b2d4f 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -343,6 +343,7 @@ def gvr_topk_decode( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, block_max: Optional[torch.Tensor] = None, + skip_min_n: Optional[int] = 200_000, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -412,6 +413,13 @@ def gvr_topk_decode( cute_dtype = _DTYPE_TORCH_TO_CUTE[logits.dtype] num_rows = logits.shape[0] + # Host dispatch gate: the compact walk only wins when the per-row + # compressed length is large (protocol: >= 2x at N=262k, 4-14% LOSS at + # N <= 131k). Below skip_min_n (compressed-index space, shape-based so + # no device sync) drop block_max and run the dense arms. None disables + # the gate (A/B probes). + if block_max is not None and skip_min_n is not None and logits.shape[1] < skip_min_n: + block_max = None enable_block_skip = block_max is not None if enable_block_skip: assert ( From e9f4865e281d1a0fe49f5b3d0b5d2956fa2fb098 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:42:41 -0700 Subject: [PATCH 007/117] [None][perf] GVR block-skip: skip-aware launch policy and dispatch gates pick_config gains has_block_max: with bounds available and N >= 200k the policy pins cluster_size = 1 - the compact list + rung tightening (cs1-only) beat the row-split configs outright once the bounds prune the scan (cold protocol, real data: BS1 1.21x, BS64 2.12x, BS1024 4.16x vs the stock picks; splitting shrinks each CTA slice below the skip break-even and disables tightening). The wrapper dispatch gains a second gate next to skip_min_n: K > 512 at num_rows < 8 keeps the stock path - the acceptance band is proportionally tighter (kC/K = 6 vs 10), the bounds prune less, and the row-split configs win (pro 262k BS1: skip 21.3-21.6us at cs1/cs8 vs stock cs8 19.7us). Cold protocol vs op26 at its own launch policy, 24 cells: zero regressions; flash 1024k 1.21/2.12/4.16x (BS 1/64/1024), pro 1024k 1.68/3.15x (BS 64/1024), everything below the gates identical to stock. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 11 +++++++++++ tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 005ff1b82b54..795f70a81148 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -5272,6 +5272,7 @@ def pick_config( num_candidates: int, max_seq_len: Optional[int] = None, num_sms: Optional[int] = None, + has_block_max: bool = False, ) -> dict: """Pick the launch-shape ctor kwargs for ``(dtype, BS, N)``. @@ -5306,6 +5307,16 @@ def pick_config( # large N -> 8; single-wave -> 4/2; multi-wave -> 1. if n_row < 65536: cluster_size = 1 + elif has_block_max and n_row >= 200_000: + # Block-skip sweet spot is cs == 1 with a large per-CTA slice: + # the compact list + rung tightening (cs1-only) beat the + # row-split configs outright once the bounds prune the scan + # (cold protocol, real data: BS1 1.18x, BS64 2.14x, BS1024 + # 4.16x vs this policy's stock picks; splitting shrinks each + # CTA's slice below the skip break-even and disables + # tightening). Below 200k the wrapper drops block_max anyway + # (skip_min_n gate) and the stock picks apply. + cluster_size = 1 elif num_rows <= 4 and n_row >= 131072: cluster_size = 8 elif num_rows * 4 <= num_sms: diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index cbc58b6b2d4f..8bb6ef90f49b 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -420,6 +420,12 @@ def gvr_topk_decode( # the gate (A/B probes). if block_max is not None and skip_min_n is not None and logits.shape[1] < skip_min_n: block_max = None + # K > 512 at tiny batch: the acceptance band is proportionally tighter + # (kC/K = 6 vs 10), the bounds prune less, and the row-split configs + # win outright (cold protocol, pro 262k BS1: skip 21.3-21.6us at + # cs1/cs8 vs stock cs8 19.7us) -> keep the stock path. + if block_max is not None and num_rows < 8 and top_k > 512: + block_max = None enable_block_skip = block_max is not None if enable_block_skip: assert ( From 344b082827a1ba64fe6434150ba71278388fd41d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:47:29 -0700 Subject: [PATCH 008/117] [None][feat] GVR: emu references for epilogue seed counts and candidates emu_seed_counts / emu_cand implement the A-side products per the epilogue<->topk buffer contract v2 so the consumer waterfall can be developed and tested against torch references before the fused indexer lands. Real-data coverage probe: 7/8 cells have a seed count inside [K, kC]; prev-kth drifts too loose at long context, so the L2 collect threshold should be the middle rung / xstate-adaptive. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 8bb6ef90f49b..d150bdaad511 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -293,6 +293,77 @@ def emu_block_meta( return meta.contiguous() +def emu_seed_counts( + logits: torch.Tensor, + seq_lens: torch.Tensor, + seed_thr: torch.Tensor, + next_n: int = 1, + compress_ratio: int = 1, +) -> torch.Tensor: + """L1 emu: exact per-row threshold counts. + + counts[r][j] = |{i < N_eff(r) : logits[r, i] >= t_j}| on the + post-conversion values (contract: epilogue_topk_interface.md). + """ + R, C = logits.shape + n_eff = _row_n_eff(seq_lens, R, next_n, compress_ratio).unsqueeze(1) + pos = torch.arange(C, device=logits.device).unsqueeze(0) + lf = logits.to(torch.float32) + valid = pos < n_eff + counts = torch.empty((R, seed_thr.shape[1]), dtype=torch.int32, device=logits.device) + for j in range(seed_thr.shape[1]): + counts[:, j] = ((lf >= seed_thr[:, j : j + 1]) & valid).sum(-1, dtype=torch.int32) + return counts + + +def emu_cand( + logits: torch.Tensor, + seq_lens: torch.Tensor, + seed_thr: torch.Tensor, + cap: int, + next_n: int = 1, + compress_ratio: int = 1, + sentinel_pad: int = 0, +) -> tuple[torch.Tensor, torch.Tensor]: + """L2 emu: unordered candidate pre-collect. + + Unordered (value fp32-bits, index) pairs of all valid positions + >= t_0 = seed_thr[:, 0]; ctl = {claimed, void}. claimed may + over-approximate the true count (window sentinels, idx word = -1) — + ``sentinel_pad`` injects that legally. void=1 when claimed > cap; on + overflow only the first ``cap`` entries are materialized (contract v2: + consumers scan [0, min(claimed, cap)) skipping sentinels). + """ + R, C = logits.shape + dev = logits.device + n_eff = _row_n_eff(seq_lens, R, next_n, compress_ratio) + lf = logits.to(torch.float32) + cand = torch.full((R, cap * 2), -1, dtype=torch.int32, device=dev) + ctl = torch.zeros((R, 2), dtype=torch.int32, device=dev) + pairs = cand.view(R, cap, 2) + for r in range(R): + ne = int(n_eff[r]) + hits = torch.nonzero(lf[r, :ne] >= seed_thr[r, 0], as_tuple=False).flatten() + cnt = hits.numel() + # unordered contract: shuffle, then interleave sentinels + perm = hits[torch.randperm(cnt, device=dev)] + ent = torch.full((cnt + sentinel_pad,), -1, dtype=torch.int64, device=dev) + if sentinel_pad: + slots = torch.randperm(cnt + sentinel_pad, device=dev)[:cnt] + slots = slots.sort().values + else: + slots = torch.arange(cnt, device=dev) + ent[slots] = perm + claimed = int(ent.numel()) + nwr = min(claimed, cap) + live = ent[:nwr] >= 0 + pairs[r, :nwr, 1] = ent[:nwr].to(torch.int32) + pairs[r, :nwr, 0][live] = lf[r, ent[:nwr][live]].view(torch.int32) + ctl[r, 0] = claimed + ctl[r, 1] = 1 if claimed > cap else 0 + return cand, ctl + + def enc_ordered_f32(t: torch.Tensor) -> torch.Tensor: """Order-preserving int encoding of fp32 (an involution). From 289553f671d1aa1276a754ff08265ff25939e7e3 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:01:45 -0700 Subject: [PATCH 009/117] [None][feat] GVR: waterfall L1 admission from external epilogue counts use_ext_counts: rung thresholds AND their exact counts arrive from the indexer epilogue (seed_thr/seed_counts [rows, 3], interface v2), so P1b and the M-ary R0 count pass are skipped. The seeded refine routes both cases: an in-band rung is re-measured once (building the per-thread hand-off Phase 3 requires) and accepted; a full miss seeds log-falsi with the external brackets. cs==1, requires fb_fix and a 3-slot rung config (the wrapper pins 2 qfracs + vseed; the values are irrelevant since P1b never runs). Real-data validation (flash/pro x 16k..1024k, thresholds {prev-kth, q35, q85} + emu counts): 8/8 exact on stock/ext/ext+skip arms incl. the pro-1M full-miss bracket cell. Directional: +9-10% at 1M (P1b + M-count saved), small-N slightly negative (the waterfall routes those to the L2 direct path instead). Next: skip P1 under ext (outer brackets from xstate) and the L2 direct-to-P4 branch. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 221 ++++++++++++------ .../cute_dsl_kernels/top_k/run_gvr_topk.py | 41 ++++ 2 files changed, 189 insertions(+), 73 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 795f70a81148..c13d6b34d4f2 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -271,6 +271,7 @@ def __init__( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, enable_block_skip: bool = False, + use_ext_counts: bool = False, ): # Redundant-warp sync reduction: every warp replays the block # reduce + decision from the same staged SMEM partials in the @@ -539,6 +540,19 @@ def __init__( self.M_thr = self.M_qf + 1 # need[m] = ceil(q_m * K) prev-topK values >= rung m. self.qneeds = tuple(max(1, int(math.ceil(q * self.top_k))) for q in self.r0_qfracs) + # use_ext_counts (waterfall L1 admission): thresholds AND their + # exact counts arrive from the indexer epilogue (interface v2) — + # P1b and the M-ary count pass are skipped; an in-band rung is + # re-measured ONCE through the seeded refine (per-thread hand-off) + # and accepted; a miss seeds log-falsi with the external brackets. + self.use_ext_counts = bool(use_ext_counts) and bool(enable_r0) + if self.use_ext_counts: + if not self.fb_fix: + raise ValueError("use_ext_counts requires fb_fix") + if self.M_thr != 3: + raise ValueError("use_ext_counts expects exactly 3 seed rungs") + if cluster_size != 1: + raise ValueError("use_ext_counts is single-CTA (cs==1) only") # R1 inline shot aim in log2-count space: geometric center of the # [K, kC] acceptance window. self.log2_r1aim = math.log2(math.sqrt(self.top_k * self.kC)) if self.r0_qfracs else 0.0 @@ -4103,6 +4117,8 @@ def gvr_topk_kernel( output_indices: cute.Tensor, # [numRows, top_k] int32 order_row: cute.Tensor, # [batch_size] int32 (or None when seqlen_sorted=False) block_max: cute.Tensor, # [numRows, nb_pad*4] fp32 (or None: no block-skip) + seed_thr: cute.Tensor, # [numRows, 3] fp32 (or None: no ext counts) + seed_counts: cute.Tensor, # [numRows, 3] int32 (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -4155,6 +4171,8 @@ def gvr_topk_kernel( output_values, output_indices, block_max=block_max, + seed_thr=seed_thr, + seed_counts=seed_counts, ) @cute.jit @@ -4167,6 +4185,8 @@ def run_one_row( output_values: cute.Tensor, # [numRows, top_k] dtype, optional output_indices: cute.Tensor, # [numRows, top_k] int32 block_max: cute.Tensor = None, # [numRows, nb_pad*4] fp32 + seed_thr: cute.Tensor = None, # [numRows, 3] fp32 (ext counts) + seed_counts: cute.Tensor = None, # [numRows, 3] int32 (ext counts) ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -4227,6 +4247,14 @@ def run_one_row( block_max_row = block_max[row_idx, None] else: block_max_row = None + if cutlass.const_expr( + self.use_ext_counts and seed_thr is not None and seed_counts is not None + ): + seed_thr_row = seed_thr[row_idx, None] + seed_counts_row = seed_counts[row_idx, None] + else: + seed_thr_row = None + seed_counts_row = None # When return_output_values=False, ``output_values`` is None at # launch and the gated writes below are compiled out; slicing into # None would crash so we keep the view None as well. @@ -4523,6 +4551,8 @@ def run_one_row( warp_id, lane, block_max_row=block_max_row, + seed_thr_row=seed_thr_row, + seed_counts_row=seed_counts_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4567,6 +4597,8 @@ def run_one_row( warp_id, lane, block_max_row=block_max_row, + seed_thr_row=seed_thr_row, + seed_counts_row=seed_counts_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4608,6 +4640,8 @@ def run_one_row( warp_id, lane, block_max_row=block_max_row, + seed_thr_row=seed_thr_row, + seed_counts_row=seed_counts_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4652,6 +4686,8 @@ def _run_phases( warp_id, lane, block_max_row=None, # block-skip: this row's per-32-position bounds + seed_thr_row=None, # ext counts: this row's 3 seed thresholds (fp32) + seed_counts_row=None, # ext counts: this row's 3 exact counts (int32) smem_active=None, s_active_cnt=None, ): @@ -4751,85 +4787,107 @@ def _run_phases( # CTA scans its slice and block_count_ge_multi cluster-merges # the rung counts (phase1b rungs are per-CTA identical since # preIdx stats are full-row). - if cutlass.const_expr(self.p1b_cache): - # rungs from the SMEM gather-cache P1 stashed (no 2nd - # GMEM gather); 16-bit only. - self.phase1b_hspace_rungs_cached( - pre_idx_count, smem_gath, smem_hist, s_thr, s_mt_thr, tidx, warp_id, lane - ) - else: - self.phase1b_hspace_rungs( + if cutlass.const_expr(self.use_ext_counts): + # ---- Waterfall L1 admission (ext counts) ---- + # Rung thresholds AND exact counts arrive from the + # indexer epilogue: skip P1b + the M-ary count pass. + # Route through the seeded refine (s_r0col = -1): an + # in-band rung is re-measured once (builds the + # per-thread hand-off Phase 3 requires) and accepted; + # a full miss seeds log-falsi from the ext brackets. + if tidx == cutlass.Int32(0): + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + s_mt_thr[m] = seed_thr_row[m] + s_mt_cnt[m] = cutlass.Int32(seed_counts_row[m]) + s_r0col[0] = cutlass.Int32(-1) + if cutlass.const_expr(not self.use_ext_counts): + if cutlass.const_expr(self.p1b_cache): + # rungs from the SMEM gather-cache P1 stashed (no 2nd + # GMEM gather); 16-bit only. + self.phase1b_hspace_rungs_cached( + pre_idx_count, + smem_gath, + smem_hist, + s_thr, + s_mt_thr, + tidx, + warp_id, + lane, + ) + else: + self.phase1b_hspace_rungs( + input_row, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_hist, + s_thr, + s_mt_thr, + tidx, + warp_id, + lane, + ) + self.block_count_ge_multi( input_row, - N, - pre_idx_row, - pre_idx_count, - pre_idx_offset, - smem_hist, - s_thr, + slice_start, + slice_end, s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, tidx, warp_id, lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) - cute.arch.barrier() - if tidx == 0: - # tightest admissible rung = SMALLEST count in [K, kC]. - # (Explicit argmin: with r0_vseed the pmean column is not - # sorted into the rung order; for sorted rungs this is - # equivalent to the old "last m in window" rule.) - # Dropped rungs (block-skip rung tightening) hold PARTIAL - # counts — never admissible. - dmask_c = cutlass.Int32(0) - if cutlass.const_expr(self.enable_block_skip): - dmask_c = s_active_cnt[2] - best_m = cutlass.Int32(-1) - best_c = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cm = s_mt_cnt[m] - if ( - cm >= cutlass.Int32(self.top_k) - and cm <= cutlass.Int32(self.kC) - and cm < best_c - and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) - == cutlass.Int32(0) - ): - best_m = cutlass.Int32(m) - best_c = cm - s_r0col[0] = best_m - if best_m >= cutlass.Int32(0): - s_thr[0] = s_mt_thr[best_m] - s_iscalars[0] = s_mt_cnt[best_m] - # done=1: the threshold is admitted, so Phase 3 must - # SKIP its retry-shrink and honor s_thr[0]. (block_count - # _ge / secant leave done via their own path; the R0 - # admission must set it explicitly or Phase 3 re-searches - # and the cluster collect diverges -> wrong output.) - s_iscalars[1] = cutlass.Int32(1) - # Snapshot this CTA's LOCAL slice count for the chosen - # rung into s_iscalars[5] — the per-CTA cand_count that - # Phase 3/4's cluster gather consumes (block_count_ge - # sets it too; the R0 admission must match). Without it - # the cluster collect under-counts -> wrong output. - if cutlass.const_expr(cluster_size > 1): - s_iscalars[5] = s_cluster_partial_m[best_m] + cute.arch.barrier() + if tidx == 0: + # tightest admissible rung = SMALLEST count in [K, kC]. + # (Explicit argmin: with r0_vseed the pmean column is not + # sorted into the rung order; for sorted rungs this is + # equivalent to the old "last m in window" rule.) + # Dropped rungs (block-skip rung tightening) hold PARTIAL + # counts — never admissible. + dmask_c = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip): + dmask_c = s_active_cnt[2] + best_m = cutlass.Int32(-1) + best_c = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cm = s_mt_cnt[m] + if ( + cm >= cutlass.Int32(self.top_k) + and cm <= cutlass.Int32(self.kC) + and cm < best_c + and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) + == cutlass.Int32(0) + ): + best_m = cutlass.Int32(m) + best_c = cm + s_r0col[0] = best_m + if best_m >= cutlass.Int32(0): + s_thr[0] = s_mt_thr[best_m] + s_iscalars[0] = s_mt_cnt[best_m] + # done=1: the threshold is admitted, so Phase 3 must + # SKIP its retry-shrink and honor s_thr[0]. (block_count + # _ge / secant leave done via their own path; the R0 + # admission must set it explicitly or Phase 3 re-searches + # and the cluster collect diverges -> wrong output.) + s_iscalars[1] = cutlass.Int32(1) + # Snapshot this CTA's LOCAL slice count for the chosen + # rung into s_iscalars[5] — the per-CTA cand_count that + # Phase 3/4's cluster gather consumes (block_count_ge + # sets it too; the R0 admission must match). Without it + # the cluster collect under-counts -> wrong output. + if cutlass.const_expr(cluster_size > 1): + s_iscalars[5] = s_cluster_partial_m[best_m] + cute.arch.barrier() bc = s_r0col[0] if bc >= cutlass.Int32(0) and bc < cutlass.Int32(self.M_qf): @@ -4902,6 +4960,19 @@ def _run_phases( cand = bhi elif clo < cutlass.Int32(0): cand = blo + if cutlass.const_expr(self.use_ext_counts): + # tightest ext rung already in [K, kC]: + # measure exactly it (accepts in one pass). + cbe = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(M): + cm4 = s_mt_cnt[m] + if ( + cm4 >= cutlass.Int32(self.top_k) + and cm4 <= cutlass.Int32(self.kC) + and cm4 < cbe + ): + cbe = cm4 + cand = s_mt_thr[m] s_thr[0] = cand cute.arch.barrier() rs = cutlass.Int32(0) @@ -5207,6 +5278,8 @@ def __call__( order_row: cute.Tensor, # or None when seqlen_sorted=False stream, block_max: cute.Tensor = None, # block-skip bounds; None = disabled + seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (ext counts) + seed_counts: cute.Tensor = None, # [num_rows, 3] int32 (ext counts) ): num_rows = input_data.shape[0] cluster_size = cutlass.const_expr(self.cluster_size) @@ -5229,6 +5302,8 @@ def __call__( output_indices, order_row, block_max, + seed_thr, + seed_counts, ).launch( grid=(total_ctas, 1, 1), block=(self.num_threads, 1, 1), diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index d150bdaad511..d3a8ab64970f 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -65,6 +65,7 @@ def _compile( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, enable_block_skip: bool = False, + use_ext_counts: bool = False, ): """JIT-compile the GVR kernel for a specific knob combination. @@ -133,6 +134,20 @@ def _compile( if enable_block_skip else None ) + seed_thr_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (n_rows, 3), stride_order=(1, 0), assumed_align=4 + ) + if use_ext_counts + else None + ) + seed_counts_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, 3), stride_order=(1, 0), assumed_align=4 + ) + if use_ext_counts + else None + ) fake_stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True) kernel = GvrTopKKernel( dtype=cute_dtype, @@ -152,6 +167,11 @@ def _compile( p4_warp_redundant=p4_warp_redundant, p2_warp_redundant=p2_warp_redundant, enable_block_skip=enable_block_skip, + use_ext_counts=use_ext_counts, + # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The + # qfrac VALUES are irrelevant on this path (P1b is skipped) — only + # the slot count matters. + r0_qfracs=(0.85, 0.35) if use_ext_counts else None, ) return cute.compile( kernel, @@ -163,6 +183,8 @@ def _compile( order_row_fake, stream=fake_stream, block_max=block_max_fake, + seed_thr=seed_thr_fake, + seed_counts=seed_counts_fake, options="--enable-tvm-ffi", ) @@ -415,6 +437,8 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, + seed_thr: Optional[torch.Tensor] = None, + seed_counts: Optional[torch.Tensor] = None, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -497,6 +521,20 @@ def gvr_topk_decode( # cs1/cs8 vs stock cs8 19.7us) -> keep the stock path. if block_max is not None and num_rows < 8 and top_k > 512: block_max = None + use_ext_counts = seed_thr is not None and seed_counts is not None + if use_ext_counts: + assert ( + seed_thr.dtype == torch.float32 + and seed_thr.is_cuda + and seed_thr.is_contiguous() + and seed_thr.shape == (num_rows, 3) + ), "seed_thr must be contiguous CUDA fp32 [num_rows, 3]" + assert ( + seed_counts.dtype == torch.int32 + and seed_counts.is_cuda + and seed_counts.is_contiguous() + and seed_counts.shape == (num_rows, 3) + ), "seed_counts must be contiguous CUDA int32 [num_rows, 3]" enable_block_skip = block_max is not None if enable_block_skip: assert ( @@ -575,6 +613,7 @@ def gvr_topk_decode( p4_warp_redundant, p2_warp_redundant, enable_block_skip, + use_ext_counts, ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. @@ -588,6 +627,8 @@ def gvr_topk_decode( out_indices, order_row if seqlen_sorted else None, block_max if enable_block_skip else None, + seed_thr if use_ext_counts else None, + seed_counts if use_ext_counts else None, ) if return_output_values: return out_values, out_indices From 1fe323809930168620059adc673e8ec8d258210d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:13:04 -0700 Subject: [PATCH 010/117] [None][perf] GVR ext counts: skip Phase 1 (preIdx gather) entirely With external epilogue counts the only surviving P1 products are the [v_lo, v_hi] outer bracket and the scalar-state init; the ext rungs provide the bracket directly (host contract: t_0 < t_2, finite, all rows valid) and tid0 initializes the scalars. A miss whose target falls outside [t_0, t_2] recovers through the refine loop's 8x bracket expansion, same as the stock fail-soft. Real data 8/8 exact unchanged; directional gains vs stock R0 improve to 1.15x at flash 256k / 1.14x at flash 1M (from 1.04x/1.09x with P1 still running). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index c13d6b34d4f2..4d9b2cc1fd88 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4713,24 +4713,42 @@ def _run_phases( s_active_cnt[2] = cutlass.Int32(0) # dropped-rung mask # ---- Phase 1: preIdx Min/Max/Mean ---- - self.phase1_preidx_stats( - input_row, - N, - pre_idx_row, - pre_idx_count, - pre_idx_offset, - smem_wmin, - smem_wmax, - smem_wsum, - smem_wcnt_p1, - s_thr, - s_iscalars, - tidx, - warp_id, - lane, - smem_gath=smem_gath, # p1b_cache: stash gathered values (None-op OFF) - s_mt_thr=s_mt_thr, # r0_vseed: park pmean in the last rung column - ) + # ext counts: P1's only surviving products are the [v_lo, v_hi] + # outer bracket and the scalar state init — the ext rungs provide + # the bracket directly (host contract: t_0 < t_2, finite, all rows + # valid), so the preIdx gather is skipped wholesale. A miss whose + # target lies outside [t_0, t_2] recovers via the refine loop's + # 8x bracket expansion (same fail-soft as the stock path). + if cutlass.const_expr(self.use_ext_counts): + if tidx == cutlass.Int32(0): + s_thr[0] = seed_thr_row[1] + s_thr[1] = seed_thr_row[0] + s_thr[2] = seed_thr_row[2] + s_iscalars[0] = cutlass.Int32(0) # cand_count + s_iscalars[1] = cutlass.Int32(0) # done + s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb seeding owns) + s_iscalars[3] = cutlass.Int32(-1) # cnt_hi + s_iscalars[4] = cutlass.Int32(0) # out_count + cute.arch.barrier() + if cutlass.const_expr(not self.use_ext_counts): + self.phase1_preidx_stats( + input_row, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_wmin, + smem_wmax, + smem_wsum, + smem_wcnt_p1, + s_thr, + s_iscalars, + tidx, + warp_id, + lane, + smem_gath=smem_gath, # p1b_cache: stash gathered values (None-op OFF) + s_mt_thr=s_mt_thr, # r0_vseed: park pmean in the last rung column + ) # Degenerate threshold init: val_hi <= -self.FLT_MAX or val_lo >= val_hi. # When preIdx values produce an unusable bracket (e.g. all -inf or From 44e9671ed13837e299e8f8264b405bccafca6094 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:38:45 -0700 Subject: [PATCH 011/117] [None][perf] GVR ext counts v2: compose with the stock count/skip path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1 routed external-count admission through the dense seeded refine and forfeited the compact-walk win (flash 1M ext 34.7us vs skipR0 15.6us cold). v2 only skips P1b: the stock M-ary pass runs on the ext rungs, so list build, rung tightening, per-thread hand-off and classify compose unchanged. When an ext count is already in [K, kC] the admitted threshold is parked in ALL rung slots (v2b) — the M-ary pass degenerates to one compact single-threshold count and classify admits it; a miss keeps the distinct rungs as measured brackets. Real data 8/8 exact (stock/ext/ext+skip). Cold protocol: flash 64k 1.21x/1.25x (BS1/BS1024, the slim-admission cell); flash 1M ext+skip 16.1us vs skipR0 15.6us (composition recovered); pro-1M miss rows still pay multi-count+refine (0.7x) — routing sends those to stock skipR0 via xstate feedback (next step). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 171 +++++++++--------- 1 file changed, 89 insertions(+), 82 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 4d9b2cc1fd88..a41de7cb6ef8 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4806,18 +4806,38 @@ def _run_phases( # the rung counts (phase1b rungs are per-CTA identical since # preIdx stats are full-row). if cutlass.const_expr(self.use_ext_counts): - # ---- Waterfall L1 admission (ext counts) ---- - # Rung thresholds AND exact counts arrive from the - # indexer epilogue: skip P1b + the M-ary count pass. - # Route through the seeded refine (s_r0col = -1): an - # in-band rung is re-measured once (builds the - # per-thread hand-off Phase 3 requires) and accepted; - # a full miss seeds log-falsi from the ext brackets. + # ---- Waterfall L1 admission (ext rungs, v2a) ---- + # Rung thresholds arrive from the indexer epilogue: + # ONLY P1b is skipped. The stock M-ary count pass runs + # on the ext rungs so the block-skip list build, rung + # tightening, per-thread hand-off and classify all + # compose unchanged (v1 routed through the dense + # refine and forfeited the compact-walk win: flash 1M + # ext 34.7us vs skipR0 15.6us cold). + # v2b: when an ext count is already in [K, kC], park + # THE ADMITTED THRESHOLD IN ALL RUNG SLOTS — the M-ary + # pass degenerates to one compact single-threshold + # count (+ list build at that threshold) and classify + # admits it; a full miss keeps the 3 distinct rungs as + # measured brackets for the seeded refine. if tidx == cutlass.Int32(0): + bx_m = cutlass.Int32(-1) + bx_c = cutlass.Int32(2147483647) for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - s_mt_thr[m] = seed_thr_row[m] - s_mt_cnt[m] = cutlass.Int32(seed_counts_row[m]) - s_r0col[0] = cutlass.Int32(-1) + cx = cutlass.Int32(seed_counts_row[m]) + if ( + cx >= cutlass.Int32(self.top_k) + and cx <= cutlass.Int32(self.kC) + and cx < bx_c + ): + bx_m = cutlass.Int32(m) + bx_c = cx + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + if bx_m >= cutlass.Int32(0): + s_mt_thr[m] = seed_thr_row[bx_m] + else: + s_mt_thr[m] = seed_thr_row[m] + cute.arch.barrier() if cutlass.const_expr(not self.use_ext_counts): if cutlass.const_expr(self.p1b_cache): # rungs from the SMEM gather-cache P1 stashed (no 2nd @@ -4846,65 +4866,65 @@ def _run_phases( warp_id, lane, ) - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) - cute.arch.barrier() - if tidx == 0: - # tightest admissible rung = SMALLEST count in [K, kC]. - # (Explicit argmin: with r0_vseed the pmean column is not - # sorted into the rung order; for sorted rungs this is - # equivalent to the old "last m in window" rule.) - # Dropped rungs (block-skip rung tightening) hold PARTIAL - # counts — never admissible. - dmask_c = cutlass.Int32(0) - if cutlass.const_expr(self.enable_block_skip): - dmask_c = s_active_cnt[2] - best_m = cutlass.Int32(-1) - best_c = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cm = s_mt_cnt[m] - if ( - cm >= cutlass.Int32(self.top_k) - and cm <= cutlass.Int32(self.kC) - and cm < best_c - and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) - == cutlass.Int32(0) - ): - best_m = cutlass.Int32(m) - best_c = cm - s_r0col[0] = best_m - if best_m >= cutlass.Int32(0): - s_thr[0] = s_mt_thr[best_m] - s_iscalars[0] = s_mt_cnt[best_m] - # done=1: the threshold is admitted, so Phase 3 must - # SKIP its retry-shrink and honor s_thr[0]. (block_count - # _ge / secant leave done via their own path; the R0 - # admission must set it explicitly or Phase 3 re-searches - # and the cluster collect diverges -> wrong output.) - s_iscalars[1] = cutlass.Int32(1) - # Snapshot this CTA's LOCAL slice count for the chosen - # rung into s_iscalars[5] — the per-CTA cand_count that - # Phase 3/4's cluster gather consumes (block_count_ge - # sets it too; the R0 admission must match). Without it - # the cluster collect under-counts -> wrong output. - if cutlass.const_expr(cluster_size > 1): - s_iscalars[5] = s_cluster_partial_m[best_m] + self.block_count_ge_multi( + input_row, + slice_start, + slice_end, + s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, + tidx, + warp_id, + lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, + ) + cute.arch.barrier() + if tidx == 0: + # tightest admissible rung = SMALLEST count in [K, kC]. + # (Explicit argmin: with r0_vseed the pmean column is not + # sorted into the rung order; for sorted rungs this is + # equivalent to the old "last m in window" rule.) + # Dropped rungs (block-skip rung tightening) hold PARTIAL + # counts — never admissible. + dmask_c = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip): + dmask_c = s_active_cnt[2] + best_m = cutlass.Int32(-1) + best_c = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cm = s_mt_cnt[m] + if ( + cm >= cutlass.Int32(self.top_k) + and cm <= cutlass.Int32(self.kC) + and cm < best_c + and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) + == cutlass.Int32(0) + ): + best_m = cutlass.Int32(m) + best_c = cm + s_r0col[0] = best_m + if best_m >= cutlass.Int32(0): + s_thr[0] = s_mt_thr[best_m] + s_iscalars[0] = s_mt_cnt[best_m] + # done=1: the threshold is admitted, so Phase 3 must + # SKIP its retry-shrink and honor s_thr[0]. (block_count + # _ge / secant leave done via their own path; the R0 + # admission must set it explicitly or Phase 3 re-searches + # and the cluster collect diverges -> wrong output.) + s_iscalars[1] = cutlass.Int32(1) + # Snapshot this CTA's LOCAL slice count for the chosen + # rung into s_iscalars[5] — the per-CTA cand_count that + # Phase 3/4's cluster gather consumes (block_count_ge + # sets it too; the R0 admission must match). Without it + # the cluster collect under-counts -> wrong output. + if cutlass.const_expr(cluster_size > 1): + s_iscalars[5] = s_cluster_partial_m[best_m] cute.arch.barrier() bc = s_r0col[0] @@ -4978,19 +4998,6 @@ def _run_phases( cand = bhi elif clo < cutlass.Int32(0): cand = blo - if cutlass.const_expr(self.use_ext_counts): - # tightest ext rung already in [K, kC]: - # measure exactly it (accepts in one pass). - cbe = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(M): - cm4 = s_mt_cnt[m] - if ( - cm4 >= cutlass.Int32(self.top_k) - and cm4 <= cutlass.Int32(self.kC) - and cm4 < cbe - ): - cbe = cm4 - cand = s_mt_thr[m] s_thr[0] = cand cute.arch.barrier() rs = cutlass.Int32(0) From 604c379188ca7f4e75b6a0687a1956e4e6327ff2 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:48:09 -0700 Subject: [PATCH 012/117] [None][feat] GVR: xstate closed-loop writeback at Phase 4 exit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emit_xstate writes the per-row loop state (interface v2 layout [rows, 8]: [0] valid, [1] kth proxy, [2] accepted threshold, [3] cand_count from the pre-P4 snapshot — P4 repurposes the s_iscalars slots) at the cs==1 Phase-4 exit; degenerate identity rows write valid=0. The next step derives its seed rung group from these fields. Real-data validation: exactness unchanged; state fields exact (cand_count == count_ge(threshold): flash 991/633, pro 1854/2354); same-step reseed from the written state admits in-band with the exact count on ALL cells — including pro 1M, whose static rung group missed entirely (the temporal rung fixes the miss AND slims flash 1M admission 1290 -> 633). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 37 +++++++++++++++++++ .../cute_dsl_kernels/top_k/run_gvr_topk.py | 21 +++++++++++ 2 files changed, 58 insertions(+) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index a41de7cb6ef8..f31251f65efd 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -272,6 +272,7 @@ def __init__( p2_warp_redundant: bool = True, enable_block_skip: bool = False, use_ext_counts: bool = False, + emit_xstate: bool = False, ): # Redundant-warp sync reduction: every warp replays the block # reduce + decision from the same staged SMEM partials in the @@ -545,6 +546,12 @@ def __init__( # P1b and the M-ary count pass are skipped; an in-band rung is # re-measured ONCE through the seeded refine (per-thread hand-off) # and accepted; a miss seeds log-falsi with the external brackets. + # emit_xstate: write the per-row closed-loop state at Phase 4 exit + # (interface v2: [0] valid, [1] kth proxy, [2] accepted threshold, + # [3] cand_count). cs==1 only (leader-gather rows land later). + self.emit_xstate = bool(emit_xstate) + if emit_xstate and cluster_size != 1: + raise ValueError("emit_xstate is single-CTA (cs==1) only") self.use_ext_counts = bool(use_ext_counts) and bool(enable_r0) if self.use_ext_counts: if not self.fb_fix: @@ -4119,6 +4126,7 @@ def gvr_topk_kernel( block_max: cute.Tensor, # [numRows, nb_pad*4] fp32 (or None: no block-skip) seed_thr: cute.Tensor, # [numRows, 3] fp32 (or None: no ext counts) seed_counts: cute.Tensor, # [numRows, 3] int32 (or None) + xstate: cute.Tensor, # [numRows, 8] fp32 closed-loop state (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -4173,6 +4181,7 @@ def gvr_topk_kernel( block_max=block_max, seed_thr=seed_thr, seed_counts=seed_counts, + xstate=xstate, ) @cute.jit @@ -4187,6 +4196,7 @@ def run_one_row( block_max: cute.Tensor = None, # [numRows, nb_pad*4] fp32 seed_thr: cute.Tensor = None, # [numRows, 3] fp32 (ext counts) seed_counts: cute.Tensor = None, # [numRows, 3] int32 (ext counts) + xstate: cute.Tensor = None, # [numRows, 8] fp32 (emit_xstate) ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -4255,6 +4265,10 @@ def run_one_row( else: seed_thr_row = None seed_counts_row = None + if cutlass.const_expr(self.emit_xstate and xstate is not None): + xstate_row = xstate[row_idx, None] + else: + xstate_row = None # When return_output_values=False, ``output_values`` is None at # launch and the gated writes below are compiled out; slicing into # None would crash so we keep the view None as well. @@ -4553,6 +4567,7 @@ def run_one_row( block_max_row=block_max_row, seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, + xstate_row=xstate_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4599,6 +4614,7 @@ def run_one_row( block_max_row=block_max_row, seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, + xstate_row=xstate_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4642,6 +4658,7 @@ def run_one_row( block_max_row=block_max_row, seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, + xstate_row=xstate_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4688,6 +4705,7 @@ def _run_phases( block_max_row=None, # block-skip: this row's per-32-position bounds seed_thr_row=None, # ext counts: this row's 3 seed thresholds (fp32) seed_counts_row=None, # ext counts: this row's 3 exact counts (int32) + xstate_row=None, # emit_xstate: this row's [8] fp32 state slot smem_active=None, s_active_cnt=None, ): @@ -4767,6 +4785,8 @@ def _run_phases( if cutlass.const_expr(self.return_output_values): output_values_row[je] = input_row[je] je = je + cutlass.Int32(1) + if cutlass.const_expr(self.emit_xstate): + xstate_row[0] = cutlass.Float32(0.0) # degenerate else: # cs>1: all cluster CTAs enter _run_phases; only leader writes. if is_leader & (tidx == cutlass.Int32(0)): @@ -4779,6 +4799,8 @@ def _run_phases( if cutlass.const_expr(self.return_output_values): output_values_row[je] = input_row[je] je = je + cutlass.Int32(1) + if cutlass.const_expr(self.emit_xstate): + xstate_row[0] = cutlass.Float32(0.0) # degenerate else: # Stage this CTA's slice into SMEM once before Phase 2's # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses @@ -5202,6 +5224,19 @@ def _run_phases( warp_id, lane, ) + if cutlass.const_expr(self.emit_xstate): + # Closed-loop state (interface v2): [0] valid, [1] kth + # proxy (= accepted threshold; the tie-fill makes it a + # tight lower bound of the true kth), [2] accepted + # threshold, [3] cand_count. The next step derives its + # seed rung group from these. + if tidx == cutlass.Int32(0): + xstate_row[0] = cutlass.Float32(1.0) + xstate_row[1] = s_thr[0] + xstate_row[2] = s_thr[0] + # cand_count_p4 = pre-P4 snapshot (P4 repurposes + # the s_iscalars slots). + xstate_row[3] = cutlass.Float32(cand_count_p4) else: # cs>1: only the leader (CTA 0 in cluster) runs Phase 4. if is_leader: @@ -5305,6 +5340,7 @@ def __call__( block_max: cute.Tensor = None, # block-skip bounds; None = disabled seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (ext counts) seed_counts: cute.Tensor = None, # [num_rows, 3] int32 (ext counts) + xstate: cute.Tensor = None, # [num_rows, 8] fp32 (emit_xstate) ): num_rows = input_data.shape[0] cluster_size = cutlass.const_expr(self.cluster_size) @@ -5329,6 +5365,7 @@ def __call__( block_max, seed_thr, seed_counts, + xstate, ).launch( grid=(total_ctas, 1, 1), block=(self.num_threads, 1, 1), diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index d3a8ab64970f..b490842a7aa2 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -66,6 +66,7 @@ def _compile( p2_warp_redundant: bool = True, enable_block_skip: bool = False, use_ext_counts: bool = False, + emit_xstate: bool = False, ): """JIT-compile the GVR kernel for a specific knob combination. @@ -148,6 +149,13 @@ def _compile( if use_ext_counts else None ) + xstate_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (n_rows, 8), stride_order=(1, 0), assumed_align=4 + ) + if emit_xstate + else None + ) fake_stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True) kernel = GvrTopKKernel( dtype=cute_dtype, @@ -168,6 +176,7 @@ def _compile( p2_warp_redundant=p2_warp_redundant, enable_block_skip=enable_block_skip, use_ext_counts=use_ext_counts, + emit_xstate=emit_xstate, # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The # qfrac VALUES are irrelevant on this path (P1b is skipped) — only # the slot count matters. @@ -185,6 +194,7 @@ def _compile( block_max=block_max_fake, seed_thr=seed_thr_fake, seed_counts=seed_counts_fake, + xstate=xstate_fake, options="--enable-tvm-ffi", ) @@ -439,6 +449,7 @@ def gvr_topk_decode( skip_min_n: Optional[int] = 200_000, seed_thr: Optional[torch.Tensor] = None, seed_counts: Optional[torch.Tensor] = None, + xstate: Optional[torch.Tensor] = None, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -535,6 +546,14 @@ def gvr_topk_decode( and seed_counts.is_contiguous() and seed_counts.shape == (num_rows, 3) ), "seed_counts must be contiguous CUDA int32 [num_rows, 3]" + emit_xstate = xstate is not None + if emit_xstate: + assert ( + xstate.dtype == torch.float32 + and xstate.is_cuda + and xstate.is_contiguous() + and xstate.shape == (num_rows, 8) + ), "xstate must be contiguous CUDA fp32 [num_rows, 8]" enable_block_skip = block_max is not None if enable_block_skip: assert ( @@ -614,6 +633,7 @@ def gvr_topk_decode( p2_warp_redundant, enable_block_skip, use_ext_counts, + emit_xstate, ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. @@ -629,6 +649,7 @@ def gvr_topk_decode( block_max if enable_block_skip else None, seed_thr if use_ext_counts else None, seed_counts if use_ext_counts else None, + xstate if emit_xstate else None, ) if return_output_values: return out_values, out_indices From f9428fc86e613b3fdb3e71edb37a4bb10ed18518 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:01:27 -0700 Subject: [PATCH 013/117] [None][feat] GVR: waterfall L2 direct-to-P4 from pre-collected pairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit use_ext_cand: epilogue-collected (value, index) pairs land straight in smem_keys/vals via a cooperative sentinel-skipping SMEM-atomic load — no P1, no counting, no Phase-3 scan. Eligibility (void == 0, claimed <= cand_cap, collect rung count in [K, kC]) is a CTA-uniform register predicate, so the dynamic skip of the P2/P3 slab stays convergent; ineligible rows fall through to the ext-counts path unchanged. Real data: 6 cells x {ext, l2, forced-void fallback} all exact. The direct path makes top-k O(cand_count), independent of N: eligible rows cost ~12.2us warm from 16k through 1M (flash 1M: 2.84x vs the ext count path, below even the cold skipR0 15.6us). With the 0.89-0.97 chain in-band rates, ~90% of production rows hit this floor when the epilogue emits cand. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 757 ++++++++++-------- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 44 + 2 files changed, 470 insertions(+), 331 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index f31251f65efd..36cd79cde247 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -272,6 +272,9 @@ def __init__( p2_warp_redundant: bool = True, enable_block_skip: bool = False, use_ext_counts: bool = False, + use_ext_cand: bool = False, + cand_cap: int = 5120, + cand_rung: int = 1, emit_xstate: bool = False, ): # Redundant-warp sync reduction: every warp replays the block @@ -552,6 +555,16 @@ def __init__( self.emit_xstate = bool(emit_xstate) if emit_xstate and cluster_size != 1: raise ValueError("emit_xstate is single-CTA (cs==1) only") + # use_ext_cand (waterfall L2 direct-to-P4): pre-collected (value, + # index) pairs from the epilogue land straight in smem_keys/vals — + # no P1, no counting, no P3 scan. Eligible when void==0, claimed + # <= cand_cap and the collect rung's exact count is in [K, kC]; + # ineligible rows fall through to the ext-counts path. + self.use_ext_cand = bool(use_ext_cand) + self.cand_cap = int(cand_cap) + self.cand_rung = int(cand_rung) + if use_ext_cand and not use_ext_counts: + raise ValueError("use_ext_cand requires use_ext_counts") self.use_ext_counts = bool(use_ext_counts) and bool(enable_r0) if self.use_ext_counts: if not self.fb_fix: @@ -4127,6 +4140,8 @@ def gvr_topk_kernel( seed_thr: cute.Tensor, # [numRows, 3] fp32 (or None: no ext counts) seed_counts: cute.Tensor, # [numRows, 3] int32 (or None) xstate: cute.Tensor, # [numRows, 8] fp32 closed-loop state (or None) + cand: cute.Tensor, # [numRows, CAP*2] int32 pairs (or None) + cand_ctl: cute.Tensor, # [numRows, 2] int32 {claimed, void} (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -4182,6 +4197,8 @@ def gvr_topk_kernel( seed_thr=seed_thr, seed_counts=seed_counts, xstate=xstate, + cand=cand, + cand_ctl=cand_ctl, ) @cute.jit @@ -4197,6 +4214,8 @@ def run_one_row( seed_thr: cute.Tensor = None, # [numRows, 3] fp32 (ext counts) seed_counts: cute.Tensor = None, # [numRows, 3] int32 (ext counts) xstate: cute.Tensor = None, # [numRows, 8] fp32 (emit_xstate) + cand: cute.Tensor = None, # [numRows, CAP*2] int32 (ext cand) + cand_ctl: cute.Tensor = None, # [numRows, 2] int32 (ext cand) ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -4269,6 +4288,12 @@ def run_one_row( xstate_row = xstate[row_idx, None] else: xstate_row = None + if cutlass.const_expr(self.use_ext_cand and cand is not None and cand_ctl is not None): + cand_row = cand[row_idx, None] + cand_ctl_row = cand_ctl[row_idx, None] + else: + cand_row = None + cand_ctl_row = None # When return_output_values=False, ``output_values`` is None at # launch and the gated writes below are compiled out; slicing into # None would crash so we keep the view None as well. @@ -4568,6 +4593,8 @@ def run_one_row( seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, xstate_row=xstate_row, + cand_row=cand_row, + cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4615,6 +4642,8 @@ def run_one_row( seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, xstate_row=xstate_row, + cand_row=cand_row, + cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4659,6 +4688,8 @@ def run_one_row( seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, xstate_row=xstate_row, + cand_row=cand_row, + cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, ) @@ -4706,6 +4737,8 @@ def _run_phases( seed_thr_row=None, # ext counts: this row's 3 seed thresholds (fp32) seed_counts_row=None, # ext counts: this row's 3 exact counts (int32) xstate_row=None, # emit_xstate: this row's [8] fp32 state slot + cand_row=None, # ext cand: this row's [CAP*2] int32 pairs + cand_ctl_row=None, # ext cand: this row's [2] int32 {claimed, void} smem_active=None, s_active_cnt=None, ): @@ -4802,304 +4835,384 @@ def _run_phases( if cutlass.const_expr(self.emit_xstate): xstate_row[0] = cutlass.Float32(0.0) # degenerate else: - # Stage this CTA's slice into SMEM once before Phase 2's - # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses - # scatter-loads OUTSIDE this slice, so it stays on GMEM. - if cutlass.const_expr(self.enable_smem_cache): - self.load_slice_to_smem( - input_row, - slice_start, - slice_end, - smem_input, - tidx, - ) - - # ---- Phase 2: R0 histogram-ladder admission (single-CTA fast - # path) or the secant threshold search ---- - # enable_r0 gates to cluster_size==1 for now: op#26's R0 scans the - # full row in one CTA. The slice-parallel + cluster count-merge - # variant that lets R0 cover the cs>1 long-row branch lands in a - # later commit; until then cs>1 keeps the secant path. - if cutlass.const_expr(self.enable_r0): - # P1b rung placement -> ONE M-ary R0 count pass -> accept the - # tightest rung with count in [K, kC]. On a miss, fall back to - # the inline log-falsi R1 shot / fb_fix refine. At cs>1 each - # CTA scans its slice and block_count_ge_multi cluster-merges - # the rung counts (phase1b rungs are per-CTA identical since - # preIdx stats are full-row). - if cutlass.const_expr(self.use_ext_counts): - # ---- Waterfall L1 admission (ext rungs, v2a) ---- - # Rung thresholds arrive from the indexer epilogue: - # ONLY P1b is skipped. The stock M-ary count pass runs - # on the ext rungs so the block-skip list build, rung - # tightening, per-thread hand-off and classify all - # compose unchanged (v1 routed through the dense - # refine and forfeited the compact-walk win: flash 1M - # ext 34.7us vs skipR0 15.6us cold). - # v2b: when an ext count is already in [K, kC], park - # THE ADMITTED THRESHOLD IN ALL RUNG SLOTS — the M-ary - # pass degenerates to one compact single-threshold - # count (+ list build at that threshold) and classify - # admits it; a full miss keeps the 3 distinct rungs as - # measured brackets for the seeded refine. + # ---- Waterfall L2: direct-to-P4 from pre-collected pairs ---- + # Eligibility is a CTA-uniform register predicate (all threads + # read the same gmem control words), so the dynamic branches + # below (with barriers inside) stay convergent. + take_cand = cutlass.Int32(0) + if cutlass.const_expr(self.use_ext_cand): + claimed_c = cutlass.Int32(cand_ctl_row[0]) + void_c = cutlass.Int32(cand_ctl_row[1]) + ccnt_c = cutlass.Int32(seed_counts_row[self.cand_rung]) + if ( + void_c == cutlass.Int32(0) + and claimed_c <= cutlass.Int32(self.cand_cap) + and ccnt_c >= cutlass.Int32(self.top_k) + and ccnt_c <= cutlass.Int32(self.kC) + ): + take_cand = cutlass.Int32(1) + if take_cand == cutlass.Int32(1): + # Cooperative sentinel-skipping load of (value, index) + # pairs into the P4 candidate arrays. SMEM-atomic + # compaction: order is irrelevant to rank-scatter. if tidx == cutlass.Int32(0): - bx_m = cutlass.Int32(-1) - bx_c = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cx = cutlass.Int32(seed_counts_row[m]) - if ( - cx >= cutlass.Int32(self.top_k) - and cx <= cutlass.Int32(self.kC) - and cx < bx_c - ): - bx_m = cutlass.Int32(m) - bx_c = cx - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - if bx_m >= cutlass.Int32(0): - s_mt_thr[m] = seed_thr_row[bx_m] - else: - s_mt_thr[m] = seed_thr_row[m] + s_iscalars[0] = cutlass.Int32(0) + s_thr[0] = seed_thr_row[self.cand_rung] + s_iscalars[1] = cutlass.Int32(1) # done: no retry cute.arch.barrier() - if cutlass.const_expr(not self.use_ext_counts): - if cutlass.const_expr(self.p1b_cache): - # rungs from the SMEM gather-cache P1 stashed (no 2nd - # GMEM gather); 16-bit only. - self.phase1b_hspace_rungs_cached( - pre_idx_count, - smem_gath, - smem_hist, - s_thr, - s_mt_thr, - tidx, - warp_id, - lane, - ) - else: - self.phase1b_hspace_rungs( - input_row, - N, - pre_idx_row, - pre_idx_count, - pre_idx_offset, - smem_hist, - s_thr, - s_mt_thr, - tidx, - warp_id, - lane, + cbase = cand_row.iterator.toint() + i_c = tidx + while i_c < claimed_c: + pa = cbase + cutlass.Int64(i_c) * cutlass.Int64(8) + ip_c = cute.make_ptr( + cutlass.Int32, + pa + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, ) - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) - cute.arch.barrier() - if tidx == 0: - # tightest admissible rung = SMALLEST count in [K, kC]. - # (Explicit argmin: with r0_vseed the pmean column is not - # sorted into the rung order; for sorted rungs this is - # equivalent to the old "last m in window" rule.) - # Dropped rungs (block-skip rung tightening) hold PARTIAL - # counts — never admissible. - dmask_c = cutlass.Int32(0) - if cutlass.const_expr(self.enable_block_skip): - dmask_c = s_active_cnt[2] - best_m = cutlass.Int32(-1) - best_c = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cm = s_mt_cnt[m] - if ( - cm >= cutlass.Int32(self.top_k) - and cm <= cutlass.Int32(self.kC) - and cm < best_c - and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) - == cutlass.Int32(0) - ): - best_m = cutlass.Int32(m) - best_c = cm - s_r0col[0] = best_m - if best_m >= cutlass.Int32(0): - s_thr[0] = s_mt_thr[best_m] - s_iscalars[0] = s_mt_cnt[best_m] - # done=1: the threshold is admitted, so Phase 3 must - # SKIP its retry-shrink and honor s_thr[0]. (block_count - # _ge / secant leave done via their own path; the R0 - # admission must set it explicitly or Phase 3 re-searches - # and the cluster collect diverges -> wrong output.) - s_iscalars[1] = cutlass.Int32(1) - # Snapshot this CTA's LOCAL slice count for the chosen - # rung into s_iscalars[5] — the per-CTA cand_count that - # Phase 3/4's cluster gather consumes (block_count_ge - # sets it too; the R0 admission must match). Without it - # the cluster collect under-counts -> wrong output. - if cutlass.const_expr(cluster_size > 1): - s_iscalars[5] = s_cluster_partial_m[best_m] + pidx = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] + if pidx >= cutlass.Int32(0): + vp_c = cute.make_ptr( + cutlass.Float32, + pa, + cute.AddressSpace.gmem, + assumed_align=8, + ) + pval = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] + wpos = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if wpos < cutlass.Int32(self.kC): + smem_keys[wpos] = pval + smem_vals[wpos] = pidx + i_c = i_c + cutlass.Int32(num_threads) + cute.arch.barrier() + if take_cand == cutlass.Int32(0): + # Stage this CTA's slice into SMEM once before Phase 2's + # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses + # scatter-loads OUTSIDE this slice, so it stays on GMEM. + if cutlass.const_expr(self.enable_smem_cache): + self.load_slice_to_smem( + input_row, + slice_start, + slice_end, + smem_input, + tidx, + ) - cute.arch.barrier() - bc = s_r0col[0] - if bc >= cutlass.Int32(0) and bc < cutlass.Int32(self.M_qf): - # accepted rung column: copy its cached per-thread counts - # into the secant hand-off buffer (zero rescan). The vseed - # column (bc == M_qf) is ALREADY in smem_ptcnt (v3 reuse). - smem_ptcnt[tidx] = smem_ptcnt_multi[bc * cutlass.Int32(num_threads) + tidx] - cute.arch.barrier() - # ---- R0 miss: SEEDED bounded log-falsi refine ---- - # At large N the M2D rungs straddle [K, kC]; the refine must - # find a threshold with count in [K, kC] between the measured - # rungs. SEED the loop with the rung bracket AND its known - # counts (clo/chi) so it does log-count regula-falsi from - # iter 0 with no re-measure and no separate R1 shot -> ~2-3 - # count passes (op#26 efficiency) instead of ~6. done=1 on - # accept so Phase 3 skips its retry-shrink. - if bc < cutlass.Int32(0): - if cutlass.const_expr(self.enable_block_skip): - if tidx == cutlass.Int32(0): - s_active_cnt[1] = cutlass.Int32(0) - if cutlass.const_expr(self.fb_fix): + # ---- Phase 2: R0 histogram-ladder admission (single-CTA fast + # path) or the secant threshold search ---- + # enable_r0 gates to cluster_size==1 for now: op#26's R0 scans the + # full row in one CTA. The slice-parallel + cluster count-merge + # variant that lets R0 cover the cs>1 long-row branch lands in a + # later commit; until then cs>1 keeps the secant path. + if cutlass.const_expr(self.enable_r0): + # P1b rung placement -> ONE M-ary R0 count pass -> accept the + # tightest rung with count in [K, kC]. On a miss, fall back to + # the inline log-falsi R1 shot / fb_fix refine. At cs>1 each + # CTA scans its slice and block_count_ge_multi cluster-merges + # the rung counts (phase1b rungs are per-CTA identical since + # preIdx stats are full-row). + if cutlass.const_expr(self.use_ext_counts): + # ---- Waterfall L1 admission (ext rungs, v2a) ---- + # Rung thresholds arrive from the indexer epilogue: + # ONLY P1b is skipped. The stock M-ary count pass runs + # on the ext rungs so the block-skip list build, rung + # tightening, per-thread hand-off and classify all + # compose unchanged (v1 routed through the dense + # refine and forfeited the compact-walk win: flash 1M + # ext 34.7us vs skipR0 15.6us cold). + # v2b: when an ext count is already in [K, kC], park + # THE ADMITTED THRESHOLD IN ALL RUNG SLOTS — the M-ary + # pass degenerates to one compact single-threshold + # count (+ list build at that threshold) and classify + # admits it; a full miss keeps the 3 distinct rungs as + # measured brackets for the seeded refine. if tidx == cutlass.Int32(0): - M = cutlass.const_expr(self.M_thr) - blo = v_lo - bhi = v_hi - clo = cutlass.Int32(-1) - chi = cutlass.Int32(-1) - dmask_f = cutlass.Int32(0) - if cutlass.const_expr(self.enable_block_skip): - dmask_f = s_active_cnt[2] - for m in cutlass.range_constexpr(M): - cm = s_mt_cnt[m] - tm = s_mt_thr[m] - m_ok = ( - dmask_f & (cutlass.Int32(1) << cutlass.Int32(m)) - ) == cutlass.Int32(0) - if ( - m_ok - and cm > cutlass.Int32(self.kC) - and (clo < cutlass.Int32(0) or tm > blo) - ): - blo = tm - clo = cm + bx_m = cutlass.Int32(-1) + bx_c = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cx = cutlass.Int32(seed_counts_row[m]) if ( - m_ok - and cm < cutlass.Int32(self.top_k) - and (chi < cutlass.Int32(0) or tm < bhi) + cx >= cutlass.Int32(self.top_k) + and cx <= cutlass.Int32(self.kC) + and cx < bx_c ): - bhi = tm - chi = cm - s_thr[1] = blo - s_thr[2] = bhi - s_iscalars[2] = clo # SEED known rung counts - s_iscalars[3] = chi - s_iscalars[1] = cutlass.Int32(0) # done=0 - cand = (blo + bhi) * cutlass.Float32(0.5) - if clo > cutlass.Int32(0) and chi >= cutlass.Int32(0): - chic = chi - if chic < cutlass.Int32(1): - chic = cutlass.Int32(1) - l_lo = cmath.log2(cutlass.Float32(clo), fastmath=True) - l_hi = cmath.log2(cutlass.Float32(chic), fastmath=True) - den = l_lo - l_hi - if den > cutlass.Float32(0.0): - t3 = (cutlass.Float32(self.log2_mstar) - l_hi) / den - cnd3 = bhi + t3 * (blo - bhi) - if cnd3 > blo and cnd3 < bhi: - cand = cnd3 - elif chi < cutlass.Int32(0): - cand = bhi - elif clo < cutlass.Int32(0): - cand = blo - s_thr[0] = cand + bx_m = cutlass.Int32(m) + bx_c = cx + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + if bx_m >= cutlass.Int32(0): + s_mt_thr[m] = seed_thr_row[bx_m] + else: + s_mt_thr[m] = seed_thr_row[m] cute.arch.barrier() - rs = cutlass.Int32(0) - while rs < cutlass.Int32(8) and s_iscalars[1] == cutlass.Int32(0): - if rs > cutlass.Int32(0): - if tidx == cutlass.Int32(0): - lo3 = s_thr[1] - hi3 = s_thr[2] - clo3 = s_iscalars[2] - chi3 = s_iscalars[3] - cand = (lo3 + hi3) * cutlass.Float32(0.5) - if chi3 < cutlass.Int32(0): - cand = hi3 - elif clo3 < cutlass.Int32(0): - cand = lo3 - else: - chic = chi3 - if chic < cutlass.Int32(1): - chic = cutlass.Int32(1) - l_lo = cmath.log2(cutlass.Float32(clo3), fastmath=True) - l_hi = cmath.log2(cutlass.Float32(chic), fastmath=True) - den3 = l_lo - l_hi - if den3 > cutlass.Float32(0.0): - t3 = (cutlass.Float32(self.log2_mstar) - l_hi) / den3 - cnd3 = hi3 + t3 * (lo3 - hi3) - if cnd3 > lo3 and cnd3 < hi3: - cand = cnd3 - s_thr[0] = cand - cute.arch.barrier() - self.block_count_ge( + if cutlass.const_expr(not self.use_ext_counts): + if cutlass.const_expr(self.p1b_cache): + # rungs from the SMEM gather-cache P1 stashed (no 2nd + # GMEM gather); 16-bit only. + self.phase1b_hspace_rungs_cached( + pre_idx_count, + smem_gath, + smem_hist, + s_thr, + s_mt_thr, + tidx, + warp_id, + lane, + ) + else: + self.phase1b_hspace_rungs( input_row, - slice_start, - slice_end, - s_thr[0], - smem_ptcnt, - smem_wcnt, - s_iscalars, - s_cluster_partial, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_hist, + s_thr, + s_mt_thr, tidx, warp_id, lane, - do_cluster_sync=do_cluster_sync, - smem_input=smem_input, ) - cute.arch.barrier() + self.block_count_ge_multi( + input_row, + slice_start, + slice_end, + s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, + tidx, + warp_id, + lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, + ) + cute.arch.barrier() + if tidx == 0: + # tightest admissible rung = SMALLEST count in [K, kC]. + # (Explicit argmin: with r0_vseed the pmean column is not + # sorted into the rung order; for sorted rungs this is + # equivalent to the old "last m in window" rule.) + # Dropped rungs (block-skip rung tightening) hold PARTIAL + # counts — never admissible. + dmask_c = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip): + dmask_c = s_active_cnt[2] + best_m = cutlass.Int32(-1) + best_c = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cm = s_mt_cnt[m] + if ( + cm >= cutlass.Int32(self.top_k) + and cm <= cutlass.Int32(self.kC) + and cm < best_c + and (dmask_c & (cutlass.Int32(1) << cutlass.Int32(m))) + == cutlass.Int32(0) + ): + best_m = cutlass.Int32(m) + best_c = cm + s_r0col[0] = best_m + if best_m >= cutlass.Int32(0): + s_thr[0] = s_mt_thr[best_m] + s_iscalars[0] = s_mt_cnt[best_m] + # done=1: the threshold is admitted, so Phase 3 must + # SKIP its retry-shrink and honor s_thr[0]. (block_count + # _ge / secant leave done via their own path; the R0 + # admission must set it explicitly or Phase 3 re-searches + # and the cluster collect diverges -> wrong output.) + s_iscalars[1] = cutlass.Int32(1) + # Snapshot this CTA's LOCAL slice count for the chosen + # rung into s_iscalars[5] — the per-CTA cand_count that + # Phase 3/4's cluster gather consumes (block_count_ge + # sets it too; the R0 admission must match). Without it + # the cluster collect under-counts -> wrong output. + if cutlass.const_expr(cluster_size > 1): + s_iscalars[5] = s_cluster_partial_m[best_m] + + cute.arch.barrier() + bc = s_r0col[0] + if bc >= cutlass.Int32(0) and bc < cutlass.Int32(self.M_qf): + # accepted rung column: copy its cached per-thread counts + # into the secant hand-off buffer (zero rescan). The vseed + # column (bc == M_qf) is ALREADY in smem_ptcnt (v3 reuse). + smem_ptcnt[tidx] = smem_ptcnt_multi[bc * cutlass.Int32(num_threads) + tidx] + cute.arch.barrier() + # ---- R0 miss: SEEDED bounded log-falsi refine ---- + # At large N the M2D rungs straddle [K, kC]; the refine must + # find a threshold with count in [K, kC] between the measured + # rungs. SEED the loop with the rung bracket AND its known + # counts (clo/chi) so it does log-count regula-falsi from + # iter 0 with no re-measure and no separate R1 shot -> ~2-3 + # count passes (op#26 efficiency) instead of ~6. done=1 on + # accept so Phase 3 skips its retry-shrink. + if bc < cutlass.Int32(0): + if cutlass.const_expr(self.enable_block_skip): if tidx == cutlass.Int32(0): - c3 = s_iscalars[0] - t3v = s_thr[0] - if c3 >= cutlass.Int32(self.top_k) and c3 <= cutlass.Int32(self.kC): - s_iscalars[1] = cutlass.Int32(1) # accept - elif c3 > cutlass.Int32(self.kC): - s_thr[1] = t3v - s_iscalars[2] = c3 - if t3v >= s_thr[2]: - rng3 = s_thr[2] - s_thr[1] - if rng3 < cutlass.Float32(1.0): - rng3 = cutlass.Float32(1.0) - s_thr[2] = s_thr[2] + rng3 * cutlass.Float32(8.0) - s_iscalars[3] = cutlass.Int32(-1) - else: - s_thr[2] = t3v - s_iscalars[3] = c3 - if t3v <= s_thr[1]: - rng3 = s_thr[2] - s_thr[1] - if rng3 < cutlass.Float32(1.0): - rng3 = cutlass.Float32(1.0) - s_thr[1] = s_thr[1] - rng3 * cutlass.Float32(8.0) - s_iscalars[2] = cutlass.Int32(-1) + s_active_cnt[1] = cutlass.Int32(0) + if cutlass.const_expr(self.fb_fix): + if tidx == cutlass.Int32(0): + M = cutlass.const_expr(self.M_thr) + blo = v_lo + bhi = v_hi + clo = cutlass.Int32(-1) + chi = cutlass.Int32(-1) + dmask_f = cutlass.Int32(0) + if cutlass.const_expr(self.enable_block_skip): + dmask_f = s_active_cnt[2] + for m in cutlass.range_constexpr(M): + cm = s_mt_cnt[m] + tm = s_mt_thr[m] + m_ok = ( + dmask_f & (cutlass.Int32(1) << cutlass.Int32(m)) + ) == cutlass.Int32(0) + if ( + m_ok + and cm > cutlass.Int32(self.kC) + and (clo < cutlass.Int32(0) or tm > blo) + ): + blo = tm + clo = cm + if ( + m_ok + and cm < cutlass.Int32(self.top_k) + and (chi < cutlass.Int32(0) or tm < bhi) + ): + bhi = tm + chi = cm + s_thr[1] = blo + s_thr[2] = bhi + s_iscalars[2] = clo # SEED known rung counts + s_iscalars[3] = chi + s_iscalars[1] = cutlass.Int32(0) # done=0 + cand = (blo + bhi) * cutlass.Float32(0.5) + if clo > cutlass.Int32(0) and chi >= cutlass.Int32(0): + chic = chi + if chic < cutlass.Int32(1): + chic = cutlass.Int32(1) + l_lo = cmath.log2(cutlass.Float32(clo), fastmath=True) + l_hi = cmath.log2(cutlass.Float32(chic), fastmath=True) + den = l_lo - l_hi + if den > cutlass.Float32(0.0): + t3 = (cutlass.Float32(self.log2_mstar) - l_hi) / den + cnd3 = bhi + t3 * (blo - bhi) + if cnd3 > blo and cnd3 < bhi: + cand = cnd3 + elif chi < cutlass.Int32(0): + cand = bhi + elif clo < cutlass.Int32(0): + cand = blo + s_thr[0] = cand cute.arch.barrier() - rs = rs + cutlass.Int32(1) - if s_iscalars[1] != cutlass.Int32(1): - # tie-plateau fail-soft: land on the measured - # undershoot side (count <= kC => no overflow). - self.block_count_ge( + rs = cutlass.Int32(0) + while rs < cutlass.Int32(8) and s_iscalars[1] == cutlass.Int32(0): + if rs > cutlass.Int32(0): + if tidx == cutlass.Int32(0): + lo3 = s_thr[1] + hi3 = s_thr[2] + clo3 = s_iscalars[2] + chi3 = s_iscalars[3] + cand = (lo3 + hi3) * cutlass.Float32(0.5) + if chi3 < cutlass.Int32(0): + cand = hi3 + elif clo3 < cutlass.Int32(0): + cand = lo3 + else: + chic = chi3 + if chic < cutlass.Int32(1): + chic = cutlass.Int32(1) + l_lo = cmath.log2(cutlass.Float32(clo3), fastmath=True) + l_hi = cmath.log2(cutlass.Float32(chic), fastmath=True) + den3 = l_lo - l_hi + if den3 > cutlass.Float32(0.0): + t3 = ( + cutlass.Float32(self.log2_mstar) - l_hi + ) / den3 + cnd3 = hi3 + t3 * (lo3 - hi3) + if cnd3 > lo3 and cnd3 < hi3: + cand = cnd3 + s_thr[0] = cand + cute.arch.barrier() + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[0], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + do_cluster_sync=do_cluster_sync, + smem_input=smem_input, + ) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + c3 = s_iscalars[0] + t3v = s_thr[0] + if c3 >= cutlass.Int32(self.top_k) and c3 <= cutlass.Int32( + self.kC + ): + s_iscalars[1] = cutlass.Int32(1) # accept + elif c3 > cutlass.Int32(self.kC): + s_thr[1] = t3v + s_iscalars[2] = c3 + if t3v >= s_thr[2]: + rng3 = s_thr[2] - s_thr[1] + if rng3 < cutlass.Float32(1.0): + rng3 = cutlass.Float32(1.0) + s_thr[2] = s_thr[2] + rng3 * cutlass.Float32(8.0) + s_iscalars[3] = cutlass.Int32(-1) + else: + s_thr[2] = t3v + s_iscalars[3] = c3 + if t3v <= s_thr[1]: + rng3 = s_thr[2] - s_thr[1] + if rng3 < cutlass.Float32(1.0): + rng3 = cutlass.Float32(1.0) + s_thr[1] = s_thr[1] - rng3 * cutlass.Float32(8.0) + s_iscalars[2] = cutlass.Int32(-1) + cute.arch.barrier() + rs = rs + cutlass.Int32(1) + if s_iscalars[1] != cutlass.Int32(1): + # tie-plateau fail-soft: land on the measured + # undershoot side (count <= kC => no overflow). + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[2], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + do_cluster_sync=do_cluster_sync, + smem_input=smem_input, + ) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + s_thr[0] = s_thr[2] + s_iscalars[1] = cutlass.Int32(1) + cute.arch.barrier() + else: + self.phase2_secant_search( input_row, + N, slice_start, slice_end, - s_thr[2], smem_ptcnt, smem_wcnt, + s_thr, s_iscalars, s_cluster_partial, tidx, @@ -5108,34 +5221,39 @@ def _run_phases( do_cluster_sync=do_cluster_sync, smem_input=smem_input, ) - cute.arch.barrier() - if tidx == cutlass.Int32(0): - s_thr[0] = s_thr[2] - s_iscalars[1] = cutlass.Int32(1) - cute.arch.barrier() - else: - self.phase2_secant_search( - input_row, - N, - slice_start, - slice_end, - smem_ptcnt, - smem_wcnt, - s_thr, - s_iscalars, - s_cluster_partial, - tidx, - warp_id, - lane, - do_cluster_sync=do_cluster_sync, - smem_input=smem_input, - ) - else: - self.phase2_secant_search( + else: + self.phase2_secant_search( + input_row, + N, + slice_start, + slice_end, + smem_ptcnt, + smem_wcnt, + s_thr, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + do_cluster_sync=do_cluster_sync, + smem_input=smem_input, + ) + + # Cluster handoff #1 (end of Phase 2). Skipped when + # do_cluster_sync is False (cs=1 or short-row degrade). + if cutlass.const_expr(cluster_size > 1): + if do_cluster_sync: + cute.arch.cluster_arrive_relaxed() + cute.arch.cluster_wait() + + # ---- Phase 3: cluster-parallel candidate collect ---- + self.phase3_collect_candidates( input_row, N, slice_start, slice_end, + smem_keys, + smem_vals, smem_ptcnt, smem_wcnt, s_thr, @@ -5146,37 +5264,10 @@ def _run_phases( lane, do_cluster_sync=do_cluster_sync, smem_input=smem_input, + smem_active=smem_active, + s_active_cnt=s_active_cnt, ) - # Cluster handoff #1 (end of Phase 2). Skipped when - # do_cluster_sync is False (cs=1 or short-row degrade). - if cutlass.const_expr(cluster_size > 1): - if do_cluster_sync: - cute.arch.cluster_arrive_relaxed() - cute.arch.cluster_wait() - - # ---- Phase 3: cluster-parallel candidate collect ---- - self.phase3_collect_candidates( - input_row, - N, - slice_start, - slice_end, - smem_keys, - smem_vals, - smem_ptcnt, - smem_wcnt, - s_thr, - s_iscalars, - s_cluster_partial, - tidx, - warp_id, - lane, - do_cluster_sync=do_cluster_sync, - smem_input=smem_input, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) - # Cluster handoff #2: leader's DSMEM gather of peer # smem_keys/smem_vals. Skipped at do_cluster_sync=False. if cutlass.const_expr(cluster_size > 1): @@ -5341,6 +5432,8 @@ def __call__( seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (ext counts) seed_counts: cute.Tensor = None, # [num_rows, 3] int32 (ext counts) xstate: cute.Tensor = None, # [num_rows, 8] fp32 (emit_xstate) + cand: cute.Tensor = None, # [num_rows, CAP*2] int32 (ext cand) + cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 (ext cand) ): num_rows = input_data.shape[0] cluster_size = cutlass.const_expr(self.cluster_size) @@ -5366,6 +5459,8 @@ def __call__( seed_thr, seed_counts, xstate, + cand, + cand_ctl, ).launch( grid=(total_ctas, 1, 1), block=(self.num_threads, 1, 1), diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index b490842a7aa2..40d238c2704e 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -67,6 +67,8 @@ def _compile( enable_block_skip: bool = False, use_ext_counts: bool = False, emit_xstate: bool = False, + use_ext_cand: bool = False, + cand_cap: int = 5120, ): """JIT-compile the GVR kernel for a specific knob combination. @@ -149,6 +151,20 @@ def _compile( if use_ext_counts else None ) + cand_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, cand_cap * 2), stride_order=(1, 0), assumed_align=8 + ) + if use_ext_cand + else None + ) + cand_ctl_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, 2), stride_order=(1, 0), assumed_align=8 + ) + if use_ext_cand + else None + ) xstate_fake = ( cute.runtime.make_fake_compact_tensor( cutlass.Float32, (n_rows, 8), stride_order=(1, 0), assumed_align=4 @@ -177,6 +193,8 @@ def _compile( enable_block_skip=enable_block_skip, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, + use_ext_cand=use_ext_cand, + cand_cap=cand_cap, # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The # qfrac VALUES are irrelevant on this path (P1b is skipped) — only # the slot count matters. @@ -195,6 +213,8 @@ def _compile( seed_thr=seed_thr_fake, seed_counts=seed_counts_fake, xstate=xstate_fake, + cand=cand_fake, + cand_ctl=cand_ctl_fake, options="--enable-tvm-ffi", ) @@ -450,6 +470,8 @@ def gvr_topk_decode( seed_thr: Optional[torch.Tensor] = None, seed_counts: Optional[torch.Tensor] = None, xstate: Optional[torch.Tensor] = None, + cand: Optional[torch.Tensor] = None, + cand_ctl: Optional[torch.Tensor] = None, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -546,6 +568,24 @@ def gvr_topk_decode( and seed_counts.is_contiguous() and seed_counts.shape == (num_rows, 3) ), "seed_counts must be contiguous CUDA int32 [num_rows, 3]" + use_ext_cand = cand is not None and cand_ctl is not None + cand_cap = 5120 + if use_ext_cand: + assert ( + cand.dtype == torch.int32 + and cand.is_cuda + and cand.is_contiguous() + and cand.dim() == 2 + and cand.shape[0] == num_rows + and cand.shape[1] % 2 == 0 + ), "cand must be contiguous CUDA int32 [num_rows, CAP*2]" + assert ( + cand_ctl.dtype == torch.int32 + and cand_ctl.is_cuda + and cand_ctl.is_contiguous() + and cand_ctl.shape == (num_rows, 2) + ), "cand_ctl must be contiguous CUDA int32 [num_rows, 2]" + cand_cap = cand.shape[1] // 2 emit_xstate = xstate is not None if emit_xstate: assert ( @@ -634,6 +674,8 @@ def gvr_topk_decode( enable_block_skip, use_ext_counts, emit_xstate, + use_ext_cand, + cand_cap, ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. @@ -650,6 +692,8 @@ def gvr_topk_decode( seed_thr if use_ext_counts else None, seed_counts if use_ext_counts else None, xstate if emit_xstate else None, + cand if use_ext_cand else None, + cand_ctl if use_ext_cand else None, ) if return_output_values: return out_values, out_indices From 076323c686988a4cc165dc47fa295bf5d70bfbeb Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:15:54 -0700 Subject: [PATCH 014/117] [None][perf] GVR: slope-adaptive seed rung derivation (host helper) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit derive_seed_rungs places the next step's guard rungs a fixed number of count-OCTAVES from the previous accepted threshold, using the local slope of log2(count) vs threshold estimated from the previous step's own 3 rung measurements (log-linearity is the same property log-falsi exploits). Fixed spreads face a two-sided trap: too narrow misses drift, too wide puts the guard rungs themselves out of band — no single value wins both models (best fixed: pro 0.97/flash 0.92 vs flash-tuned 0.82/0.97). Real-chain kernel validation (V4-Pro/Flash multi-step captures): in-band admission pro 0.89 -> 0.99, flash 0.96, all steps exact. Combined with the L2 direct arm this routes ~97%+ of production rows to the O(cand_count) floor (cold protocol: flash 1M BS1024 30.9us = 12.1x, BS1 8.7us = 4.0x; pro 256k 1.5-1.8x). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 40d238c2704e..ed79a19cbb23 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -345,6 +345,44 @@ def emu_block_meta( return meta.contiguous() +def derive_seed_rungs( + prev_thr: torch.Tensor, + prev_sthr: "torch.Tensor | None" = None, + prev_counts: "torch.Tensor | None" = None, + count_octaves: float = 2.0, + fallback_spread: float = 0.5, +) -> torch.Tensor: + """Host-side slope-adaptive seed rung derivation (waterfall closed loop). + + Estimates the per-row local slope of log2(count) vs threshold from the + PREVIOUS step's 3 rung measurements and places the next step's guard + rungs ``count_octaves`` octaves away from the mid rung (= the previous + accepted threshold). Real-chain validation (V4-Pro/Flash captures): + in-band admission 0.958/0.975 vs 0.82-0.97 for any fixed spread. + + Args: + prev_thr: [rows] previous accepted threshold (xstate[:, 2]). + prev_sthr: [rows, 3] previous step's rung thresholds (or None). + prev_counts: [rows, 3] previous step's rung counts (or None). + + Returns: + [rows, 3] fp32 seed thresholds (ascending). + """ + if prev_sthr is None or prev_counts is None: + d = torch.full_like(prev_thr, fallback_spread) + else: + c_lo = prev_counts[:, 0].float().clamp(min=1.0) + c_hi = prev_counts[:, 2].float().clamp(min=1.0) + dthr = (prev_sthr[:, 2] - prev_sthr[:, 0]).clamp(min=1e-3) + slope = (torch.log2(c_lo) - torch.log2(c_hi)) / dthr + d = torch.where( + slope > 0.05, + count_octaves / slope.clamp(min=0.05), + torch.full_like(slope, fallback_spread), + ).clamp(0.1, 4.0) + return torch.stack([prev_thr - d, prev_thr, prev_thr + d], dim=1).contiguous() + + def emu_seed_counts( logits: torch.Tensor, seq_lens: torch.Tensor, From 0e3470d57dcac6448a08d6829a85c35db7b08ec4 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:30:26 -0700 Subject: [PATCH 015/117] [None][perf] GVR ext counts: per-row dynamic routing to the stock path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ext knobs were compile-time, so a row whose epilogue rungs all miss [K, kC] (or an xstate-invalid row, t_0 = +FLT_MAX) still paid the ext bracket-refine — measurably worse than stock (pro-1M cold: ext-miss 51us vs stock-skip 21us). Routing is now a per-row runtime predicate read from the ext counts themselves (CTA-uniform loads, so the dynamic branches with barriers inside stay convergent): in-band rows keep the ext fast path (skip P1 + P1b), miss/invalid rows run the full stock path (P1 + P1b + vseed + count) including the block-skip machinery. Warm validation: pro 1M ext+skip 43.5us (0.77x) -> 18.8us (1.80x); in-band cells unchanged (flash 1M 2.40x, pro 256k 1.07x); mixed-row chains exact with in-band 0.99/0.97 (pro/flash, adaptive rungs). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 149 +++++++++++++----- 1 file changed, 107 insertions(+), 42 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 36cd79cde247..07a98c06c1cf 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4763,6 +4763,22 @@ def _run_phases( s_active_cnt[1] = cutlass.Int32(0) s_active_cnt[2] = cutlass.Int32(0) # dropped-rung mask + # ---- Per-row dynamic routing (ext counts) ---- + # Use the epilogue rungs ONLY when the row is valid (finite t_0, + # xstate contract) AND some rung count already lies in [K, kC]. + # A miss/invalid row runs the full stock path (P1 + P1b + vseed + + # count): real data shows stock beats ext-bracket refine on + # misses (pro-1M cold: stock-skip 21us vs ext-miss 51us). All + # threads read the same control words, so the predicate is + # CTA-uniform and the dynamic branches below stay convergent. + ext_row = cutlass.Int32(0) + if cutlass.const_expr(self.use_ext_counts): + if seed_thr_row[0] < cutlass.Float32(1e37): + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cm_e = cutlass.Int32(seed_counts_row[m]) + if cm_e >= cutlass.Int32(self.top_k) and cm_e <= cutlass.Int32(self.kC): + ext_row = cutlass.Int32(1) + # ---- Phase 1: preIdx Min/Max/Mean ---- # ext counts: P1's only surviving products are the [v_lo, v_hi] # outer bracket and the scalar state init — the ext rungs provide @@ -4771,16 +4787,36 @@ def _run_phases( # target lies outside [t_0, t_2] recovers via the refine loop's # 8x bracket expansion (same fail-soft as the stock path). if cutlass.const_expr(self.use_ext_counts): - if tidx == cutlass.Int32(0): - s_thr[0] = seed_thr_row[1] - s_thr[1] = seed_thr_row[0] - s_thr[2] = seed_thr_row[2] - s_iscalars[0] = cutlass.Int32(0) # cand_count - s_iscalars[1] = cutlass.Int32(0) # done - s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb seeding owns) - s_iscalars[3] = cutlass.Int32(-1) # cnt_hi - s_iscalars[4] = cutlass.Int32(0) # out_count - cute.arch.barrier() + if ext_row == cutlass.Int32(1): + if tidx == cutlass.Int32(0): + s_thr[0] = seed_thr_row[1] + s_thr[1] = seed_thr_row[0] + s_thr[2] = seed_thr_row[2] + s_iscalars[0] = cutlass.Int32(0) # cand_count + s_iscalars[1] = cutlass.Int32(0) # done + s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb seeding owns) + s_iscalars[3] = cutlass.Int32(-1) # cnt_hi + s_iscalars[4] = cutlass.Int32(0) # out_count + cute.arch.barrier() + if ext_row == cutlass.Int32(0): + self.phase1_preidx_stats( + input_row, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_wmin, + smem_wmax, + smem_wsum, + smem_wcnt_p1, + s_thr, + s_iscalars, + tidx, + warp_id, + lane, + smem_gath=smem_gath, # p1b_cache: stash gathered values (None-op OFF) + s_mt_thr=s_mt_thr, # r0_vseed: park pmean in the last rung column + ) if cutlass.const_expr(not self.use_ext_counts): self.phase1_preidx_stats( input_row, @@ -4915,38 +4951,67 @@ def _run_phases( # the rung counts (phase1b rungs are per-CTA identical since # preIdx stats are full-row). if cutlass.const_expr(self.use_ext_counts): - # ---- Waterfall L1 admission (ext rungs, v2a) ---- - # Rung thresholds arrive from the indexer epilogue: - # ONLY P1b is skipped. The stock M-ary count pass runs - # on the ext rungs so the block-skip list build, rung - # tightening, per-thread hand-off and classify all - # compose unchanged (v1 routed through the dense - # refine and forfeited the compact-walk win: flash 1M - # ext 34.7us vs skipR0 15.6us cold). - # v2b: when an ext count is already in [K, kC], park - # THE ADMITTED THRESHOLD IN ALL RUNG SLOTS — the M-ary - # pass degenerates to one compact single-threshold - # count (+ list build at that threshold) and classify - # admits it; a full miss keeps the 3 distinct rungs as - # measured brackets for the seeded refine. - if tidx == cutlass.Int32(0): - bx_m = cutlass.Int32(-1) - bx_c = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cx = cutlass.Int32(seed_counts_row[m]) - if ( - cx >= cutlass.Int32(self.top_k) - and cx <= cutlass.Int32(self.kC) - and cx < bx_c - ): - bx_m = cutlass.Int32(m) - bx_c = cx - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - if bx_m >= cutlass.Int32(0): - s_mt_thr[m] = seed_thr_row[bx_m] - else: - s_mt_thr[m] = seed_thr_row[m] - cute.arch.barrier() + if ext_row == cutlass.Int32(1): + # ---- Waterfall L1 admission (ext rungs, v2a) ---- + # Rung thresholds arrive from the indexer epilogue: + # ONLY P1b is skipped. The stock M-ary count pass runs + # on the ext rungs so the block-skip list build, rung + # tightening, per-thread hand-off and classify all + # compose unchanged (v1 routed through the dense + # refine and forfeited the compact-walk win: flash 1M + # ext 34.7us vs skipR0 15.6us cold). + # v2b: when an ext count is already in [K, kC], park + # THE ADMITTED THRESHOLD IN ALL RUNG SLOTS — the M-ary + # pass degenerates to one compact single-threshold + # count (+ list build at that threshold) and classify + # admits it; a full miss keeps the 3 distinct rungs as + # measured brackets for the seeded refine. + if tidx == cutlass.Int32(0): + bx_m = cutlass.Int32(-1) + bx_c = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cx = cutlass.Int32(seed_counts_row[m]) + if ( + cx >= cutlass.Int32(self.top_k) + and cx <= cutlass.Int32(self.kC) + and cx < bx_c + ): + bx_m = cutlass.Int32(m) + bx_c = cx + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + if bx_m >= cutlass.Int32(0): + s_mt_thr[m] = seed_thr_row[bx_m] + else: + s_mt_thr[m] = seed_thr_row[m] + cute.arch.barrier() + if ext_row == cutlass.Int32(0): + if cutlass.const_expr(self.p1b_cache): + # rungs from the SMEM gather-cache P1 stashed (no 2nd + # GMEM gather); 16-bit only. + self.phase1b_hspace_rungs_cached( + pre_idx_count, + smem_gath, + smem_hist, + s_thr, + s_mt_thr, + tidx, + warp_id, + lane, + ) + else: + self.phase1b_hspace_rungs( + input_row, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_hist, + s_thr, + s_mt_thr, + tidx, + warp_id, + lane, + ) if cutlass.const_expr(not self.use_ext_counts): if cutlass.const_expr(self.p1b_cache): # rungs from the SMEM gather-cache P1 stashed (no 2nd From b30d476295dc0d22ec136c31a79b78d3575d057b Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 21 Jul 2026 00:29:38 -0700 Subject: [PATCH 016/117] [None][perf] GVR waterfall: cluster_size > 1 support for ext/L2/xstate The BS=1 mid/long-row cells stayed on op26 because the waterfall fast paths were cs==1-only while op26's pick_config splits a single row across cs=4/8 CTAs. The pre-collected pairs are O(cand_count), so row splitting buys the direct path nothing: at cs > 1 the LEADER loads the pairs alone (take_cand is cluster-uniform - every CTA reads the same per-row control words) and peers publish zero local candidates for the DSMEM gather; ineligible/invalid rows fall through to the native stock path at op26's own cluster split. xstate writes at the leader's Phase-4 exit; the ext count pass composes with the existing cs>1 cluster merge unchanged. Validation: bl2 cells exact at cs=1/4/8 including forced-void fallback; cs1 smokes and the adaptive-rung chains unchanged (in-band 0.99). Cold protocol with the production arm (op26 launch config + ext inputs + in-kernel routing), vs op26 baseline: flash 256k BS1/64 1.24x/1.59x, flash 512k 1.33x/1.42x, flash 1M BS64 3.69x, pro 256k 1.20-1.73x - the former regression cells flip to wins; pro 512k/1M BS1 static-rung misses route to stock (adaptive xstate rungs take them direct in the closed loop, 1.4-1.6x steady-state). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 07a98c06c1cf..259efdce6211 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -553,8 +553,6 @@ def __init__( # (interface v2: [0] valid, [1] kth proxy, [2] accepted threshold, # [3] cand_count). cs==1 only (leader-gather rows land later). self.emit_xstate = bool(emit_xstate) - if emit_xstate and cluster_size != 1: - raise ValueError("emit_xstate is single-CTA (cs==1) only") # use_ext_cand (waterfall L2 direct-to-P4): pre-collected (value, # index) pairs from the epilogue land straight in smem_keys/vals — # no P1, no counting, no P3 scan. Eligible when void==0, claimed @@ -571,8 +569,10 @@ def __init__( raise ValueError("use_ext_counts requires fb_fix") if self.M_thr != 3: raise ValueError("use_ext_counts expects exactly 3 seed rungs") - if cluster_size != 1: - raise ValueError("use_ext_counts is single-CTA (cs==1) only") + # cluster_size > 1 supported: the ext rungs/counts are + # per-row (identical across the cluster), the stock multi + # count pass cluster-merges as usual, and the L2 direct + # loader runs leader-only (peers contribute zero candidates). # R1 inline shot aim in log2-count space: geometric center of the # [K, kC] acceptance window. self.log2_r1aim = math.log2(math.sqrt(self.top_k * self.kC)) if self.r0_qfracs else 0.0 @@ -4891,6 +4891,15 @@ def _run_phases( # Cooperative sentinel-skipping load of (value, index) # pairs into the P4 candidate arrays. SMEM-atomic # compaction: order is irrelevant to rank-scatter. + # cs > 1: the pre-collected pairs are O(cand_count) — + # row splitting buys nothing, so the LEADER loads them + # alone and peers publish zero local candidates for + # the DSMEM gather (take_cand is cluster-uniform: all + # CTAs read the same per-row control words). + if cutlass.const_expr(cluster_size > 1): + if tidx == cutlass.Int32(0): + s_iscalars[5] = cutlass.Int32(0) + cute.arch.barrier() if tidx == cutlass.Int32(0): s_iscalars[0] = cutlass.Int32(0) s_thr[0] = seed_thr_row[self.cand_rung] @@ -4898,6 +4907,9 @@ def _run_phases( cute.arch.barrier() cbase = cand_row.iterator.toint() i_c = tidx + if cutlass.const_expr(cluster_size > 1): + if cta_in_cluster != cutlass.Int32(0): + i_c = claimed_c # peers: skip the walk while i_c < claimed_c: pa = cbase + cutlass.Int64(i_c) * cutlass.Int64(8) ip_c = cute.make_ptr( @@ -4924,6 +4936,12 @@ def _run_phases( smem_vals[wpos] = pidx i_c = i_c + cutlass.Int32(num_threads) cute.arch.barrier() + if cutlass.const_expr(cluster_size > 1): + # leader's local count feeds the DSMEM gather + if is_leader: + if tidx == cutlass.Int32(0): + s_iscalars[5] = s_iscalars[0] + cute.arch.barrier() if take_cand == cutlass.Int32(0): # Stage this CTA's slice into SMEM once before Phase 2's # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses @@ -5470,6 +5488,14 @@ def _run_phases( warp_id, lane, ) + if cutlass.const_expr(self.emit_xstate): + # closed-loop state, leader-only at cs > 1 (same + # layout as the cs == 1 exit). + if tidx == cutlass.Int32(0): + xstate_row[0] = cutlass.Float32(1.0) + xstate_row[1] = s_thr[0] + xstate_row[2] = s_thr[0] + xstate_row[3] = cutlass.Float32(cand_count_p4) # Final cluster barrier: keep peer CTAs (and their SMEM) alive # until the leader's gather + Phase 4 finish. Skipped at From 6cf89c1fa89a884faa3f28d1e0e286c4e66e65fa Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:40:05 -0700 Subject: [PATCH 017/117] [None][perf] GVR L2 direct: two-parameter (K, K_max) in-list filtering Collect the pre-collected candidate list at the LOOSEST seed rung and admit it whenever it is complete (claimed <= K_max) and any rung counts >= K; the filter rung (count closest to K from above) is applied on the fly while loading the pairs, so P4 sees the thinnest covering set. kC leaves the admission vocabulary and remains only as the physical smem capacity guard. Correctness: C(t_lo) >= C(t_filt) >= K implies true top-K subset of list subset of filtered set; list truncation (claim order is value-blind) remains the only fatal case and falls back. - K_max = 24576, set by a four-chain search on real captures (incl. 320k/640k long decode): 16K->24K gains 8pp direct-hit rate, 24K->32K only 0.1pp (band-limited, not capacity-limited). - Loader: 4x-unrolled latency-overlapped walk with ballot-batched smem claims (loop exit must stay warp-uniform: ragged exits deadlock the warp collectives) and un-nested value loads. Device-level (nsys kern-sum, cold L2) on a 160k real chain: the naive walk ran 0.64x vs the block-skip arm; this form reaches 1.05x at full loosest-rung coverage (eligibility 1.00). - Straddle refine (cs=1): when no rung count lands in [K, kC] but the list is complete, one 256-bin histogram pass over the list finds an in-band edge and the filtered load proceeds; smem overflow demotes to the fallback. 640k chain: straddle steps 30 -> 14-16us, device mean 1.41x -> 1.73x vs block-skip. - Byte-parity routing keeps fat lists (2*claimed*cs >= N) on the fallback: measured both ways, walking them is slower at every cs. Validation (B200): four admission modes exact (direct/filter/refine/ fallback) at cs=1 and 18/18 exact at cs=1/4/8 on real V4 bundles; four real decode chains (160k/132k/320k/640k) all-step exact with wall ratios 0.97/1.00/1.04/1.21x and device-level 1.05x (160k) / 1.73x (640k) vs the block-skip arm. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 265 ++++++++++++++++-- 1 file changed, 234 insertions(+), 31 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 259efdce6211..3db23dcc045d 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -274,7 +274,7 @@ def __init__( use_ext_counts: bool = False, use_ext_cand: bool = False, cand_cap: int = 5120, - cand_rung: int = 1, + cand_rung: int = 0, emit_xstate: bool = False, ): # Redundant-warp sync reduction: every warp replays the block @@ -4879,14 +4879,161 @@ def _run_phases( if cutlass.const_expr(self.use_ext_cand): claimed_c = cutlass.Int32(cand_ctl_row[0]) void_c = cutlass.Int32(cand_ctl_row[1]) - ccnt_c = cutlass.Int32(seed_counts_row[self.cand_rung]) + # Two-parameter admission (K, K_max): the list (collected + # at rung cand_rung) is usable whenever it is COMPLETE + # (claimed <= K_max = cand_cap) and some rung at or above + # the collect rung counts >= K. The FILTER line is the + # rung with the smallest such count (closest to K from + # above) - monotonicity gives C(collect) >= C(filter) + # >= K, so the filtered set still covers the true top-K + # and is as small as the rung group allows (cheapest P4). + # kC survives only as the physical smem-capacity guard. + filt_t = cutlass.Float32(0.0) + filt_c = cutlass.Int32(2147483647) + filt_j = cutlass.Int32(-1) + for _fj in cutlass.range_constexpr( + cutlass.const_expr(self.cand_rung), + cutlass.const_expr(self.M_thr), + ): + cfj = cutlass.Int32(seed_counts_row[_fj]) + # <= prefers the tightest rung among equal counts + if cfj >= cutlass.Int32(self.top_k) and cfj <= filt_c: + filt_c = cfj + filt_t = seed_thr_row[_fj] + filt_j = cutlass.Int32(_fj) + # Byte-parity routing: the list walk reads 8B/entry on the + # LEADER alone, a fallback re-scan reads 4B/elem split + # across the cluster - a list fatter than N/(2*cs) routes + # to the fallback. Measured both ways (static-threshold + # grid + 160k closed-loop chain): dropping this gate at + # cs=1 turned every fat-list fallback into a slower walk + # (pro 64k 13.5 -> 18.3us), so it stays for all cs. if ( void_c == cutlass.Int32(0) and claimed_c <= cutlass.Int32(self.cand_cap) - and ccnt_c >= cutlass.Int32(self.top_k) - and ccnt_c <= cutlass.Int32(self.kC) + and filt_c <= cutlass.Int32(self.kC) + and (claimed_c + claimed_c) * cutlass.Int32(cluster_size) < N ): take_cand = cutlass.Int32(1) + if cutlass.const_expr(cluster_size == 1): + # ---- Straddle refine: no rung count lands in the + # [K, kC] band (the loosest >=K rung is fatter than + # smem, the next one is under K), yet the list is + # COMPLETE - so instead of falling back to a full + # row re-scan, one 256-bin histogram pass over the + # list values between the two rungs finds a refined + # in-band edge, and the normal filtered load takes + # over. cs>1 keeps the plain fallback: the fire + # decision would need a DSMEM exchange to stay + # cluster-uniform. All gating values are CTA-uniform + # (gmem control words), so the barriers below are + # safe. Runs on ~1 row in 8 on distribution-shift + # steps; those rows otherwise dominate the whole + # launch (per-row max ~30us vs ~15us). + strad = cutlass.Int32(0) + if ( + take_cand == cutlass.Int32(0) + and void_c == cutlass.Int32(0) + and claimed_c <= cutlass.Int32(self.cand_cap) + and filt_j >= cutlass.Int32(0) + and filt_c > cutlass.Int32(self.kC) + and filt_j < cutlass.Int32(self.M_thr - 1) + and (claimed_c + claimed_c) < N + ): + t_thin_s = seed_thr_row[filt_j + cutlass.Int32(1)] + if t_thin_s > filt_t: + strad = cutlass.Int32(1) + if strad == cutlass.Int32(1): + t_fat = filt_t + t_thin = seed_thr_row[filt_j + cutlass.Int32(1)] + c_thin = cutlass.Int32(seed_counts_row[filt_j + cutlass.Int32(1)]) + NBr = cutlass.const_expr(256) + w_r = (t_thin - t_fat) / cutlass.Float32(NBr) + inv_wr = cutlass.Float32(1.0) / w_r + jz_r = tidx + while jz_r < cutlass.Int32(NBr): + smem_hist[jz_r] = cutlass.Int32(0) + jz_r = jz_r + cutlass.Int32(num_threads) + # s_iscalars[2]/[3] (P2's cnt_lo/cnt_hi) are dead + # until P2 re-inits them on the fallback path -> + # free {fired flag, bin index} broadcast slots. + if tidx == cutlass.Int32(0): + s_iscalars[2] = cutlass.Int32(0) + cute.arch.barrier() + cb_r = cand_row.iterator.toint() + i_r = tidx + # ragged exit is fine here: no warp collectives, + # only smem atomics. 4x unroll overlaps the pair + # loads (same latency argument as the loader). + while i_r < claimed_c: + for _jr in cutlass.range_constexpr(4): + i_rj = i_r + cutlass.Int32(_jr * num_threads) + if i_rj < claimed_c: + pa_r = cb_r + cutlass.Int64(i_rj) * cutlass.Int64(8) + vp_r = cute.make_ptr( + cutlass.Float32, + pa_r, + cute.AddressSpace.gmem, + assumed_align=8, + ) + ip_r = cute.make_ptr( + cutlass.Int32, + pa_r + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + v_r = cute.make_tensor(vp_r, cute.make_layout((1,)))[0] + x_r = cute.make_tensor(ip_r, cute.make_layout((1,)))[0] + # only the open band [t_fat, t_thin): + # >= t_thin is already counted by + # c_thin, < t_fat is below the edge. + if x_r >= cutlass.Int32(0) and v_r >= t_fat and v_r < t_thin: + b_r = cutlass.Int32((v_r - t_fat) * inv_wr) + if b_r < cutlass.Int32(0): + b_r = cutlass.Int32(0) + if b_r > cutlass.Int32(NBr - 1): + b_r = cutlass.Int32(NBr - 1) + atomicAdd(smem_hist.iterator + b_r, cutlass.Int32(1)) + i_r = i_r + cutlass.Int32(4 * num_threads) + cute.arch.barrier() + # warp-0 top-down cumulative scan (P1b idiom): + # lane l owns SEG bins descending from NB-1-l*SEG; + # fire at the unique bin where the cumulative + # count (incl. c_thin base) crosses K, admit if + # it also fits smem with a float-edge margin. + if warp_id == cutlass.Int32(0): + SEGr = cutlass.const_expr(8) + top_r = cutlass.Int32(NBr - 1) - lane * cutlass.Int32(SEGr) + seg_r = cute.make_fragment((SEGr,), cutlass.Int32) + part_r = cutlass.Int32(0) + for _js in cutlass.range_constexpr(SEGr): + v8_r = smem_hist[top_r - cutlass.Int32(_js)] + seg_r[_js] = v8_r + part_r = part_r + v8_r + tp_r = part_r + for _os in cutlass.range_constexpr(5): + ov_r = cutlass.const_expr(1 << _os) + oth_r = cute.arch.shuffle_sync_up(tp_r, ov_r, mask_and_clamp=0) + if lane >= cutlass.Int32(ov_r): + tp_r = tp_r + oth_r + excl_r = tp_r - part_r + run_r = cutlass.Int32(0) + kneed = cutlass.Int32(self.top_k) - c_thin + kfit = cutlass.Int32(self.kC - 64) - c_thin + for _js in cutlass.range_constexpr(SEGr): + run_r = run_r + seg_r[_js] + cum_at = excl_r + run_r + cum_bef = cum_at - seg_r[_js] + if cum_bef < kneed and cum_at >= kneed: + if cum_at <= kfit: + s_iscalars[3] = top_r - cutlass.Int32(_js) + s_iscalars[2] = cutlass.Int32(1) + cute.arch.barrier() + if s_iscalars[2] == cutlass.Int32(1): + # every thread recomputes the edge from the + # uniform bin index + filt_t = t_fat + cutlass.Float32(s_iscalars[3]) * w_r + take_cand = cutlass.Int32(1) if take_cand == cutlass.Int32(1): # Cooperative sentinel-skipping load of (value, index) # pairs into the P4 candidate arrays. SMEM-atomic @@ -4902,40 +5049,96 @@ def _run_phases( cute.arch.barrier() if tidx == cutlass.Int32(0): s_iscalars[0] = cutlass.Int32(0) - s_thr[0] = seed_thr_row[self.cand_rung] + s_thr[0] = filt_t s_iscalars[1] = cutlass.Int32(1) # done: no retry cute.arch.barrier() cbase = cand_row.iterator.toint() + lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) i_c = tidx if cutlass.const_expr(cluster_size > 1): if cta_in_cluster != cutlass.Int32(0): - i_c = claimed_c # peers: skip the walk - while i_c < claimed_c: - pa = cbase + cutlass.Int64(i_c) * cutlass.Int64(8) - ip_c = cute.make_ptr( - cutlass.Int32, - pa + cutlass.Int64(4), - cute.AddressSpace.gmem, - assumed_align=4, - ) - pidx = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] - if pidx >= cutlass.Int32(0): - vp_c = cute.make_ptr( - cutlass.Float32, - pa, - cute.AddressSpace.gmem, - assumed_align=8, - ) - pval = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] - wpos = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), - cutlass.Int32(1), - ) - if wpos < cutlass.Int32(self.kC): - smem_keys[wpos] = pval - smem_vals[wpos] = pidx - i_c = i_c + cutlass.Int32(num_threads) + # peers: skip the walk. Must stay warp-uniform + # for the ballot below, so park every lane at + # (claimed + lane) -> warp base == claimed. + i_c = claimed_c + lane_c + # Loop condition on the warp BASE index (uniform across + # the warp): a ragged per-lane exit would strand the + # remaining lanes in vote_ballot_sync (full-warp + # collective) on the tail iteration -> device hang. + # + # 4x-unrolled, latency-overlapped walk. The naive form + # (idx load -> dependent val load -> ballot, one entry + # per thread per trip) costs ~2 serialized cold-DRAM + # round trips per trip; a fat list (C(t_lo) ~ 6-12K) + # then eats ~1.5us/trip (measured: wf 0.64x vs skip at + # 160k). Here all 8 loads (4 entries x {idx,val}) issue + # independently BEFORE the first use, so a trip costs + # ~one round trip for 4x the entries. The val load is + # NOT nested under the sentinel check: sentinel pairs + # are mapped memory, their value is simply ignored. + while (i_c - lane_c) < claimed_c: + pvals = [] + pidxs = [] + for _ju in cutlass.range_constexpr(4): + pval = cutlass.Float32(0.0) + pidx = cutlass.Int32(-1) + i_cj = i_c + cutlass.Int32(_ju * num_threads) + if i_cj < claimed_c: + pa = cbase + cutlass.Int64(i_cj) * cutlass.Int64(8) + vp_c = cute.make_ptr( + cutlass.Float32, + pa, + cute.AddressSpace.gmem, + assumed_align=8, + ) + ip_c = cute.make_ptr( + cutlass.Int32, + pa + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + pval = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] + pidx = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] + pvals.append(pval) + pidxs.append(pidx) + for _ju in cutlass.range_constexpr(4): + keep_c = cutlass.Int32(0) + if pidxs[_ju] >= cutlass.Int32(0): + if pvals[_ju] >= filt_t: + keep_c = cutlass.Int32(1) + # ballot-batched claim (the P4 writeback idiom): + # one smem atomic per warp-trip instead of one + # per survivor. + mask_k = cute.arch.vote_ballot_sync(keep_c != cutlass.Int32(0)) + if mask_k != cutlass.Uint32(0): + nk = cutlass.Int32(cute.arch.popc(mask_k)) + lmk = ( + cutlass.Uint32(1) << cutlass.Uint32(lane_c) + ) - cutlass.Uint32(1) + offk = cutlass.Int32(cute.arch.popc(mask_k & lmk)) + bk = cutlass.Int32(0) + if lane_c == cutlass.Int32(0): + bk = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), nk) + bk = cute.arch.shuffle_sync(bk, cutlass.Int32(0)) + wpos = bk + offk + if keep_c != cutlass.Int32(0) and wpos < cutlass.Int32(self.kC): + smem_keys[wpos] = pvals[_ju] + smem_vals[wpos] = pidxs[_ju] + i_c = i_c + cutlass.Int32(4 * num_threads) cute.arch.barrier() + if cutlass.const_expr(cluster_size == 1): + # Hard net for the refined edge: its count is + # float-boundary approximate. If survivors + # overflowed kC the smem set is truncated + # (value-blind) -> demote to the fallback + # re-scan. Exact-rung filters cannot overflow + # (filt_c <= kC checked); s_iscalars[0] is + # CTA-uniform after the barrier above. + if s_iscalars[0] > cutlass.Int32(self.kC): + take_cand = cutlass.Int32(0) + if tidx == cutlass.Int32(0): + s_iscalars[1] = cutlass.Int32(0) + cute.arch.barrier() if cutlass.const_expr(cluster_size > 1): # leader's local count feeds the DSMEM gather if is_leader: From b11f4a097c3c58f9dae215944b02c3aa82b40575 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 23 Jul 2026 05:25:55 -0700 Subject: [PATCH 018/117] [None][perf] GVR L2: count-only smem-list selection (SoA candidates) Replace the rung-based in-list-filter admission with the count-only scheme: the candidate list is SoA (score column + position column, sentinel score -inf), collected at a single loose line, and admitted purely by entry count (K + 64 <= claimed <= K_max = 24576; the 64 is the emitter sentinel-pad bound, so the live count provably covers K). Rung admission, filter-line selection, straddle refine and the parity gate are all deleted - the seed-count columns are no longer consumed on the list path (the GEMM-side L1 pass becomes deletable, -3.2% emission tax). - THIN list (fits kC): every entry lands AT ITS LIST INDEX in the candidate buffers - no ballots, no smem atomics (128 serialized same-address atomics per trip measured ~1.1us/1k entries), no warp-uniform loop constraint. - FAT list: atomic-free copy of the score column into a dedicated 96KB smem region (sentinels sanitized to t_lo - 1), a zooming smem histogram (3 rounds, NBL^3 resolution - value-linear bins collapse on long-tailed logits) finds an edge whose exact count lands in [K, kC] (lands ~1030 for K=1024), survivors compact with one merged-ballot atomic per warp per trip. The vals slots carry LIST INDICES (no second cold gmem pass over the position column); a post-P4 repair swaps the K winners' positions with fully-parallel gathers. - Closed loop: xstate[1] publishes the exact k-th (output slot K-1 of the rank-ordered scatter), xstate[2] the ~3K-crossing anchor from the round-0 histogram. Host policy picks the anchor field per domain (tight k-th for short/stable rows, wide 3K edge for volatile long rows - the exact-k-th anchor alone shrinks the next down-guard target to 4K and slope noise then undershoots K, forcing ~26us fallbacks). GVR_P4_TAIL_DBG compiles per-phase clock64 stamps into the spare xstate slots. Validation (B200): 24/24 exact across cs=1/4/8 and the straddle- threshold suite on real V4 bundles; per-row cold device phases: thin walk 1.5-3us, fat stage+zoom+compact ~1.1us/1k entries, Phase 4 flat 5.5-6.5us. Real-chain device-level vs the block-skip arm: 640k 1.42x (fallback steps are C(t_lo) < K undershoots - a host anchor-policy matter), 160k 0.93x. Kernel-only chain means trade 5-20% vs the previous rung-based commit at B=8 in exchange for the interface collapse; the deleted L1 emission pass dominates E2E at large batch. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 657 +++++++++++------- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 73 +- 2 files changed, 442 insertions(+), 288 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 3db23dcc045d..7993a773895a 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -27,6 +27,7 @@ """ import math +import os from dataclasses import dataclass from typing import Optional @@ -41,6 +42,11 @@ from ..utils import TRTLLM_ENABLE_PDL, griddepcontrol_launch_dependents, griddepcontrol_wait from .block_scan import warp_scan +# Diagnostic knob: compile per-phase clock64 stamps of the list path +# into the spare xstate slots (harness-side analysis). Off by default; +# NEVER set in production. +_P4_TAIL_DBG = bool(int(os.environ.get("GVR_P4_TAIL_DBG", "0"))) + # --------------------------------------------------------------------------- # DSMEM primitives (inline PTX) @@ -560,6 +566,13 @@ def __init__( # ineligible rows fall through to the ext-counts path. self.use_ext_cand = bool(use_ext_cand) self.cand_cap = int(cand_cap) + # list path: the score column is staged into a DEDICATED smem + # region sized cand_cap fp32 (96KB at 24576). Budget note: this + # coexists with everything except a simultaneously-enabled big + # slice cache (128KB) - that combination exceeds the 227KB CTA + # limit and fails loudly at compile time. Long rows (the list + # path's target) cannot enable the slice cache anyway. + self.list_cap = int(cand_cap) self.cand_rung = int(cand_rung) if use_ext_cand and not use_ext_counts: raise ValueError("use_ext_cand requires use_ext_counts") @@ -3165,6 +3178,10 @@ def phase4_rank_scatter( # UNMODIFIED radix select below (verbatim copy). if cutlass.const_expr(self.p4_exact_tail and self.p4_tail_fast): # [p4tt] need0 = cutlass.Int32(kK) - rank_above_fine + if cutlass.const_expr(_P4_TAIL_DBG): + if tidx == cutlass.Int32(0): + s_thr[1] = cutlass.Float32(cnt_strad) + s_thr[2] = cutlass.Float32(need0) if cnt_strad > need0 and need0 > cutlass.Int32(0): if cnt_strad <= cutlass.Int32(128): # [p4tt] SMEM: (value_bits, cand_idx) pairs at @@ -4140,7 +4157,8 @@ def gvr_topk_kernel( seed_thr: cute.Tensor, # [numRows, 3] fp32 (or None: no ext counts) seed_counts: cute.Tensor, # [numRows, 3] int32 (or None) xstate: cute.Tensor, # [numRows, 8] fp32 closed-loop state (or None) - cand: cute.Tensor, # [numRows, CAP*2] int32 pairs (or None) + cand_vals: cute.Tensor, # [numRows, CAP] fp32 scores (or None) + cand_idx: cute.Tensor, # [numRows, CAP] int32 positions (or None) cand_ctl: cute.Tensor, # [numRows, 2] int32 {claimed, void} (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -4197,7 +4215,8 @@ def gvr_topk_kernel( seed_thr=seed_thr, seed_counts=seed_counts, xstate=xstate, - cand=cand, + cand_vals=cand_vals, + cand_idx=cand_idx, cand_ctl=cand_ctl, ) @@ -4214,7 +4233,8 @@ def run_one_row( seed_thr: cute.Tensor = None, # [numRows, 3] fp32 (ext counts) seed_counts: cute.Tensor = None, # [numRows, 3] int32 (ext counts) xstate: cute.Tensor = None, # [numRows, 8] fp32 (emit_xstate) - cand: cute.Tensor = None, # [numRows, CAP*2] int32 (ext cand) + cand_vals: cute.Tensor = None, # [numRows, CAP] fp32 (ext cand) + cand_idx: cute.Tensor = None, # [numRows, CAP] int32 (ext cand) cand_ctl: cute.Tensor = None, # [numRows, 2] int32 (ext cand) ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -4288,11 +4308,18 @@ def run_one_row( xstate_row = xstate[row_idx, None] else: xstate_row = None - if cutlass.const_expr(self.use_ext_cand and cand is not None and cand_ctl is not None): - cand_row = cand[row_idx, None] + if cutlass.const_expr( + self.use_ext_cand + and cand_vals is not None + and cand_idx is not None + and cand_ctl is not None + ): + cand_vals_row = cand_vals[row_idx, None] + cand_idx_row = cand_idx[row_idx, None] cand_ctl_row = cand_ctl[row_idx, None] else: - cand_row = None + cand_vals_row = None + cand_idx_row = None cand_ctl_row = None # When return_output_values=False, ``output_values`` is None at # launch and the gated writes below are compiled out; slicing into @@ -4437,6 +4464,21 @@ def run_one_row( else: smem_input = None + # List-path score staging (SoA ext cand, cs=1, fp32 only). + if cutlass.const_expr( + self.use_ext_cand + and self.use_ext_counts + and cluster_size == 1 + and self.dtype == cutlass.Float32 + ): + smem_list = smem.allocate_tensor( + element_type=cutlass.Float32, + layout=cute.make_ordered_layout((self.list_cap,), order=(0,)), + byte_alignment=128, + ) + else: + smem_list = None + # op#26 R0 admission scratch (single-CTA fast path). Allocated only # when enable_r0; None otherwise so the base SMEM layout is byte-for- # byte unchanged and these propagate harmlessly through _run_phases' @@ -4593,7 +4635,9 @@ def run_one_row( seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, xstate_row=xstate_row, - cand_row=cand_row, + cand_vals_row=cand_vals_row, + cand_idx_row=cand_idx_row, + smem_list=smem_list, cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, @@ -4642,7 +4686,9 @@ def run_one_row( seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, xstate_row=xstate_row, - cand_row=cand_row, + cand_vals_row=cand_vals_row, + cand_idx_row=cand_idx_row, + smem_list=smem_list, cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, @@ -4688,7 +4734,9 @@ def run_one_row( seed_thr_row=seed_thr_row, seed_counts_row=seed_counts_row, xstate_row=xstate_row, - cand_row=cand_row, + cand_vals_row=cand_vals_row, + cand_idx_row=cand_idx_row, + smem_list=smem_list, cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, @@ -4737,7 +4785,9 @@ def _run_phases( seed_thr_row=None, # ext counts: this row's 3 seed thresholds (fp32) seed_counts_row=None, # ext counts: this row's 3 exact counts (int32) xstate_row=None, # emit_xstate: this row's [8] fp32 state slot - cand_row=None, # ext cand: this row's [CAP*2] int32 pairs + cand_vals_row=None, # ext cand: this row's [CAP] fp32 scores + cand_idx_row=None, # ext cand: this row's [CAP] int32 positions + smem_list=None, # list path: [list_cap] fp32 score staging cand_ctl_row=None, # ext cand: this row's [2] int32 {claimed, void} smem_active=None, s_active_cnt=None, @@ -4771,6 +4821,8 @@ def _run_phases( # misses (pro-1M cold: stock-skip 21us vs ext-miss 51us). All # threads read the same control words, so the predicate is # CTA-uniform and the dynamic branches below stay convergent. + ck0 = cutlass.Int64(0) + ck1 = cutlass.Int64(0) ext_row = cutlass.Int32(0) if cutlass.const_expr(self.use_ext_counts): if seed_thr_row[0] < cutlass.Float32(1e37): @@ -4778,6 +4830,25 @@ def _run_phases( cm_e = cutlass.Int32(seed_counts_row[m]) if cm_e >= cutlass.Int32(self.top_k) and cm_e <= cutlass.Int32(self.kC): ext_row = cutlass.Int32(1) + # list path preview: when the SoA candidate list will be taken + # (count-only admission), Phase 1's gather buys nothing either + # - reuse the same skip (the seed rungs still provide the + # [t_0, t_2] bracket the degenerate check wants). + if cutlass.const_expr( + self.use_ext_cand + and self.use_ext_counts + and cluster_size == 1 + and self.dtype == cutlass.Float32 + ): + claimed_p = cutlass.Int32(cand_ctl_row[0]) + void_p = cutlass.Int32(cand_ctl_row[1]) + if ( + void_p == cutlass.Int32(0) + and claimed_p >= cutlass.Int32(self.top_k) + and claimed_p <= cutlass.Int32(self.list_cap) + and seed_thr_row[0] < cutlass.Float32(1e37) + ): + ext_row = cutlass.Int32(1) # ---- Phase 1: preIdx Min/Max/Mean ---- # ext counts: P1's only surviving products are the [v_lo, v_hi] @@ -4871,280 +4942,288 @@ def _run_phases( if cutlass.const_expr(self.emit_xstate): xstate_row[0] = cutlass.Float32(0.0) # degenerate else: - # ---- Waterfall L2: direct-to-P4 from pre-collected pairs ---- - # Eligibility is a CTA-uniform register predicate (all threads - # read the same gmem control words), so the dynamic branches - # below (with barriers inside) stay convergent. + # ---- List path (SoA): complete candidate list -> on-chip ---- + # Admission is the entry count alone: K <= claimed <= list_cap + # (and no overflow) guarantees the true top-K lives in the + # list (it holds every position >= t_lo = seed_thr[0]). The + # score column is staged into the slice-cache smem region + # (fp32 specs; the fallback re-scan never runs on list rows, + # so the alias is safe), one smem histogram finds an edge + # whose exact count lands in [K, kC], survivors compact into + # the standard kC candidate buffers and the stock Phase 4 + # finishes. All gating values are CTA-uniform (gmem control + # words / smem scalars), so the barriers stay convergent. + # cs>1 and 16-bit dtypes keep the plain fallback. take_cand = cutlass.Int32(0) - if cutlass.const_expr(self.use_ext_cand): + list_used = cutlass.Int32(0) # list path taken (xstate publish) + claimed_c = cutlass.Int32(0) + if cutlass.const_expr( + self.use_ext_cand + and self.use_ext_counts + and cluster_size == 1 + and self.dtype == cutlass.Float32 + ): claimed_c = cutlass.Int32(cand_ctl_row[0]) void_c = cutlass.Int32(cand_ctl_row[1]) - # Two-parameter admission (K, K_max): the list (collected - # at rung cand_rung) is usable whenever it is COMPLETE - # (claimed <= K_max = cand_cap) and some rung at or above - # the collect rung counts >= K. The FILTER line is the - # rung with the smallest such count (closest to K from - # above) - monotonicity gives C(collect) >= C(filter) - # >= K, so the filtered set still covers the true top-K - # and is as small as the rung group allows (cheapest P4). - # kC survives only as the physical smem-capacity guard. - filt_t = cutlass.Float32(0.0) - filt_c = cutlass.Int32(2147483647) - filt_j = cutlass.Int32(-1) - for _fj in cutlass.range_constexpr( - cutlass.const_expr(self.cand_rung), - cutlass.const_expr(self.M_thr), - ): - cfj = cutlass.Int32(seed_counts_row[_fj]) - # <= prefers the tightest rung among equal counts - if cfj >= cutlass.Int32(self.top_k) and cfj <= filt_c: - filt_c = cfj - filt_t = seed_thr_row[_fj] - filt_j = cutlass.Int32(_fj) - # Byte-parity routing: the list walk reads 8B/entry on the - # LEADER alone, a fallback re-scan reads 4B/elem split - # across the cluster - a list fatter than N/(2*cs) routes - # to the fallback. Measured both ways (static-threshold - # grid + 160k closed-loop chain): dropping this gate at - # cs=1 turned every fat-list fallback into a slower walk - # (pro 64k 13.5 -> 18.3us), so it stays for all cs. + # claimed >= K + 64: sentinel slots are bounded by the + # emitter contract (<= 64 pad), so this guarantees the + # LIVE count covers K without counting anything. if ( void_c == cutlass.Int32(0) - and claimed_c <= cutlass.Int32(self.cand_cap) - and filt_c <= cutlass.Int32(self.kC) - and (claimed_c + claimed_c) * cutlass.Int32(cluster_size) < N + and claimed_c >= cutlass.Int32(self.top_k + 64) + and claimed_c <= cutlass.Int32(self.list_cap) ): take_cand = cutlass.Int32(1) - if cutlass.const_expr(cluster_size == 1): - # ---- Straddle refine: no rung count lands in the - # [K, kC] band (the loosest >=K rung is fatter than - # smem, the next one is under K), yet the list is - # COMPLETE - so instead of falling back to a full - # row re-scan, one 256-bin histogram pass over the - # list values between the two rungs finds a refined - # in-band edge, and the normal filtered load takes - # over. cs>1 keeps the plain fallback: the fire - # decision would need a DSMEM exchange to stay - # cluster-uniform. All gating values are CTA-uniform - # (gmem control words), so the barriers below are - # safe. Runs on ~1 row in 8 on distribution-shift - # steps; those rows otherwise dominate the whole - # launch (per-row max ~30us vs ~15us). - strad = cutlass.Int32(0) - if ( - take_cand == cutlass.Int32(0) - and void_c == cutlass.Int32(0) - and claimed_c <= cutlass.Int32(self.cand_cap) - and filt_j >= cutlass.Int32(0) - and filt_c > cutlass.Int32(self.kC) - and filt_j < cutlass.Int32(self.M_thr - 1) - and (claimed_c + claimed_c) < N - ): - t_thin_s = seed_thr_row[filt_j + cutlass.Int32(1)] - if t_thin_s > filt_t: - strad = cutlass.Int32(1) - if strad == cutlass.Int32(1): - t_fat = filt_t - t_thin = seed_thr_row[filt_j + cutlass.Int32(1)] - c_thin = cutlass.Int32(seed_counts_row[filt_j + cutlass.Int32(1)]) - NBr = cutlass.const_expr(256) - w_r = (t_thin - t_fat) / cutlass.Float32(NBr) - inv_wr = cutlass.Float32(1.0) / w_r - jz_r = tidx - while jz_r < cutlass.Int32(NBr): - smem_hist[jz_r] = cutlass.Int32(0) - jz_r = jz_r + cutlass.Int32(num_threads) - # s_iscalars[2]/[3] (P2's cnt_lo/cnt_hi) are dead - # until P2 re-inits them on the fallback path -> - # free {fired flag, bin index} broadcast slots. - if tidx == cutlass.Int32(0): - s_iscalars[2] = cutlass.Int32(0) - cute.arch.barrier() - cb_r = cand_row.iterator.toint() - i_r = tidx - # ragged exit is fine here: no warp collectives, - # only smem atomics. 4x unroll overlaps the pair - # loads (same latency argument as the loader). - while i_r < claimed_c: - for _jr in cutlass.range_constexpr(4): - i_rj = i_r + cutlass.Int32(_jr * num_threads) - if i_rj < claimed_c: - pa_r = cb_r + cutlass.Int64(i_rj) * cutlass.Int64(8) - vp_r = cute.make_ptr( - cutlass.Float32, - pa_r, - cute.AddressSpace.gmem, - assumed_align=8, - ) - ip_r = cute.make_ptr( - cutlass.Int32, - pa_r + cutlass.Int64(4), - cute.AddressSpace.gmem, - assumed_align=4, - ) - v_r = cute.make_tensor(vp_r, cute.make_layout((1,)))[0] - x_r = cute.make_tensor(ip_r, cute.make_layout((1,)))[0] - # only the open band [t_fat, t_thin): - # >= t_thin is already counted by - # c_thin, < t_fat is below the edge. - if x_r >= cutlass.Int32(0) and v_r >= t_fat and v_r < t_thin: - b_r = cutlass.Int32((v_r - t_fat) * inv_wr) - if b_r < cutlass.Int32(0): - b_r = cutlass.Int32(0) - if b_r > cutlass.Int32(NBr - 1): - b_r = cutlass.Int32(NBr - 1) - atomicAdd(smem_hist.iterator + b_r, cutlass.Int32(1)) - i_r = i_r + cutlass.Int32(4 * num_threads) - cute.arch.barrier() - # warp-0 top-down cumulative scan (P1b idiom): - # lane l owns SEG bins descending from NB-1-l*SEG; - # fire at the unique bin where the cumulative - # count (incl. c_thin base) crosses K, admit if - # it also fits smem with a float-edge margin. - if warp_id == cutlass.Int32(0): - SEGr = cutlass.const_expr(8) - top_r = cutlass.Int32(NBr - 1) - lane * cutlass.Int32(SEGr) - seg_r = cute.make_fragment((SEGr,), cutlass.Int32) - part_r = cutlass.Int32(0) - for _js in cutlass.range_constexpr(SEGr): - v8_r = smem_hist[top_r - cutlass.Int32(_js)] - seg_r[_js] = v8_r - part_r = part_r + v8_r - tp_r = part_r - for _os in cutlass.range_constexpr(5): - ov_r = cutlass.const_expr(1 << _os) - oth_r = cute.arch.shuffle_sync_up(tp_r, ov_r, mask_and_clamp=0) - if lane >= cutlass.Int32(ov_r): - tp_r = tp_r + oth_r - excl_r = tp_r - part_r - run_r = cutlass.Int32(0) - kneed = cutlass.Int32(self.top_k) - c_thin - kfit = cutlass.Int32(self.kC - 64) - c_thin - for _js in cutlass.range_constexpr(SEGr): - run_r = run_r + seg_r[_js] - cum_at = excl_r + run_r - cum_bef = cum_at - seg_r[_js] - if cum_bef < kneed and cum_at >= kneed: - if cum_at <= kfit: - s_iscalars[3] = top_r - cutlass.Int32(_js) - s_iscalars[2] = cutlass.Int32(1) - cute.arch.barrier() - if s_iscalars[2] == cutlass.Int32(1): - # every thread recomputes the edge from the - # uniform bin index - filt_t = t_fat + cutlass.Float32(s_iscalars[3]) * w_r - take_cand = cutlass.Int32(1) + if cutlass.const_expr(_P4_TAIL_DBG): + ck0 = cute.arch.clock64() + ck1 = ck0 if take_cand == cutlass.Int32(1): - # Cooperative sentinel-skipping load of (value, index) - # pairs into the P4 candidate arrays. SMEM-atomic - # compaction: order is irrelevant to rank-scatter. - # cs > 1: the pre-collected pairs are O(cand_count) — - # row splitting buys nothing, so the LEADER loads them - # alone and peers publish zero local candidates for - # the DSMEM gather (take_cand is cluster-uniform: all - # CTAs read the same per-row control words). - if cutlass.const_expr(cluster_size > 1): - if tidx == cutlass.Int32(0): - s_iscalars[5] = cutlass.Int32(0) - cute.arch.barrier() - if tidx == cutlass.Int32(0): - s_iscalars[0] = cutlass.Int32(0) - s_thr[0] = filt_t - s_iscalars[1] = cutlass.Int32(1) # done: no retry - cute.arch.barrier() - cbase = cand_row.iterator.toint() + t_lo = seed_thr_row[0] lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) - i_c = tidx - if cutlass.const_expr(cluster_size > 1): - if cta_in_cluster != cutlass.Int32(0): - # peers: skip the walk. Must stay warp-uniform - # for the ballot below, so park every lane at - # (claimed + lane) -> warp base == claimed. - i_c = claimed_c + lane_c - # Loop condition on the warp BASE index (uniform across - # the warp): a ragged per-lane exit would strand the - # remaining lanes in vote_ballot_sync (full-warp - # collective) on the tail iteration -> device hang. - # - # 4x-unrolled, latency-overlapped walk. The naive form - # (idx load -> dependent val load -> ballot, one entry - # per thread per trip) costs ~2 serialized cold-DRAM - # round trips per trip; a fat list (C(t_lo) ~ 6-12K) - # then eats ~1.5us/trip (measured: wf 0.64x vs skip at - # 160k). Here all 8 loads (4 entries x {idx,val}) issue - # independently BEFORE the first use, so a trip costs - # ~one round trip for 4x the entries. The val load is - # NOT nested under the sentinel check: sentinel pairs - # are mapped memory, their value is simply ignored. - while (i_c - lane_c) < claimed_c: - pvals = [] - pidxs = [] + vbase = cand_vals_row.iterator.toint() + NBL = cutlass.const_expr(self.kNumBins) + # ---- 1) stage: pure copy of the score column into + # the dedicated smem region. No ballots, no atomics + # (128 serialized smem atomics per trip measured + # ~1.1us/1k entries), no warp-uniform constraint. + # Sentinels sanitize to t_lo - 1 (rank below all live + # scores; the K+64 admission bound covers K). + list_used = cutlass.Int32(1) + if tidx == cutlass.Int32(0): + s_iscalars[2] = cutlass.Int32(0) + snt = t_lo - cutlass.Float32(1.0) + lmax = cutlass.Float32(self.NEG_FLT_MAX) + i_s = tidx + while i_s < claimed_c: for _ju in cutlass.range_constexpr(4): - pval = cutlass.Float32(0.0) - pidx = cutlass.Int32(-1) - i_cj = i_c + cutlass.Int32(_ju * num_threads) - if i_cj < claimed_c: - pa = cbase + cutlass.Int64(i_cj) * cutlass.Int64(8) - vp_c = cute.make_ptr( + i_sj = i_s + cutlass.Int32(_ju * num_threads) + if i_sj < claimed_c: + vp_l = cute.make_ptr( cutlass.Float32, - pa, - cute.AddressSpace.gmem, - assumed_align=8, - ) - ip_c = cute.make_ptr( - cutlass.Int32, - pa + cutlass.Int64(4), + vbase + cutlass.Int64(i_sj) * cutlass.Int64(4), cute.AddressSpace.gmem, assumed_align=4, ) - pval = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] - pidx = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] - pvals.append(pval) - pidxs.append(pidx) - for _ju in cutlass.range_constexpr(4): - keep_c = cutlass.Int32(0) - if pidxs[_ju] >= cutlass.Int32(0): - if pvals[_ju] >= filt_t: - keep_c = cutlass.Int32(1) - # ballot-batched claim (the P4 writeback idiom): - # one smem atomic per warp-trip instead of one - # per survivor. - mask_k = cute.arch.vote_ballot_sync(keep_c != cutlass.Int32(0)) - if mask_k != cutlass.Uint32(0): - nk = cutlass.Int32(cute.arch.popc(mask_k)) - lmk = ( - cutlass.Uint32(1) << cutlass.Uint32(lane_c) - ) - cutlass.Uint32(1) - offk = cutlass.Int32(cute.arch.popc(mask_k & lmk)) - bk = cutlass.Int32(0) + v_l = cute.make_tensor(vp_l, cute.make_layout((1,)))[0] + if v_l < t_lo: + v_l = snt + smem_list[i_sj] = v_l + lmax = cute.arch.fmax(lmax, v_l) + i_s = i_s + cutlass.Int32(4 * num_threads) + wmax_l = self.warp_reduce_max_f32(lmax) + if lane == cutlass.Int32(0): + smem_wmax[warp_id] = wmax_l + cute.arch.barrier() + # ---- 2) zooming histogram on the smem scores: find + # the edge whose exact count lands in [K, kC], as + # close to K as the bins allow. Value-linear bins + # collapse on long-tailed logits (vmax is an extreme + # outlier), so each round re-bins INSIDE the crossing + # bin - one cheap smem pass per round, NBL^3 total + # resolution; only genuine ties fall through. + vmax = cutlass.Float32(self.NEG_FLT_MAX) + for _wr in cutlass.range_constexpr(self.num_warps): + vmax = cute.arch.fmax(vmax, smem_wmax[_wr]) + rng_l = vmax - t_lo + if rng_l <= cutlass.Float32(0.0): + rng_l = cutlass.Float32(1.0) + r_lo = t_lo + r_w = rng_l / cutlass.Float32(NBL) + w0 = r_w # round-0 bin width (anchor edge recompute) + base_c = cutlass.Int32(0) + t_star = t_lo + searching = cutlass.Int32(1) + if tidx == cutlass.Int32(0): + # smem_wcnt[1]: round-0 anchor bin (the ~3K + # crossing; [0] is the descend base). -1 = not + # found -> anchor falls back to t_star. P4 + # clobbers smem_wcnt only AFTER we read this. + smem_wcnt[1] = cutlass.Int32(-1) + for _rd in cutlass.range_constexpr(3): + if searching == cutlass.Int32(1): + jz_l = tidx + while jz_l < cutlass.Int32(NBL): + smem_hist[jz_l] = cutlass.Int32(0) + jz_l = jz_l + cutlass.Int32(num_threads) + if tidx == cutlass.Int32(0): + s_iscalars[2] = cutlass.Int32(0) + cute.arch.barrier() + inv_wr = cutlass.Float32(1.0) / r_w + r_hi = r_lo + r_w * cutlass.Float32(NBL) + i_h = tidx + while i_h < claimed_c: + v_h = smem_list[i_h] + if v_h >= r_lo and v_h < r_hi: + b_h = cutlass.Int32((v_h - r_lo) * inv_wr) + if b_h < cutlass.Int32(0): + b_h = cutlass.Int32(0) + if b_h > cutlass.Int32(NBL - 1): + b_h = cutlass.Int32(NBL - 1) + atomicAdd(smem_hist.iterator + b_h, cutlass.Int32(1)) + i_h = i_h + cutlass.Int32(num_threads) + cute.arch.barrier() + # warp-0 top-down cumulative scan (P1b idiom) + if warp_id == cutlass.Int32(0): + SEGL = cutlass.const_expr(NBL // self.WARP_SIZE) + top_l = cutlass.Int32(NBL - 1) - lane * cutlass.Int32(SEGL) + seg_l = cute.make_fragment((SEGL,), cutlass.Int32) + part_l = cutlass.Int32(0) + for _js in cutlass.range_constexpr(SEGL): + v8_l = smem_hist[top_l - cutlass.Int32(_js)] + seg_l[_js] = v8_l + part_l = part_l + v8_l + tp_l = part_l + for _os in cutlass.range_constexpr(5): + ov_l = cutlass.const_expr(1 << _os) + oth_l = cute.arch.shuffle_sync_up(tp_l, ov_l, mask_and_clamp=0) + if lane >= cutlass.Int32(ov_l): + tp_l = tp_l + oth_l + excl_l = tp_l - part_l + kneed = cutlass.Int32(self.top_k) - base_c + kfit = cutlass.Int32(self.kC) - base_c + run_l = cutlass.Int32(0) + for _js in cutlass.range_constexpr(SEGL): + run_l = run_l + seg_l[_js] + cum_at = excl_l + run_l + cum_bef = cum_at - seg_l[_js] + if cum_bef < kneed and cum_at >= kneed: + s_iscalars[3] = top_l - cutlass.Int32(_js) + smem_wcnt[0] = cum_bef + if cum_at <= kfit: + s_iscalars[2] = cutlass.Int32(1) + else: + s_iscalars[2] = cutlass.Int32(2) + if cutlass.const_expr(_rd == 0): + # closed-loop ANCHOR: the ~3K + # crossing edge. Publishing the + # exact k-th made the next-step + # down-guard target only 4K - + # slope noise then undershoots K + # and forces ~26us fallbacks; a + # 3K-count anchor restores the + # old edge semantics (~12K guard + # target) at zero extra passes. + anch_n = cutlass.const_expr( + min(3 * self.top_k, (self.top_k + self.kC) // 2) + ) + if cum_bef < cutlass.Int32( + anch_n + ) and cum_at >= cutlass.Int32(anch_n): + smem_wcnt[1] = top_l - cutlass.Int32(_js) + cute.arch.barrier() + st_l = s_iscalars[2] + if st_l == cutlass.Int32(1): + t_star = r_lo + cutlass.Float32(s_iscalars[3]) * r_w + searching = cutlass.Int32(0) + if st_l == cutlass.Int32(2): + base_c = base_c + smem_wcnt[0] + r_lo = r_lo + cutlass.Float32(s_iscalars[3]) * r_w + r_w = r_w / cutlass.Float32(NBL) + if r_w <= cutlass.Float32(0.0): + searching = cutlass.Int32(0) + if st_l == cutlass.Int32(0): + searching = cutlass.Int32(0) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + if s_iscalars[2] != cutlass.Int32(1): + s_iscalars[2] = cutlass.Int32(0) + cute.arch.barrier() + fired_l = s_iscalars[2] + t_anch = t_star + anch_b = smem_wcnt[1] + if anch_b >= cutlass.Int32(0): + t_anch = t_lo + cutlass.Float32(anch_b) * w0 + if fired_l == cutlass.Int32(1): + # ---- 3) compact survivors into the standard kC + # buffers: score from smem, position streamed + # from the gmem index column. The four unrolled + # sub-ballots merge into ONE atomic per warp per + # trip (the per-sub-ballot atomic serialized 128 + # adds per trip across 32 warps). + if tidx == cutlass.Int32(0): + s_iscalars[0] = cutlass.Int32(0) + s_thr[0] = t_anch # closed-loop anchor + s_iscalars[1] = cutlass.Int32(1) # done + cute.arch.barrier() + i_c = tidx + while (i_c - lane_c) < claimed_c: + pvals = [] + pidxs = [] + keeps = [] + for _ju in cutlass.range_constexpr(4): + i_cj = i_c + cutlass.Int32(_ju * num_threads) + pval = cutlass.Float32(self.NEG_FLT_MAX) + # vals slot carries the LIST INDEX - a + # register, not a second (cold) gmem pass + # over the position column; the post-P4 + # repair gathers true positions for the K + # winners only. + pidx = i_cj + keep = cutlass.Int32(0) + if i_cj < claimed_c: + pval = smem_list[i_cj] + if pval >= t_star: + keep = cutlass.Int32(1) + pvals.append(pval) + pidxs.append(pidx) + keeps.append(keep) + m0 = cute.arch.vote_ballot_sync(keeps[0] != cutlass.Int32(0)) + m1 = cute.arch.vote_ballot_sync(keeps[1] != cutlass.Int32(0)) + m2 = cute.arch.vote_ballot_sync(keeps[2] != cutlass.Int32(0)) + m3 = cute.arch.vote_ballot_sync(keeps[3] != cutlass.Int32(0)) + nk = cutlass.Int32( + cute.arch.popc(m0) + + cute.arch.popc(m1) + + cute.arch.popc(m2) + + cute.arch.popc(m3) + ) + bk = cutlass.Int32(0) + if nk > cutlass.Int32(0): if lane_c == cutlass.Int32(0): bk = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), nk) bk = cute.arch.shuffle_sync(bk, cutlass.Int32(0)) - wpos = bk + offk - if keep_c != cutlass.Int32(0) and wpos < cutlass.Int32(self.kC): - smem_keys[wpos] = pvals[_ju] - smem_vals[wpos] = pidxs[_ju] - i_c = i_c + cutlass.Int32(4 * num_threads) - cute.arch.barrier() - if cutlass.const_expr(cluster_size == 1): - # Hard net for the refined edge: its count is - # float-boundary approximate. If survivors - # overflowed kC the smem set is truncated - # (value-blind) -> demote to the fallback - # re-scan. Exact-rung filters cannot overflow - # (filt_c <= kC checked); s_iscalars[0] is - # CTA-uniform after the barrier above. - if s_iscalars[0] > cutlass.Int32(self.kC): + lmk = ( + cutlass.Uint32(1) << cutlass.Uint32(lane_c) + ) - cutlass.Uint32(1) + off = bk + for _ju in cutlass.range_constexpr(4): + mj = ( + m0 + if _ju == 0 + else m1 + if _ju == 1 + else m2 + if _ju == 2 + else m3 + ) + if keeps[_ju] != cutlass.Int32(0): + wpos = off + cutlass.Int32(cute.arch.popc(mj & lmk)) + if wpos < cutlass.Int32(self.kC): + smem_keys[wpos] = pvals[_ju] + smem_vals[wpos] = pidxs[_ju] + off = off + cutlass.Int32(cute.arch.popc(mj)) + i_c = i_c + cutlass.Int32(4 * num_threads) + cute.arch.barrier() + # hard net: histogram binning and the compact + # test round float edges independently + cnt_l = s_iscalars[0] + if cnt_l < cutlass.Int32(self.top_k) or cnt_l > cutlass.Int32(self.kC): take_cand = cutlass.Int32(0) + list_used = cutlass.Int32(0) if tidx == cutlass.Int32(0): s_iscalars[1] = cutlass.Int32(0) + s_iscalars[2] = cutlass.Int32(-1) + s_iscalars[3] = cutlass.Int32(-1) cute.arch.barrier() - if cutlass.const_expr(cluster_size > 1): - # leader's local count feeds the DSMEM gather - if is_leader: - if tidx == cutlass.Int32(0): - s_iscalars[5] = s_iscalars[0] + if fired_l == cutlass.Int32(0): + take_cand = cutlass.Int32(0) + list_used = cutlass.Int32(0) + if tidx == cutlass.Int32(0): + s_iscalars[2] = cutlass.Int32(-1) + s_iscalars[3] = cutlass.Int32(-1) cute.arch.barrier() + if cutlass.const_expr(_P4_TAIL_DBG): + ck1 = cute.arch.clock64() if take_cand == cutlass.Int32(0): # Stage this CTA's slice into SMEM once before Phase 2's # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses @@ -5567,6 +5646,9 @@ def _run_phases( # Pre-init cand_count_p4 so CuTe DSL sees a stable Int32 type # across the runtime ``if is_leader:`` branch in cs>1 mode # (DSL forbids first-assigning a variable inside a dynamic if). + ck2 = cutlass.Int64(0) + if cutlass.const_expr(_P4_TAIL_DBG): + ck2 = cute.arch.clock64() cand_count_p4 = cutlass.Int32(0) if cutlass.const_expr(cluster_size == 1): # cs=1: the single CTA per row IS the leader. @@ -5601,6 +5683,31 @@ def _run_phases( warp_id, lane, ) + if cutlass.const_expr( + self.use_ext_cand and self.use_ext_counts and self.dtype == cutlass.Float32 + ): + # List rows: the compact stored LIST INDICES in the + # vals slots (saving a second cold gmem pass over the + # position column). Swap them for true positions with + # K fully-parallel gathers. Must precede the xstate + # publish, which reads output slot K-1 as a position. + if list_used == cutlass.Int32(1): + io_r = tidx + while io_r < cutlass.Int32(self.top_k): + li_r = output_indices_row[io_r] + if li_r >= cutlass.Int32(0) and li_r < claimed_c: + ip_r = cute.make_ptr( + cutlass.Int32, + cand_idx_row.iterator.toint() + + cutlass.Int64(li_r) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + output_indices_row[io_r] = cute.make_tensor( + ip_r, cute.make_layout((1,)) + )[0] + io_r = io_r + cutlass.Int32(num_threads) + cute.arch.barrier() if cutlass.const_expr(self.emit_xstate): # Closed-loop state (interface v2): [0] valid, [1] kth # proxy (= accepted threshold; the tie-fill makes it a @@ -5609,8 +5716,24 @@ def _run_phases( # seed rung group from these. if tidx == cutlass.Int32(0): xstate_row[0] = cutlass.Float32(1.0) - xstate_row[1] = s_thr[0] - xstate_row[2] = s_thr[0] + thr_pub = s_thr[0] + anch_pub = s_thr[0] + if list_used == cutlass.Int32(1): + # list rows: rank-scatter's output is rank- + # ordered, so slot K-1 holds the exact k-th + # boundary - a tighter, healthier closed-loop + # anchor than the loose collect line. + idx_k = output_indices_row[cutlass.Int32(self.top_k - 1)] + if idx_k >= cutlass.Int32(0) and idx_k < N: + thr_pub = cutlass.Float32(input_row[idx_k]) + xstate_row[1] = thr_pub + xstate_row[2] = anch_pub + if cutlass.const_expr(_P4_TAIL_DBG): + ck3 = cute.arch.clock64() + xstate_row[4] = cutlass.Float32(cutlass.Int32(ck1 - ck0)) # walk+flags + xstate_row[5] = cutlass.Float32(cutlass.Int32(ck2 - ck1)) # P2/P3 gap + xstate_row[6] = cutlass.Float32(cutlass.Int32(ck3 - ck2)) # Phase 4 + xstate_row[7] = s_thr[1] # cnt_strad # cand_count_p4 = pre-P4 snapshot (P4 repurposes # the s_iscalars slots). xstate_row[3] = cutlass.Float32(cand_count_p4) @@ -5726,7 +5849,8 @@ def __call__( seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (ext counts) seed_counts: cute.Tensor = None, # [num_rows, 3] int32 (ext counts) xstate: cute.Tensor = None, # [num_rows, 8] fp32 (emit_xstate) - cand: cute.Tensor = None, # [num_rows, CAP*2] int32 (ext cand) + cand_vals: cute.Tensor = None, # [num_rows, CAP] fp32 (ext cand) + cand_idx: cute.Tensor = None, # [num_rows, CAP] int32 (ext cand) cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 (ext cand) ): num_rows = input_data.shape[0] @@ -5753,7 +5877,8 @@ def __call__( seed_thr, seed_counts, xstate, - cand, + cand_vals, + cand_idx, cand_ctl, ).launch( grid=(total_ctas, 1, 1), diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index ed79a19cbb23..f464f01a2031 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -151,9 +151,16 @@ def _compile( if use_ext_counts else None ) - cand_fake = ( + cand_vals_fake = ( cute.runtime.make_fake_compact_tensor( - cutlass.Int32, (n_rows, cand_cap * 2), stride_order=(1, 0), assumed_align=8 + cutlass.Float32, (n_rows, cand_cap), stride_order=(1, 0), assumed_align=4 + ) + if use_ext_cand + else None + ) + cand_idx_fake = ( + cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, cand_cap), stride_order=(1, 0), assumed_align=4 ) if use_ext_cand else None @@ -213,7 +220,8 @@ def _compile( seed_thr=seed_thr_fake, seed_counts=seed_counts_fake, xstate=xstate_fake, - cand=cand_fake, + cand_vals=cand_vals_fake, + cand_idx=cand_idx_fake, cand_ctl=cand_ctl_fake, options="--enable-tvm-ffi", ) @@ -351,6 +359,7 @@ def derive_seed_rungs( prev_counts: "torch.Tensor | None" = None, count_octaves: float = 2.0, fallback_spread: float = 0.5, + top_k: "int | None" = None, ) -> torch.Tensor: """Host-side slope-adaptive seed rung derivation (waterfall closed loop). @@ -370,6 +379,7 @@ def derive_seed_rungs( """ if prev_sthr is None or prev_counts is None: d = torch.full_like(prev_thr, fallback_spread) + d_lo = d else: c_lo = prev_counts[:, 0].float().clamp(min=1.0) c_hi = prev_counts[:, 2].float().clamp(min=1.0) @@ -380,7 +390,19 @@ def derive_seed_rungs( count_octaves / slope.clamp(min=0.05), torch.full_like(slope, fallback_spread), ).clamp(0.1, 4.0) - return torch.stack([prev_thr - d, prev_thr, prev_thr + d], dim=1).contiguous() + # undershoot hysteresis: a row whose PREVIOUS loose rung caught + # fewer than K cannot use its list (fallback ~26us); widen only + # that row's next down-guard by +2 octaves. Rows in band keep the + # tight spread (a fat list taxes every step's walk). + oct_lo = torch.full_like(slope, count_octaves) + if top_k is not None: + oct_lo = torch.where(prev_counts[:, 0].float() < float(top_k), oct_lo + 2.0, oct_lo) + d_lo = torch.where( + slope > 0.05, + oct_lo / slope.clamp(min=0.05), + torch.full_like(slope, fallback_spread), + ).clamp(0.1, 6.0) + return torch.stack([prev_thr - d_lo, prev_thr, prev_thr + d], dim=1).contiguous() def emu_seed_counts( @@ -414,8 +436,8 @@ def emu_cand( next_n: int = 1, compress_ratio: int = 1, sentinel_pad: int = 0, -) -> tuple[torch.Tensor, torch.Tensor]: - """L2 emu: unordered candidate pre-collect. +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """L2 emu: unordered candidate pre-collect (SoA). Unordered (value fp32-bits, index) pairs of all valid positions >= t_0 = seed_thr[:, 0]; ctl = {claimed, void}. claimed may @@ -428,9 +450,9 @@ def emu_cand( dev = logits.device n_eff = _row_n_eff(seq_lens, R, next_n, compress_ratio) lf = logits.to(torch.float32) - cand = torch.full((R, cap * 2), -1, dtype=torch.int32, device=dev) + cand_vals = torch.full((R, cap), float("-inf"), dtype=torch.float32, device=dev) + cand_idx = torch.full((R, cap), -1, dtype=torch.int32, device=dev) ctl = torch.zeros((R, 2), dtype=torch.int32, device=dev) - pairs = cand.view(R, cap, 2) for r in range(R): ne = int(n_eff[r]) hits = torch.nonzero(lf[r, :ne] >= seed_thr[r, 0], as_tuple=False).flatten() @@ -447,11 +469,11 @@ def emu_cand( claimed = int(ent.numel()) nwr = min(claimed, cap) live = ent[:nwr] >= 0 - pairs[r, :nwr, 1] = ent[:nwr].to(torch.int32) - pairs[r, :nwr, 0][live] = lf[r, ent[:nwr][live]].view(torch.int32) + cand_idx[r, :nwr] = ent[:nwr].to(torch.int32) + cand_vals[r, :nwr][live] = lf[r, ent[:nwr][live]] ctl[r, 0] = claimed ctl[r, 1] = 1 if claimed > cap else 0 - return cand, ctl + return cand_vals, cand_idx, ctl def enc_ordered_f32(t: torch.Tensor) -> torch.Tensor: @@ -508,7 +530,8 @@ def gvr_topk_decode( seed_thr: Optional[torch.Tensor] = None, seed_counts: Optional[torch.Tensor] = None, xstate: Optional[torch.Tensor] = None, - cand: Optional[torch.Tensor] = None, + cand_vals: Optional[torch.Tensor] = None, + cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -606,24 +629,29 @@ def gvr_topk_decode( and seed_counts.is_contiguous() and seed_counts.shape == (num_rows, 3) ), "seed_counts must be contiguous CUDA int32 [num_rows, 3]" - use_ext_cand = cand is not None and cand_ctl is not None + use_ext_cand = cand_vals is not None and cand_idx is not None and cand_ctl is not None cand_cap = 5120 if use_ext_cand: assert ( - cand.dtype == torch.int32 - and cand.is_cuda - and cand.is_contiguous() - and cand.dim() == 2 - and cand.shape[0] == num_rows - and cand.shape[1] % 2 == 0 - ), "cand must be contiguous CUDA int32 [num_rows, CAP*2]" + cand_vals.dtype == torch.float32 + and cand_vals.is_cuda + and cand_vals.is_contiguous() + and cand_vals.dim() == 2 + and cand_vals.shape[0] == num_rows + ), "cand_vals must be contiguous CUDA fp32 [num_rows, CAP]" + assert ( + cand_idx.dtype == torch.int32 + and cand_idx.is_cuda + and cand_idx.is_contiguous() + and cand_idx.shape == cand_vals.shape + ), "cand_idx must be contiguous CUDA int32 [num_rows, CAP]" assert ( cand_ctl.dtype == torch.int32 and cand_ctl.is_cuda and cand_ctl.is_contiguous() and cand_ctl.shape == (num_rows, 2) ), "cand_ctl must be contiguous CUDA int32 [num_rows, 2]" - cand_cap = cand.shape[1] // 2 + cand_cap = cand_vals.shape[1] emit_xstate = xstate is not None if emit_xstate: assert ( @@ -730,7 +758,8 @@ def gvr_topk_decode( seed_thr if use_ext_counts else None, seed_counts if use_ext_counts else None, xstate if emit_xstate else None, - cand if use_ext_cand else None, + cand_vals if use_ext_cand else None, + cand_idx if use_ext_cand else None, cand_ctl if use_ext_cand else None, ) if return_output_values: From 56775417d4366a89cc04a14afcc6f301283f1d10 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:46:59 -0700 Subject: [PATCH 019/117] [None][perf] GVR L2 v4: emitter-counted lines, known-counts admission The emitter (indexer GEMM epilogue; emulated host-side in the bench harness) now counts the two tighter lines while writing the SoA list - two extra compares per EMITTED element only, against the full-row L1 pass this replaces - and the control words widen to {n0, void, n1, n2}. The topk side enters with every count known and the whole list path collapses to a scalar state machine: - some line's count lands in the acceptance band [K, B*]: cut at the TIGHTEST such line, ONE filtered gmem pass straight into the candidate buffers (positions deferred as list indices; the position column is gathered only for the K winners after Phase 4). Counts and load predicates are the same comparison, so line cuts need no overflow net at all. - the band is straddled or overshot by every line: a zooming histogram over the gmem list CLAMPED between the two known bracket lines finds an in-band edge (narrow domain - no long-tail bin collapse; the all-above case takes one max pass first). - void, or n0 < K + 64 (the emitter sentinel bound, proving live coverage of K): fallback. The dedicated smem staging region is deleted (frees 96-128KB; the kernel's smem drops back to the pre-list footprint), and B* / kC become constructor knobs (accept_cap, kc_override) for the band search. Closed loop publishes the exact k-th (rank-ordered output slot K-1) and the loosest in-band line as the anchor. Line placement is a searched host policy (derive_seed_lines_v4): count targets (t0, t1, t2), grid-searched on real chains = (4096, 3584, 1536) for short domains / (12288, 5120, 2048) for long; physical kC stays 5120 (8192 measured no gain). Validation (B200): five admission states each exercised exact (hit-t2/hit-t1/bracketed-histogram/above-t2-histogram/fallback), 24/24 exact at cs=1/4/8; real-chain device-level vs the block-skip arm: 160k 1.09x (first config of this lineage to beat the rung-based 1.05x), 640k 1.39x (residual: 2-3 volatile rows/step whose collection count escapes any placement - a host anchor-policy iteration item). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 532 +++++++++--------- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 60 +- 2 files changed, 320 insertions(+), 272 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 7993a773895a..a2e64640704c 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -264,6 +264,8 @@ def __init__( seqlen_sorted: bool = False, kc_diet: Optional[bool] = None, enable_r0: bool = True, + accept_cap: "int | None" = None, + kc_override: "int | None" = None, r0_qfracs: Optional[tuple] = None, mt_unroll: int = 4, p1b_cache: Optional[bool] = None, @@ -565,6 +567,13 @@ def __init__( # <= cand_cap and the collect rung's exact count is in [K, kC]; # ineligible rows fall through to the ext-counts path. self.use_ext_cand = bool(use_ext_cand) + if kc_override is not None: + # physical candidate-buffer capacity override (B* search) + self.kC = int(kc_override) + # acceptance band top B*: a cut whose count fits [K, B*] goes + # straight to Phase 4. Cost-bounded (refine ~1.9us/1k cands vs + # ~21-30us fallback), physically bounded by kC. + self.accept_cap = int(accept_cap) if accept_cap is not None else self.kC self.cand_cap = int(cand_cap) # list path: the score column is staged into a DEDICATED smem # region sized cand_cap fp32 (96KB at 24576). Budget note: this @@ -4464,21 +4473,6 @@ def run_one_row( else: smem_input = None - # List-path score staging (SoA ext cand, cs=1, fp32 only). - if cutlass.const_expr( - self.use_ext_cand - and self.use_ext_counts - and cluster_size == 1 - and self.dtype == cutlass.Float32 - ): - smem_list = smem.allocate_tensor( - element_type=cutlass.Float32, - layout=cute.make_ordered_layout((self.list_cap,), order=(0,)), - byte_alignment=128, - ) - else: - smem_list = None - # op#26 R0 admission scratch (single-CTA fast path). Allocated only # when enable_r0; None otherwise so the base SMEM layout is byte-for- # byte unchanged and these propagate harmlessly through _run_phases' @@ -4637,7 +4631,6 @@ def run_one_row( xstate_row=xstate_row, cand_vals_row=cand_vals_row, cand_idx_row=cand_idx_row, - smem_list=smem_list, cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, @@ -4688,7 +4681,6 @@ def run_one_row( xstate_row=xstate_row, cand_vals_row=cand_vals_row, cand_idx_row=cand_idx_row, - smem_list=smem_list, cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, @@ -4736,7 +4728,6 @@ def run_one_row( xstate_row=xstate_row, cand_vals_row=cand_vals_row, cand_idx_row=cand_idx_row, - smem_list=smem_list, cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, @@ -4787,7 +4778,6 @@ def _run_phases( xstate_row=None, # emit_xstate: this row's [8] fp32 state slot cand_vals_row=None, # ext cand: this row's [CAP] fp32 scores cand_idx_row=None, # ext cand: this row's [CAP] int32 positions - smem_list=None, # list path: [list_cap] fp32 score staging cand_ctl_row=None, # ext cand: this row's [2] int32 {claimed, void} smem_active=None, s_active_cnt=None, @@ -4844,7 +4834,7 @@ def _run_phases( void_p = cutlass.Int32(cand_ctl_row[1]) if ( void_p == cutlass.Int32(0) - and claimed_p >= cutlass.Int32(self.top_k) + and claimed_p >= cutlass.Int32(self.top_k + 64) and claimed_p <= cutlass.Int32(self.list_cap) and seed_thr_row[0] < cutlass.Float32(1e37) ): @@ -4942,17 +4932,24 @@ def _run_phases( if cutlass.const_expr(self.emit_xstate): xstate_row[0] = cutlass.Float32(0.0) # degenerate else: - # ---- List path (SoA): complete candidate list -> on-chip ---- - # Admission is the entry count alone: K <= claimed <= list_cap - # (and no overflow) guarantees the true top-K lives in the - # list (it holds every position >= t_lo = seed_thr[0]). The - # score column is staged into the slice-cache smem region - # (fp32 specs; the fallback re-scan never runs on list rows, - # so the alias is safe), one smem histogram finds an edge - # whose exact count lands in [K, kC], survivors compact into - # the standard kC candidate buffers and the stock Phase 4 - # finishes. All gating values are CTA-uniform (gmem control - # words / smem scalars), so the barriers stay convergent. + # ---- List path v4: known-counts admission ---- + # The emitter wrote the SoA list (score column + position + # column, sentinel score -inf) collected at t0 = seed_thr[0] + # and COUNTED the two tighter lines on the way out: the + # control words carry {n0, void, n1, n2} with n_i = #(>= t_i), + # n0 >= n1 >= n2. Admission and cut selection are pure scalar + # lookups - no in-kernel counting, no staging, no gamble: + # 1. some n_i lands in the acceptance band [K, B*] -> + # cut at the TIGHTEST such line, ONE filtered pass. + # 2. the band is straddled or overshot by every line -> + # a histogram over the gmem list CLAMPED between the + # two known bracket lines finds an in-band edge (the + # narrow domain kills the long-tail bin collapse). + # 3. void, or n0 < K + 64 (64 = emitter sentinel bound, + # so live coverage of K is proven) -> fallback. + # The K+64 slack also lets every accepted cut load run + # WITHOUT any overflow net: counts and load predicates are + # the same comparison on the same data. # cs>1 and 16-bit dtypes keep the plain fallback. take_cand = cutlass.Int32(0) list_used = cutlass.Int32(0) # list path taken (xstate publish) @@ -4965,265 +4962,264 @@ def _run_phases( ): claimed_c = cutlass.Int32(cand_ctl_row[0]) void_c = cutlass.Int32(cand_ctl_row[1]) - # claimed >= K + 64: sentinel slots are bounded by the - # emitter contract (<= 64 pad), so this guarantees the - # LIVE count covers K without counting anything. + n1_c = cutlass.Int32(cand_ctl_row[2]) + n2_c = cutlass.Int32(cand_ctl_row[3]) + bstar = cutlass.const_expr(min(self.accept_cap, self.kC)) + if cutlass.const_expr(_P4_TAIL_DBG): + ck0 = cute.arch.clock64() + ck1 = ck0 + usable = cutlass.Int32(0) if ( void_c == cutlass.Int32(0) and claimed_c >= cutlass.Int32(self.top_k + 64) and claimed_c <= cutlass.Int32(self.list_cap) ): + usable = cutlass.Int32(1) + # pre-declared: the DSL forbids first-assigning inside a + # dynamic if when read outside it + kK_l = cutlass.Int32(self.top_k) + bs_l = cutlass.Int32(bstar) + cut_t = cutlass.Float32(0.0) + have = cutlass.Int32(0) + anch_t = cutlass.Float32(0.0) + if usable == cutlass.Int32(1): + # cut = tightest line whose count is in [K, B*]; + # anchor (closed-loop publish) = loosest such line. + if n2_c >= kK_l and n2_c <= bs_l: + cut_t = seed_thr_row[2] + anch_t = seed_thr_row[2] + have = cutlass.Int32(1) + if n1_c >= kK_l and n1_c <= bs_l: + if have == cutlass.Int32(0): + cut_t = seed_thr_row[1] + anch_t = seed_thr_row[1] + have = cutlass.Int32(1) + if claimed_c <= bs_l: + if have == cutlass.Int32(0): + cut_t = seed_thr_row[0] + anch_t = seed_thr_row[0] + have = cutlass.Int32(1) + if have == cutlass.Int32(0): + # No line in band: bracket the cut between the + # two known lines that straddle it and find an + # in-band edge with a clamped gmem histogram. + # all counts > B* -> (t2, +inf): one max + # pass supplies the top; + # n1 > B* and n2 < K -> (t1, t2); + # n0 > B* and n1 < K -> (t0, t1). + vbase = cand_vals_row.iterator.toint() + b_lo = seed_thr_row[0] + b_hi = seed_thr_row[1] + base_c = n1_c + if n1_c > bs_l: + b_lo = seed_thr_row[1] + b_hi = seed_thr_row[2] + base_c = n2_c + need_max = cutlass.Int32(0) + if n2_c > bs_l: + b_lo = seed_thr_row[2] + base_c = cutlass.Int32(0) + need_max = cutlass.Int32(1) + if need_max == cutlass.Int32(1): + lmax = cutlass.Float32(self.NEG_FLT_MAX) + i_m = tidx + while i_m < claimed_c: + for _ju in cutlass.range_constexpr(4): + i_mj = i_m + cutlass.Int32(_ju * num_threads) + if i_mj < claimed_c: + vp_m = cute.make_ptr( + cutlass.Float32, + vbase + cutlass.Int64(i_mj) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + lmax = cute.arch.fmax( + lmax, cute.make_tensor(vp_m, cute.make_layout((1,)))[0] + ) + i_m = i_m + cutlass.Int32(4 * num_threads) + wmax_l = self.warp_reduce_max_f32(lmax) + if lane == cutlass.Int32(0): + smem_wmax[warp_id] = wmax_l + cute.arch.barrier() + vmax_l = cutlass.Float32(self.NEG_FLT_MAX) + for _wr in cutlass.range_constexpr(self.num_warps): + vmax_l = cute.arch.fmax(vmax_l, smem_wmax[_wr]) + b_hi = vmax_l + cutlass.Float32(1e-3) + # zooming clamped histogram (up to 3 rounds; the + # bracket is narrow so round 0 almost always + # fires). State broadcasts ride s_iscalars[2]/[3] + # (P2's slots, re-sentineled on every exit path). + NBL = cutlass.const_expr(self.kNumBins) + r_lo = b_lo + r_w = (b_hi - b_lo) / cutlass.Float32(NBL) + if r_w <= cutlass.Float32(0.0): + r_w = cutlass.Float32(1e-6) + searching = cutlass.Int32(1) + for _rd in cutlass.range_constexpr(3): + if searching == cutlass.Int32(1): + jz_l = tidx + while jz_l < cutlass.Int32(NBL): + smem_hist[jz_l] = cutlass.Int32(0) + jz_l = jz_l + cutlass.Int32(num_threads) + if tidx == cutlass.Int32(0): + s_iscalars[2] = cutlass.Int32(0) + cute.arch.barrier() + inv_wr = cutlass.Float32(1.0) / r_w + r_hi = r_lo + r_w * cutlass.Float32(NBL) + i_h = tidx + while i_h < claimed_c: + for _ju in cutlass.range_constexpr(4): + i_hj = i_h + cutlass.Int32(_ju * num_threads) + if i_hj < claimed_c: + vp_h = cute.make_ptr( + cutlass.Float32, + vbase + cutlass.Int64(i_hj) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + v_h = cute.make_tensor(vp_h, cute.make_layout((1,)))[0] + if v_h >= r_lo and v_h < r_hi: + b_h = cutlass.Int32((v_h - r_lo) * inv_wr) + if b_h < cutlass.Int32(0): + b_h = cutlass.Int32(0) + if b_h > cutlass.Int32(NBL - 1): + b_h = cutlass.Int32(NBL - 1) + atomicAdd( + smem_hist.iterator + b_h, cutlass.Int32(1) + ) + i_h = i_h + cutlass.Int32(4 * num_threads) + cute.arch.barrier() + if warp_id == cutlass.Int32(0): + SEGL = cutlass.const_expr(NBL // self.WARP_SIZE) + top_l = cutlass.Int32(NBL - 1) - lane * cutlass.Int32(SEGL) + seg_l = cute.make_fragment((SEGL,), cutlass.Int32) + part_l = cutlass.Int32(0) + for _js in cutlass.range_constexpr(SEGL): + v8_l = smem_hist[top_l - cutlass.Int32(_js)] + seg_l[_js] = v8_l + part_l = part_l + v8_l + tp_l = part_l + for _os in cutlass.range_constexpr(5): + ov_l = cutlass.const_expr(1 << _os) + oth_l = cute.arch.shuffle_sync_up( + tp_l, ov_l, mask_and_clamp=0 + ) + if lane >= cutlass.Int32(ov_l): + tp_l = tp_l + oth_l + excl_l = tp_l - part_l + kneed = kK_l - base_c + kfit = bs_l - base_c + run_l = cutlass.Int32(0) + for _js in cutlass.range_constexpr(SEGL): + run_l = run_l + seg_l[_js] + cum_at = excl_l + run_l + cum_bef = cum_at - seg_l[_js] + if cum_bef < kneed and cum_at >= kneed: + s_iscalars[3] = top_l - cutlass.Int32(_js) + smem_wcnt[0] = cum_bef + if cum_at <= kfit: + s_iscalars[2] = cutlass.Int32(1) + else: + s_iscalars[2] = cutlass.Int32(2) + cute.arch.barrier() + st_l = s_iscalars[2] + if st_l == cutlass.Int32(1): + cut_t = r_lo + cutlass.Float32(s_iscalars[3]) * r_w + anch_t = cut_t + have = cutlass.Int32(1) + searching = cutlass.Int32(0) + if st_l == cutlass.Int32(2): + base_c = base_c + smem_wcnt[0] + r_lo = r_lo + cutlass.Float32(s_iscalars[3]) * r_w + r_w = r_w / cutlass.Float32(NBL) + if r_w <= cutlass.Float32(0.0): + searching = cutlass.Int32(0) + if st_l == cutlass.Int32(0): + searching = cutlass.Int32(0) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + # restore P2's cnt_lo/cnt_hi sentinels + s_iscalars[2] = cutlass.Int32(-1) + s_iscalars[3] = cutlass.Int32(-1) + cute.arch.barrier() + if usable == cutlass.Int32(1) and have == cutlass.Int32(1): + # ---- single filtered load at the cut ---- take_cand = cutlass.Int32(1) - if cutlass.const_expr(_P4_TAIL_DBG): - ck0 = cute.arch.clock64() - ck1 = ck0 - if take_cand == cutlass.Int32(1): - t_lo = seed_thr_row[0] - lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) - vbase = cand_vals_row.iterator.toint() - NBL = cutlass.const_expr(self.kNumBins) - # ---- 1) stage: pure copy of the score column into - # the dedicated smem region. No ballots, no atomics - # (128 serialized smem atomics per trip measured - # ~1.1us/1k entries), no warp-uniform constraint. - # Sentinels sanitize to t_lo - 1 (rank below all live - # scores; the K+64 admission bound covers K). list_used = cutlass.Int32(1) if tidx == cutlass.Int32(0): - s_iscalars[2] = cutlass.Int32(0) - snt = t_lo - cutlass.Float32(1.0) - lmax = cutlass.Float32(self.NEG_FLT_MAX) - i_s = tidx - while i_s < claimed_c: + s_iscalars[0] = cutlass.Int32(0) + s_thr[0] = anch_t # closed-loop anchor + s_iscalars[1] = cutlass.Int32(1) # done + cute.arch.barrier() + vbase = cand_vals_row.iterator.toint() + lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) + i_c = tidx + while (i_c - lane_c) < claimed_c: + pvals = [] + pidxs = [] + keeps = [] for _ju in cutlass.range_constexpr(4): - i_sj = i_s + cutlass.Int32(_ju * num_threads) - if i_sj < claimed_c: + i_cj = i_c + cutlass.Int32(_ju * num_threads) + pval = cutlass.Float32(self.NEG_FLT_MAX) + keep = cutlass.Int32(0) + if i_cj < claimed_c: vp_l = cute.make_ptr( cutlass.Float32, - vbase + cutlass.Int64(i_sj) * cutlass.Int64(4), + vbase + cutlass.Int64(i_cj) * cutlass.Int64(4), cute.AddressSpace.gmem, assumed_align=4, ) - v_l = cute.make_tensor(vp_l, cute.make_layout((1,)))[0] - if v_l < t_lo: - v_l = snt - smem_list[i_sj] = v_l - lmax = cute.arch.fmax(lmax, v_l) - i_s = i_s + cutlass.Int32(4 * num_threads) - wmax_l = self.warp_reduce_max_f32(lmax) - if lane == cutlass.Int32(0): - smem_wmax[warp_id] = wmax_l - cute.arch.barrier() - # ---- 2) zooming histogram on the smem scores: find - # the edge whose exact count lands in [K, kC], as - # close to K as the bins allow. Value-linear bins - # collapse on long-tailed logits (vmax is an extreme - # outlier), so each round re-bins INSIDE the crossing - # bin - one cheap smem pass per round, NBL^3 total - # resolution; only genuine ties fall through. - vmax = cutlass.Float32(self.NEG_FLT_MAX) - for _wr in cutlass.range_constexpr(self.num_warps): - vmax = cute.arch.fmax(vmax, smem_wmax[_wr]) - rng_l = vmax - t_lo - if rng_l <= cutlass.Float32(0.0): - rng_l = cutlass.Float32(1.0) - r_lo = t_lo - r_w = rng_l / cutlass.Float32(NBL) - w0 = r_w # round-0 bin width (anchor edge recompute) - base_c = cutlass.Int32(0) - t_star = t_lo - searching = cutlass.Int32(1) - if tidx == cutlass.Int32(0): - # smem_wcnt[1]: round-0 anchor bin (the ~3K - # crossing; [0] is the descend base). -1 = not - # found -> anchor falls back to t_star. P4 - # clobbers smem_wcnt only AFTER we read this. - smem_wcnt[1] = cutlass.Int32(-1) - for _rd in cutlass.range_constexpr(3): - if searching == cutlass.Int32(1): - jz_l = tidx - while jz_l < cutlass.Int32(NBL): - smem_hist[jz_l] = cutlass.Int32(0) - jz_l = jz_l + cutlass.Int32(num_threads) - if tidx == cutlass.Int32(0): - s_iscalars[2] = cutlass.Int32(0) - cute.arch.barrier() - inv_wr = cutlass.Float32(1.0) / r_w - r_hi = r_lo + r_w * cutlass.Float32(NBL) - i_h = tidx - while i_h < claimed_c: - v_h = smem_list[i_h] - if v_h >= r_lo and v_h < r_hi: - b_h = cutlass.Int32((v_h - r_lo) * inv_wr) - if b_h < cutlass.Int32(0): - b_h = cutlass.Int32(0) - if b_h > cutlass.Int32(NBL - 1): - b_h = cutlass.Int32(NBL - 1) - atomicAdd(smem_hist.iterator + b_h, cutlass.Int32(1)) - i_h = i_h + cutlass.Int32(num_threads) - cute.arch.barrier() - # warp-0 top-down cumulative scan (P1b idiom) - if warp_id == cutlass.Int32(0): - SEGL = cutlass.const_expr(NBL // self.WARP_SIZE) - top_l = cutlass.Int32(NBL - 1) - lane * cutlass.Int32(SEGL) - seg_l = cute.make_fragment((SEGL,), cutlass.Int32) - part_l = cutlass.Int32(0) - for _js in cutlass.range_constexpr(SEGL): - v8_l = smem_hist[top_l - cutlass.Int32(_js)] - seg_l[_js] = v8_l - part_l = part_l + v8_l - tp_l = part_l - for _os in cutlass.range_constexpr(5): - ov_l = cutlass.const_expr(1 << _os) - oth_l = cute.arch.shuffle_sync_up(tp_l, ov_l, mask_and_clamp=0) - if lane >= cutlass.Int32(ov_l): - tp_l = tp_l + oth_l - excl_l = tp_l - part_l - kneed = cutlass.Int32(self.top_k) - base_c - kfit = cutlass.Int32(self.kC) - base_c - run_l = cutlass.Int32(0) - for _js in cutlass.range_constexpr(SEGL): - run_l = run_l + seg_l[_js] - cum_at = excl_l + run_l - cum_bef = cum_at - seg_l[_js] - if cum_bef < kneed and cum_at >= kneed: - s_iscalars[3] = top_l - cutlass.Int32(_js) - smem_wcnt[0] = cum_bef - if cum_at <= kfit: - s_iscalars[2] = cutlass.Int32(1) - else: - s_iscalars[2] = cutlass.Int32(2) - if cutlass.const_expr(_rd == 0): - # closed-loop ANCHOR: the ~3K - # crossing edge. Publishing the - # exact k-th made the next-step - # down-guard target only 4K - - # slope noise then undershoots K - # and forces ~26us fallbacks; a - # 3K-count anchor restores the - # old edge semantics (~12K guard - # target) at zero extra passes. - anch_n = cutlass.const_expr( - min(3 * self.top_k, (self.top_k + self.kC) // 2) - ) - if cum_bef < cutlass.Int32( - anch_n - ) and cum_at >= cutlass.Int32(anch_n): - smem_wcnt[1] = top_l - cutlass.Int32(_js) - cute.arch.barrier() - st_l = s_iscalars[2] - if st_l == cutlass.Int32(1): - t_star = r_lo + cutlass.Float32(s_iscalars[3]) * r_w - searching = cutlass.Int32(0) - if st_l == cutlass.Int32(2): - base_c = base_c + smem_wcnt[0] - r_lo = r_lo + cutlass.Float32(s_iscalars[3]) * r_w - r_w = r_w / cutlass.Float32(NBL) - if r_w <= cutlass.Float32(0.0): - searching = cutlass.Int32(0) - if st_l == cutlass.Int32(0): - searching = cutlass.Int32(0) - cute.arch.barrier() - if tidx == cutlass.Int32(0): - if s_iscalars[2] != cutlass.Int32(1): - s_iscalars[2] = cutlass.Int32(0) - cute.arch.barrier() - fired_l = s_iscalars[2] - t_anch = t_star - anch_b = smem_wcnt[1] - if anch_b >= cutlass.Int32(0): - t_anch = t_lo + cutlass.Float32(anch_b) * w0 - if fired_l == cutlass.Int32(1): - # ---- 3) compact survivors into the standard kC - # buffers: score from smem, position streamed - # from the gmem index column. The four unrolled - # sub-ballots merge into ONE atomic per warp per - # trip (the per-sub-ballot atomic serialized 128 - # adds per trip across 32 warps). - if tidx == cutlass.Int32(0): - s_iscalars[0] = cutlass.Int32(0) - s_thr[0] = t_anch # closed-loop anchor - s_iscalars[1] = cutlass.Int32(1) # done - cute.arch.barrier() - i_c = tidx - while (i_c - lane_c) < claimed_c: - pvals = [] - pidxs = [] - keeps = [] + pval = cute.make_tensor(vp_l, cute.make_layout((1,)))[0] + if pval >= cut_t: + keep = cutlass.Int32(1) + pvals.append(pval) + # vals slot carries the LIST INDEX (register; + # the position column is only gathered for + # the K winners after Phase 4) + pidxs.append(i_cj) + keeps.append(keep) + m0 = cute.arch.vote_ballot_sync(keeps[0] != cutlass.Int32(0)) + m1 = cute.arch.vote_ballot_sync(keeps[1] != cutlass.Int32(0)) + m2 = cute.arch.vote_ballot_sync(keeps[2] != cutlass.Int32(0)) + m3 = cute.arch.vote_ballot_sync(keeps[3] != cutlass.Int32(0)) + nk = cutlass.Int32( + cute.arch.popc(m0) + + cute.arch.popc(m1) + + cute.arch.popc(m2) + + cute.arch.popc(m3) + ) + bk = cutlass.Int32(0) + if nk > cutlass.Int32(0): + if lane_c == cutlass.Int32(0): + bk = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), nk) + bk = cute.arch.shuffle_sync(bk, cutlass.Int32(0)) + lmk = (cutlass.Uint32(1) << cutlass.Uint32(lane_c)) - cutlass.Uint32(1) + off = bk for _ju in cutlass.range_constexpr(4): - i_cj = i_c + cutlass.Int32(_ju * num_threads) - pval = cutlass.Float32(self.NEG_FLT_MAX) - # vals slot carries the LIST INDEX - a - # register, not a second (cold) gmem pass - # over the position column; the post-P4 - # repair gathers true positions for the K - # winners only. - pidx = i_cj - keep = cutlass.Int32(0) - if i_cj < claimed_c: - pval = smem_list[i_cj] - if pval >= t_star: - keep = cutlass.Int32(1) - pvals.append(pval) - pidxs.append(pidx) - keeps.append(keep) - m0 = cute.arch.vote_ballot_sync(keeps[0] != cutlass.Int32(0)) - m1 = cute.arch.vote_ballot_sync(keeps[1] != cutlass.Int32(0)) - m2 = cute.arch.vote_ballot_sync(keeps[2] != cutlass.Int32(0)) - m3 = cute.arch.vote_ballot_sync(keeps[3] != cutlass.Int32(0)) - nk = cutlass.Int32( - cute.arch.popc(m0) - + cute.arch.popc(m1) - + cute.arch.popc(m2) - + cute.arch.popc(m3) - ) - bk = cutlass.Int32(0) - if nk > cutlass.Int32(0): - if lane_c == cutlass.Int32(0): - bk = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), nk) - bk = cute.arch.shuffle_sync(bk, cutlass.Int32(0)) - lmk = ( - cutlass.Uint32(1) << cutlass.Uint32(lane_c) - ) - cutlass.Uint32(1) - off = bk - for _ju in cutlass.range_constexpr(4): - mj = ( - m0 - if _ju == 0 - else m1 - if _ju == 1 - else m2 - if _ju == 2 - else m3 - ) - if keeps[_ju] != cutlass.Int32(0): - wpos = off + cutlass.Int32(cute.arch.popc(mj & lmk)) - if wpos < cutlass.Int32(self.kC): - smem_keys[wpos] = pvals[_ju] - smem_vals[wpos] = pidxs[_ju] - off = off + cutlass.Int32(cute.arch.popc(mj)) + mj = m0 if _ju == 0 else m1 if _ju == 1 else m2 if _ju == 2 else m3 + if keeps[_ju] != cutlass.Int32(0): + wpos = off + cutlass.Int32(cute.arch.popc(mj & lmk)) + if wpos < cutlass.Int32(self.kC): + smem_keys[wpos] = pvals[_ju] + smem_vals[wpos] = pidxs[_ju] + off = off + cutlass.Int32(cute.arch.popc(mj)) i_c = i_c + cutlass.Int32(4 * num_threads) - cute.arch.barrier() - # hard net: histogram binning and the compact - # test round float edges independently - cnt_l = s_iscalars[0] - if cnt_l < cutlass.Int32(self.top_k) or cnt_l > cutlass.Int32(self.kC): - take_cand = cutlass.Int32(0) - list_used = cutlass.Int32(0) - if tidx == cutlass.Int32(0): - s_iscalars[1] = cutlass.Int32(0) - s_iscalars[2] = cutlass.Int32(-1) - s_iscalars[3] = cutlass.Int32(-1) - cute.arch.barrier() - if fired_l == cutlass.Int32(0): + if nk == cutlass.Int32(0): + i_c = i_c + cutlass.Int32(4 * num_threads) + cute.arch.barrier() + # histogram-edge cuts round float edges independently + # of the load predicate: keep the demote net for them + # (line cuts are exact by construction and never trip) + cnt_l = s_iscalars[0] + if cnt_l < cutlass.Int32(self.top_k) or cnt_l > cutlass.Int32(self.kC): take_cand = cutlass.Int32(0) list_used = cutlass.Int32(0) if tidx == cutlass.Int32(0): - s_iscalars[2] = cutlass.Int32(-1) - s_iscalars[3] = cutlass.Int32(-1) + s_iscalars[1] = cutlass.Int32(0) cute.arch.barrier() if cutlass.const_expr(_P4_TAIL_DBG): ck1 = cute.arch.clock64() + if take_cand == cutlass.Int32(0): # Stage this CTA's slice into SMEM once before Phase 2's # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index f464f01a2031..1e03cc03ad2f 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -23,6 +23,7 @@ import argparse import functools +import os import sys from pathlib import Path from typing import Optional @@ -69,6 +70,8 @@ def _compile( emit_xstate: bool = False, use_ext_cand: bool = False, cand_cap: int = 5120, + accept_cap: "int | None" = None, + kc_override: "int | None" = None, ): """JIT-compile the GVR kernel for a specific knob combination. @@ -167,7 +170,7 @@ def _compile( ) cand_ctl_fake = ( cute.runtime.make_fake_compact_tensor( - cutlass.Int32, (n_rows, 2), stride_order=(1, 0), assumed_align=8 + cutlass.Int32, (n_rows, 4), stride_order=(1, 0), assumed_align=8 ) if use_ext_cand else None @@ -202,6 +205,8 @@ def _compile( emit_xstate=emit_xstate, use_ext_cand=use_ext_cand, cand_cap=cand_cap, + accept_cap=accept_cap, + kc_override=kc_override, # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The # qfrac VALUES are irrelevant on this path (P1b is skipped) — only # the slot count matters. @@ -405,6 +410,47 @@ def derive_seed_rungs( return torch.stack([prev_thr - d_lo, prev_thr, prev_thr + d], dim=1).contiguous() +def derive_seed_lines_v4( + prev_anchor: torch.Tensor, + prev_sthr: "torch.Tensor | None" = None, + prev_ctl: "torch.Tensor | None" = None, + targets: "tuple[float, float, float]" = (8192.0, 5120.0, 2048.0), + fallback_spread: float = 0.5, +) -> torch.Tensor: + """v4 host-side line placement: put [t0, t1, t2] at target COUNTS. + + Slope of log2(count) vs threshold is fit from the previous step's + (t0, n0) / (t2, n2) pairs (counts ride in the widened control words); + each new line lands where the fit predicts its target count. Targets + descend (t0 loosest / largest count, t2 tightest). + + Args: + prev_anchor: [rows] previous accepted cut value (xstate[:, 2]). + prev_sthr: [rows, 3] previous lines (or None -> fixed spread). + prev_ctl: [rows, 4] previous control words {n0, void, n1, n2}. + targets: (T0, T1, T2) target counts, T0 > T1 > T2. + + Returns: + [rows, 3] fp32 lines ascending [t0, t1, t2]. + """ + t0_t, t1_t, t2_t = targets + if prev_sthr is None or prev_ctl is None: + d = torch.full_like(prev_anchor, fallback_spread) + return torch.stack([prev_anchor - d, prev_anchor, prev_anchor + d], dim=1).contiguous() + c0 = prev_ctl[:, 0].float().clamp(min=1.0) + c2 = prev_ctl[:, 3].float().clamp(min=1.0) + dthr = (prev_sthr[:, 2] - prev_sthr[:, 0]).clamp(min=1e-3) + slope = ((torch.log2(c0) - torch.log2(c2)) / dthr).clamp(min=0.05, max=64.0) + # anchor count estimate: slide the anchor onto the prev line fit + anch_c = (c2 * torch.exp2(-(prev_anchor - prev_sthr[:, 2]) * slope)).clamp(min=1.0, max=1e6) + lines = [prev_anchor + torch.log2(anch_c / tgt) / slope for tgt in (t0_t, t1_t, t2_t)] + out = torch.stack(lines, dim=1) + # enforce strictly ascending (degenerate slope guards) + out[:, 1] = torch.maximum(out[:, 1], out[:, 0] + 1e-4) + out[:, 2] = torch.maximum(out[:, 2], out[:, 1] + 1e-4) + return out.contiguous() + + def emu_seed_counts( logits: torch.Tensor, seq_lens: torch.Tensor, @@ -452,11 +498,15 @@ def emu_cand( lf = logits.to(torch.float32) cand_vals = torch.full((R, cap), float("-inf"), dtype=torch.float32, device=dev) cand_idx = torch.full((R, cap), -1, dtype=torch.int32, device=dev) - ctl = torch.zeros((R, 2), dtype=torch.int32, device=dev) + ctl = torch.zeros((R, 4), dtype=torch.int32, device=dev) for r in range(R): ne = int(n_eff[r]) hits = torch.nonzero(lf[r, :ne] >= seed_thr[r, 0], as_tuple=False).flatten() cnt = hits.numel() + # emitter-side counts: two extra compares per EMITTED element + # (t1, t2 > t0 so counting over the list == counting over the row) + ctl[r, 2] = int((lf[r, :ne] >= seed_thr[r, 1]).sum()) + ctl[r, 3] = int((lf[r, :ne] >= seed_thr[r, 2]).sum()) # unordered contract: shuffle, then interleave sentinels perm = hits[torch.randperm(cnt, device=dev)] ent = torch.full((cnt + sentinel_pad,), -1, dtype=torch.int64, device=dev) @@ -649,8 +699,8 @@ def gvr_topk_decode( cand_ctl.dtype == torch.int32 and cand_ctl.is_cuda and cand_ctl.is_contiguous() - and cand_ctl.shape == (num_rows, 2) - ), "cand_ctl must be contiguous CUDA int32 [num_rows, 2]" + and cand_ctl.shape == (num_rows, 4) + ), "cand_ctl must be contiguous CUDA int32 [num_rows, 4]" cand_cap = cand_vals.shape[1] emit_xstate = xstate is not None if emit_xstate: @@ -742,6 +792,8 @@ def gvr_topk_decode( emit_xstate, use_ext_cand, cand_cap, + int(os.environ["GVR_BSTAR"]) if "GVR_BSTAR" in os.environ else None, + int(os.environ["GVR_KC"]) if "GVR_KC" in os.environ else None, ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. From cac0898d0b938986e07ba7a0da75164ef3e35d4f Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:46:21 -0700 Subject: [PATCH 020/117] [None][perf] GVR L2 v5: bucketed segments, mapped-prefix copy, sampled histogram Emitter writes the candidate list into three fixed segments (>=t2 / [t1,t2) / [t0,t1), caps B*/B*/rest, spill to the looser segment on overflow), so a line cut only ever reads the dense mapped prefix of the segments above it: the hit path becomes a pure copy (no value filter, no ballots, no atomics) and the histogram path walks mapped indices. When all three lines overshoot B*, the bracket segment's own prefix doubles as an unbiased sample: the histogram runs on it at the sample rate with 1.25x-scaled fire targets, and the exact post-load count net absorbs the sampling noise. Device-level cold-chain results vs the block-skip arm (B=1): flash 132k 1.58x, pro 160k 1.57x, pro 320k 1.54x, pro 640k 1.98x (fastest steps 8-14us). Exactness smokes pass for cs=1/4/8 including forced straddle/void routings. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 290 ++++++++++++------ .../cute_dsl_kernels/top_k/run_gvr_topk.py | 64 ++++ 2 files changed, 260 insertions(+), 94 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index a2e64640704c..9d415717dd4c 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -581,7 +581,11 @@ def __init__( # slice cache (128KB) - that combination exceeds the 227KB CTA # limit and fails loudly at compile time. Long rows (the list # path's target) cannot enable the slice cache anyway. - self.list_cap = int(cand_cap) + # v5 bucketed layout: tensor width = 2 * accept_cap + # (segments A, B) + segment-C capacity; the admission + # bounds the ENTRY COUNT by C's capacity (the only + # segment that can void). + self.list_cap = max(0, int(cand_cap) - 2 * self.accept_cap) self.cand_rung = int(cand_rung) if use_ext_cand and not use_ext_counts: raise ValueError("use_ext_cand requires use_ext_counts") @@ -4960,14 +4964,38 @@ def _run_phases( and cluster_size == 1 and self.dtype == cutlass.Float32 ): + # ---- List path v5: BUCKETED segments ---- + # The emitter classifies each entry by the tightest line + # it passes and appends into one of three fixed segments + # (A = [0, segA) holds >= t2, B = [segA, 2*segA) holds + # [t1, t2), C = [2*segA, ...) holds [t0, t1)); a full + # segment spills to the next looser one. Segment caps = + # B*, so "line in the acceptance band" <=> "its segment + # prefix group is complete" - the same condition the cut + # selection already checks. A LINE cut therefore loads a + # dense prefix of known length: a pure mapped copy, no + # filtering, no ballots, no atomics, zero wasted reads. + # Histogram fallbacks value-scan the mapped extents. claimed_c = cutlass.Int32(cand_ctl_row[0]) void_c = cutlass.Int32(cand_ctl_row[1]) n1_c = cutlass.Int32(cand_ctl_row[2]) n2_c = cutlass.Int32(cand_ctl_row[3]) - bstar = cutlass.const_expr(min(self.accept_cap, self.kC)) + segA = cutlass.const_expr(min(self.accept_cap, self.kC)) + bstar = segA if cutlass.const_expr(_P4_TAIL_DBG): ck0 = cute.arch.clock64() ck1 = ck0 + # segment extents (pads live at C's tail: sentinel score + # -inf slots, harmless to copy, never rank) + lenA = n2_c + if lenA > cutlass.Int32(segA): + lenA = cutlass.Int32(segA) + spillA = n2_c - lenA + lenB = n1_c - n2_c + spillA + if lenB > cutlass.Int32(segA): + lenB = cutlass.Int32(segA) + lenC = claimed_c - lenA - lenB + total_l = claimed_c usable = cutlass.Int32(0) if ( void_c == cutlass.Int32(0) @@ -4975,61 +5003,84 @@ def _run_phases( and claimed_c <= cutlass.Int32(self.list_cap) ): usable = cutlass.Int32(1) - # pre-declared: the DSL forbids first-assigning inside a - # dynamic if when read outside it kK_l = cutlass.Int32(self.top_k) bs_l = cutlass.Int32(bstar) cut_t = cutlass.Float32(0.0) + cut_n = cutlass.Int32(0) have = cutlass.Int32(0) + line_cut = cutlass.Int32(0) anch_t = cutlass.Float32(0.0) + vbase = cutlass.Int64(0) + if cutlass.const_expr(True): + vbase = cand_vals_row.iterator.toint() if usable == cutlass.Int32(1): - # cut = tightest line whose count is in [K, B*]; - # anchor (closed-loop publish) = loosest such line. + # cut = tightest line in [K, B*]; anchor = loosest. if n2_c >= kK_l and n2_c <= bs_l: cut_t = seed_thr_row[2] + cut_n = n2_c anch_t = seed_thr_row[2] have = cutlass.Int32(1) + line_cut = cutlass.Int32(1) if n1_c >= kK_l and n1_c <= bs_l: if have == cutlass.Int32(0): cut_t = seed_thr_row[1] + cut_n = n1_c anch_t = seed_thr_row[1] have = cutlass.Int32(1) + line_cut = cutlass.Int32(1) if claimed_c <= bs_l: if have == cutlass.Int32(0): cut_t = seed_thr_row[0] + cut_n = claimed_c anch_t = seed_thr_row[0] have = cutlass.Int32(1) + line_cut = cutlass.Int32(1) if have == cutlass.Int32(0): - # No line in band: bracket the cut between the - # two known lines that straddle it and find an - # in-band edge with a clamped gmem histogram. - # all counts > B* -> (t2, +inf): one max - # pass supplies the top; - # n1 > B* and n2 < K -> (t1, t2); - # n0 > B* and n1 < K -> (t0, t1). - vbase = cand_vals_row.iterator.toint() + # ---- clamped-histogram fallback over the mapped + # extents (bracket between two known lines; the + # all-above case takes one max pass first) ---- + # histogram source = the bracket's own SEGMENT + # prefix: if the segment is full it is a value- + # blind (unbiased) SAMPLE of the band - scale the + # targets by band/segment and let the post-load + # count net verify; if not full it IS the exact + # band. Either way the scan shrinks from the + # whole list to <= one segment. b_lo = seed_thr_row[0] b_hi = seed_thr_row[1] base_c = n1_c + hs_base = cutlass.Int32(2 * segA) # segment C + hs_len = lenC + hs_band = claimed_c - n1_c if n1_c > bs_l: b_lo = seed_thr_row[1] b_hi = seed_thr_row[2] base_c = n2_c + hs_base = cutlass.Int32(segA) # segment B + hs_len = lenB + hs_band = n1_c - n2_c need_max = cutlass.Int32(0) if n2_c > bs_l: b_lo = seed_thr_row[2] base_c = cutlass.Int32(0) + hs_base = cutlass.Int32(0) # segment A + hs_len = lenA + hs_band = n2_c need_max = cutlass.Int32(1) + if hs_band < cutlass.Int32(1): + hs_band = cutlass.Int32(1) + samp_f = (cutlass.Float32(1.0) * hs_len) / hs_band if need_max == cutlass.Int32(1): lmax = cutlass.Float32(self.NEG_FLT_MAX) i_m = tidx - while i_m < claimed_c: + while i_m < hs_len: for _ju in cutlass.range_constexpr(4): - i_mj = i_m + cutlass.Int32(_ju * num_threads) - if i_mj < claimed_c: + j_m = i_m + cutlass.Int32(_ju * num_threads) + if j_m < hs_len: + src_m = hs_base + j_m vp_m = cute.make_ptr( cutlass.Float32, - vbase + cutlass.Int64(i_mj) * cutlass.Int64(4), + vbase + cutlass.Int64(src_m) * cutlass.Int64(4), cute.AddressSpace.gmem, assumed_align=4, ) @@ -5045,15 +5096,26 @@ def _run_phases( for _wr in cutlass.range_constexpr(self.num_warps): vmax_l = cute.arch.fmax(vmax_l, smem_wmax[_wr]) b_hi = vmax_l + cutlass.Float32(1e-3) - # zooming clamped histogram (up to 3 rounds; the - # bracket is narrow so round 0 almost always - # fires). State broadcasts ride s_iscalars[2]/[3] - # (P2's slots, re-sentineled on every exit path). NBL = cutlass.const_expr(self.kNumBins) r_lo = b_lo r_w = (b_hi - b_lo) / cutlass.Float32(NBL) if r_w <= cutlass.Float32(0.0): r_w = cutlass.Float32(1e-6) + # sample-unit targets (population targets scaled + # by segment/band); the descend base stays in + # sample units too - the post-load exact-count + # net absorbs the sampling error. + # fire target = 1.25x the K-need: the sampled + # estimate carries ~1-2% noise and firing at the + # band's bottom edge would demote half the time + kneedS = cutlass.Int32( + (cutlass.Float32(1.25) * (kK_l - base_c)) * samp_f + + cutlass.Float32(0.5) + ) + if kneedS < cutlass.Int32(1): + kneedS = cutlass.Int32(1) + kfitS = cutlass.Int32((cutlass.Float32(1.0) * (bs_l - base_c)) * samp_f) + sbase = cutlass.Int32(0) searching = cutlass.Int32(1) for _rd in cutlass.range_constexpr(3): if searching == cutlass.Int32(1): @@ -5067,13 +5129,14 @@ def _run_phases( inv_wr = cutlass.Float32(1.0) / r_w r_hi = r_lo + r_w * cutlass.Float32(NBL) i_h = tidx - while i_h < claimed_c: + while i_h < hs_len: for _ju in cutlass.range_constexpr(4): - i_hj = i_h + cutlass.Int32(_ju * num_threads) - if i_hj < claimed_c: + j_h = i_h + cutlass.Int32(_ju * num_threads) + if j_h < hs_len: + src_h = hs_base + j_h vp_h = cute.make_ptr( cutlass.Float32, - vbase + cutlass.Int64(i_hj) * cutlass.Int64(4), + vbase + cutlass.Int64(src_h) * cutlass.Int64(4), cute.AddressSpace.gmem, assumed_align=4, ) @@ -5107,8 +5170,8 @@ def _run_phases( if lane >= cutlass.Int32(ov_l): tp_l = tp_l + oth_l excl_l = tp_l - part_l - kneed = kK_l - base_c - kfit = bs_l - base_c + kneed = kneedS - sbase + kfit = kfitS - sbase run_l = cutlass.Int32(0) for _js in cutlass.range_constexpr(SEGL): run_l = run_l + seg_l[_js] @@ -5129,7 +5192,7 @@ def _run_phases( have = cutlass.Int32(1) searching = cutlass.Int32(0) if st_l == cutlass.Int32(2): - base_c = base_c + smem_wcnt[0] + sbase = sbase + smem_wcnt[0] r_lo = r_lo + cutlass.Float32(s_iscalars[3]) * r_w r_w = r_w / cutlass.Float32(NBL) if r_w <= cutlass.Float32(0.0): @@ -5138,85 +5201,122 @@ def _run_phases( searching = cutlass.Int32(0) cute.arch.barrier() if tidx == cutlass.Int32(0): - # restore P2's cnt_lo/cnt_hi sentinels s_iscalars[2] = cutlass.Int32(-1) s_iscalars[3] = cutlass.Int32(-1) cute.arch.barrier() if usable == cutlass.Int32(1) and have == cutlass.Int32(1): - # ---- single filtered load at the cut ---- take_cand = cutlass.Int32(1) list_used = cutlass.Int32(1) if tidx == cutlass.Int32(0): s_iscalars[0] = cutlass.Int32(0) - s_thr[0] = anch_t # closed-loop anchor + s_thr[0] = anch_t s_iscalars[1] = cutlass.Int32(1) # done cute.arch.barrier() - vbase = cand_vals_row.iterator.toint() lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) - i_c = tidx - while (i_c - lane_c) < claimed_c: - pvals = [] - pidxs = [] - keeps = [] - for _ju in cutlass.range_constexpr(4): - i_cj = i_c + cutlass.Int32(_ju * num_threads) - pval = cutlass.Float32(self.NEG_FLT_MAX) - keep = cutlass.Int32(0) - if i_cj < claimed_c: - vp_l = cute.make_ptr( - cutlass.Float32, - vbase + cutlass.Int64(i_cj) * cutlass.Int64(4), - cute.AddressSpace.gmem, - assumed_align=4, - ) - pval = cute.make_tensor(vp_l, cute.make_layout((1,)))[0] - if pval >= cut_t: - keep = cutlass.Int32(1) - pvals.append(pval) - # vals slot carries the LIST INDEX (register; - # the position column is only gathered for - # the K winners after Phase 4) - pidxs.append(i_cj) - keeps.append(keep) - m0 = cute.arch.vote_ballot_sync(keeps[0] != cutlass.Int32(0)) - m1 = cute.arch.vote_ballot_sync(keeps[1] != cutlass.Int32(0)) - m2 = cute.arch.vote_ballot_sync(keeps[2] != cutlass.Int32(0)) - m3 = cute.arch.vote_ballot_sync(keeps[3] != cutlass.Int32(0)) - nk = cutlass.Int32( - cute.arch.popc(m0) - + cute.arch.popc(m1) - + cute.arch.popc(m2) - + cute.arch.popc(m3) - ) - bk = cutlass.Int32(0) - if nk > cutlass.Int32(0): - if lane_c == cutlass.Int32(0): - bk = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), nk) - bk = cute.arch.shuffle_sync(bk, cutlass.Int32(0)) - lmk = (cutlass.Uint32(1) << cutlass.Uint32(lane_c)) - cutlass.Uint32(1) - off = bk + if line_cut == cutlass.Int32(1): + # ---- LINE cut: dense mapped-prefix COPY of + # exactly cut_n entries. No filter, no ballots, + # no atomics - every read is a winner candidate. + if tidx == cutlass.Int32(0): + s_iscalars[0] = cut_n + i_c = tidx + while i_c < cut_n: for _ju in cutlass.range_constexpr(4): - mj = m0 if _ju == 0 else m1 if _ju == 1 else m2 if _ju == 2 else m3 - if keeps[_ju] != cutlass.Int32(0): - wpos = off + cutlass.Int32(cute.arch.popc(mj & lmk)) - if wpos < cutlass.Int32(self.kC): - smem_keys[wpos] = pvals[_ju] - smem_vals[wpos] = pidxs[_ju] - off = off + cutlass.Int32(cute.arch.popc(mj)) + j_c = i_c + cutlass.Int32(_ju * num_threads) + if j_c < cut_n: + src_c = j_c + if j_c >= lenA: + src_c = cutlass.Int32(segA) + j_c - lenA + if j_c >= lenA + lenB: + src_c = cutlass.Int32(2 * segA) + j_c - lenA - lenB + vp_c = cute.make_ptr( + cutlass.Float32, + vbase + cutlass.Int64(src_c) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + smem_keys[j_c] = cute.make_tensor(vp_c, cute.make_layout((1,)))[ + 0 + ] + smem_vals[j_c] = src_c i_c = i_c + cutlass.Int32(4 * num_threads) - if nk == cutlass.Int32(0): + cute.arch.barrier() + if line_cut == cutlass.Int32(0): + # ---- histogram-edge cut: value-filtered mapped + # walk with merged-ballot claims (float edges + # round independently -> demote net below). + i_c = tidx + while (i_c - lane_c) < total_l: + pvals = [] + pidxs = [] + keeps = [] + for _ju in cutlass.range_constexpr(4): + j_c = i_c + cutlass.Int32(_ju * num_threads) + pval = cutlass.Float32(self.NEG_FLT_MAX) + src_c = cutlass.Int32(0) + keep = cutlass.Int32(0) + if j_c < total_l: + src_c = j_c + if j_c >= lenA: + src_c = cutlass.Int32(segA) + j_c - lenA + if j_c >= lenA + lenB: + src_c = cutlass.Int32(2 * segA) + j_c - lenA - lenB + vp_c = cute.make_ptr( + cutlass.Float32, + vbase + cutlass.Int64(src_c) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + pval = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] + if pval >= cut_t: + keep = cutlass.Int32(1) + pvals.append(pval) + pidxs.append(src_c) + keeps.append(keep) + m0 = cute.arch.vote_ballot_sync(keeps[0] != cutlass.Int32(0)) + m1 = cute.arch.vote_ballot_sync(keeps[1] != cutlass.Int32(0)) + m2 = cute.arch.vote_ballot_sync(keeps[2] != cutlass.Int32(0)) + m3 = cute.arch.vote_ballot_sync(keeps[3] != cutlass.Int32(0)) + nk = cutlass.Int32( + cute.arch.popc(m0) + + cute.arch.popc(m1) + + cute.arch.popc(m2) + + cute.arch.popc(m3) + ) + bk = cutlass.Int32(0) + if nk > cutlass.Int32(0): + if lane_c == cutlass.Int32(0): + bk = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), nk) + bk = cute.arch.shuffle_sync(bk, cutlass.Int32(0)) + lmk = ( + cutlass.Uint32(1) << cutlass.Uint32(lane_c) + ) - cutlass.Uint32(1) + off = bk + for _ju in cutlass.range_constexpr(4): + mj = ( + m0 + if _ju == 0 + else m1 + if _ju == 1 + else m2 + if _ju == 2 + else m3 + ) + if keeps[_ju] != cutlass.Int32(0): + wpos = off + cutlass.Int32(cute.arch.popc(mj & lmk)) + if wpos < cutlass.Int32(self.kC): + smem_keys[wpos] = pvals[_ju] + smem_vals[wpos] = pidxs[_ju] + off = off + cutlass.Int32(cute.arch.popc(mj)) i_c = i_c + cutlass.Int32(4 * num_threads) - cute.arch.barrier() - # histogram-edge cuts round float edges independently - # of the load predicate: keep the demote net for them - # (line cuts are exact by construction and never trip) - cnt_l = s_iscalars[0] - if cnt_l < cutlass.Int32(self.top_k) or cnt_l > cutlass.Int32(self.kC): - take_cand = cutlass.Int32(0) - list_used = cutlass.Int32(0) - if tidx == cutlass.Int32(0): - s_iscalars[1] = cutlass.Int32(0) cute.arch.barrier() + cnt_l = s_iscalars[0] + if cnt_l < cutlass.Int32(self.top_k) or cnt_l > cutlass.Int32(self.kC): + take_cand = cutlass.Int32(0) + list_used = cutlass.Int32(0) + if tidx == cutlass.Int32(0): + s_iscalars[1] = cutlass.Int32(0) + cute.arch.barrier() if cutlass.const_expr(_P4_TAIL_DBG): ck1 = cute.arch.clock64() @@ -5691,7 +5791,9 @@ def _run_phases( io_r = tidx while io_r < cutlass.Int32(self.top_k): li_r = output_indices_row[io_r] - if li_r >= cutlass.Int32(0) and li_r < claimed_c: + # slots are segmented offsets (may exceed the + # entry count); only sentinel -1 is invalid + if li_r >= cutlass.Int32(0): ip_r = cute.make_ptr( cutlass.Int32, cand_idx_row.iterator.toint() diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 1e03cc03ad2f..70a64d2e0e28 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -410,6 +410,70 @@ def derive_seed_rungs( return torch.stack([prev_thr - d_lo, prev_thr, prev_thr + d], dim=1).contiguous() +def emu_cand_bucketed( + logits: torch.Tensor, + seq_lens: torch.Tensor, + seed_thr: torch.Tensor, + cap: int, + seg_cap: int = 8192, + next_n: int = 1, + compress_ratio: int = 1, + sentinel_pad: int = 0, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Bucketed SoA candidate emission (v5 contract). + + Three fixed segments in one buffer: A = [0, seg_cap) holds >= t2, + B = [seg_cap, 2*seg_cap) holds [t1, t2), C = [2*seg_cap, 2*seg_cap + + cap) holds [t0, t1). A full segment spills to the next looser one + (never drops an entry), so the union always equals the full >= t0 + set, and a segment group is complete exactly when its line's count + fits the acceptance band (seg_cap = B*). Sentinel pads land in C. + ctl = {n0 (claimed incl pads), void, n1, n2}. + """ + R, _C = logits.shape + dev = logits.device + n_eff = _row_n_eff(seq_lens, R, next_n, compress_ratio) + lf = logits.to(torch.float32) + width = 2 * seg_cap + cap + cand_vals = torch.full((R, width), float("-inf"), dtype=torch.float32, device=dev) + cand_idx = torch.full((R, width), -1, dtype=torch.int32, device=dev) + ctl = torch.zeros((R, 4), dtype=torch.int32, device=dev) + for r in range(R): + ne = int(n_eff[r]) + row = lf[r, :ne] + hits = torch.nonzero(row >= seed_thr[r, 0], as_tuple=False).flatten() + cnt = hits.numel() + ctl[r, 2] = int((row >= seed_thr[r, 1]).sum()) + ctl[r, 3] = int((row >= seed_thr[r, 2]).sum()) + # emission order is value-blind: shuffle, then classify + perm = hits[torch.randperm(cnt, device=dev)] + v = row[perm] + seg = torch.where(v >= seed_thr[r, 2], 0, torch.where(v >= seed_thr[r, 1], 1, 2)) + # vectorized spill-to-looser: stream s = native entries + spill + # from s-1 (in emission order); ordinal beyond the cap spills on. + in_a = seg == 0 + ord_a = torch.cumsum(in_a.int(), 0) + stay_a = in_a & (ord_a <= seg_cap) + in_b = (seg == 1) | (in_a & ~stay_a) + ord_b = torch.cumsum(in_b.int(), 0) + stay_b = in_b & (ord_b <= seg_cap) + in_c = (seg == 2) | (in_b & ~stay_b) + ord_c = torch.cumsum(in_c.int(), 0) + stay_c = in_c & (ord_c <= cap) + voided = int(in_c.sum()) > cap + slot = torch.full_like(seg, -1) + slot[stay_a] = ord_a[stay_a] - 1 + slot[stay_b] = seg_cap + (ord_b[stay_b] - 1) + slot[stay_c] = 2 * seg_cap + (ord_c[stay_c] - 1) + live = slot >= 0 + cand_vals[r, slot[live].long()] = v[live] + cand_idx[r, slot[live].long()] = perm[live].int() + claimed = cnt + sentinel_pad + ctl[r, 0] = claimed + ctl[r, 1] = 1 if (voided or claimed > cap) else 0 + return cand_vals, cand_idx, ctl + + def derive_seed_lines_v4( prev_anchor: torch.Tensor, prev_sthr: "torch.Tensor | None" = None, From 3ca97312463a1cfd225bd04240df08419206cf76 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 05:50:03 -0700 Subject: [PATCH 021/117] [None][perf] GVR P4: in-place class compaction + three-tier boundary repair Sub-phase clock64 instrumentation (GVR_P4_SUB_DBG) showed the P4 rank-scatter core costs only ~0.6us/k candidates; the chain-observed ~1.9us/k came from the exact-tail boundary repair: the tiny-tie fast path ran an O(need x class) serial select on thread0 (~10us on real rows with need ~100 x class ~100), and bigger classes re-scanned every candidate per radix level behind ~20 block barriers. The repair is now: (1) a block-wide pure-tie check over the straddle class (bit-equal class needs no repair at all - the scatter's arrival fill is already value-set exact); (2) mixed classes are compacted IN PLACE into smem_keys/vals[0..class) with a register-buffered two-phase pass (warp-aggregated slot claims), so every later step scales with the class, never the candidate count; (3) class <= 128 takes an exact warp0 pairwise-rank rewrite, larger classes a block-parallel 4-level MSB radix over the compacted pairs with a warp0 shuffle-scan digit search (3 block barriers per level instead of 5). The full-candidate radix fallback is gone from the fast-tail variant. Device-level cold chains, 640k B=1: step mean 13.7 -> 12.6us (1.97x -> 2.13x vs block-skip; the previously slowest window improves 17.35 -> 12.6us as five 19-21us serial-repair victims drop to 9.3-10.8us); 640k B=8 19.5us (1.60x). Exactness: 18/18 microbench cells including forced tie/outlier stressors, smokes cs=1/4/8 plus forced straddle/void routings all bit-exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 544 ++++++++++-------- 1 file changed, 310 insertions(+), 234 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 9d415717dd4c..c9b3a6800f40 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -46,6 +46,9 @@ # into the spare xstate slots (harness-side analysis). Off by default; # NEVER set in production. _P4_TAIL_DBG = bool(int(os.environ.get("GVR_P4_TAIL_DBG", "0"))) +# P4 sub-phase clock64 breakdown -> xstate[1,2,4,5,6,7] (debug: clobbers +# the closed-loop thr/anch publish; single-shot cells only, not chains) +_P4_SUB_DBG = bool(int(os.environ.get("GVR_P4_SUB_DBG", "0"))) # --------------------------------------------------------------------------- @@ -2925,6 +2928,15 @@ def phase4_rank_scatter( output_indices_row[i4] = smem_vals[i4] i4 = i4 + cutlass.Int32(num_threads) elif cand_count > cutlass.Int32(kK): + sc0 = cutlass.Int64(0) + sc1 = cutlass.Int64(0) + sc2 = cutlass.Int64(0) + sc3 = cutlass.Int64(0) + sc4 = cutlass.Int64(0) + sc5 = cutlass.Int64(0) + sc6 = cutlass.Int64(0) + if cutlass.const_expr(_P4_SUB_DBG): + sc0 = cute.arch.clock64() # ---- block min/max over candidates ---- local_cmin = cutlass.Float32(self.FLT_MAX) local_cmax = cutlass.Float32(self.NEG_FLT_MAX) @@ -2954,6 +2966,8 @@ def phase4_rank_scatter( if bmax_r <= bmin_r: bmax_r = bmin_r + cutlass.Float32(1e-6) cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc1 = cute.arch.clock64() # ---- zero + build histogram ---- i6 = tidx while i6 < cutlass.Int32(kBins): @@ -2973,6 +2987,8 @@ def phase4_rank_scatter( atomicAdd(smem_hist.iterator + bin_i, cutlass.Int32(1)) i7 = i7 + cutlass.Int32(num_threads) cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc2 = cute.arch.clock64() # ---- 3-step high→low bin search → straddling bin b* + rank_above ---- warp_bin_sum = cutlass.Int32(0) for jb in cutlass.range_constexpr(bins_per_warp): @@ -3024,6 +3040,8 @@ def phase4_rank_scatter( s_iscalars[4] = cutlass.Int32(0) # cnt_above s_iscalars[1] = cutlass.Int32(0) # cnt_straddle cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc3 = cute.arch.clock64() b_star = s_iscalars[3] rank_above = s_iscalars[2] @@ -3122,6 +3140,8 @@ def phase4_rank_scatter( s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) s_iscalars[1] = cutlass.Int32(0) # cnt_strad (b*, sub==sb*) cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc4 = cute.arch.clock64() sb_star = smem_hist[2] rank_above_fine = smem_hist[3] isc = tidx @@ -3160,6 +3180,8 @@ def phase4_rank_scatter( output_indices_row[pos] = smem_vals[isc] isc = isc + cutlass.Int32(num_threads) cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc5 = cute.arch.clock64() cnt_strad = s_iscalars[1] filled = rank_above_fine + cnt_strad if filled > cutlass.Int32(kK): @@ -3183,257 +3205,281 @@ def phase4_rank_scatter( # slot range [rank_above_fine, kK). Unambiguous rows (the # overwhelming majority) pay two scalar compares; the counters # and the fine histogram are reused, so SMEM does not grow. - # [p4tt] tiny-tie fast path: when the exact-tail gate fires - # with a small (b*, sb*) tie class (cnt_strad <= 128 — the - # real firing cells hold 2), ONE candidate pass collects the - # class and thread0 selects the top-need exactly, replacing - # the 4 unconditional radix passes. Larger classes take the - # UNMODIFIED radix select below (verbatim copy). + # [p4tt] boundary-class repair: collect the (b*, sb*) tie + # class compactly (ONE candidate pass), then select inside + # it. Tiny jobs (need x class <= 512) keep the thread0 + # serial select (cheapest at that size). Bigger classes up + # to capc get a 4-level MSB radix over the COLLECTED class + # (each level scans <= capc pairs with all threads instead + # of re-scanning every candidate) — the old serial select + # was O(need x class) and measured 8.5us on real chain rows + # (need ~100 x class ~100). Classes beyond capc take the + # UNMODIFIED full-candidate radix below (verbatim copy). if cutlass.const_expr(self.p4_exact_tail and self.p4_tail_fast): # [p4tt] need0 = cutlass.Int32(kK) - rank_above_fine - if cutlass.const_expr(_P4_TAIL_DBG): + # [p4tt-v3] per-thread compact buffers, bounded by the + # strided trip count over the candidate array + nbuf7 = cutlass.const_expr((self.kC + self.num_threads - 1) // self.num_threads) + rv7 = cute.make_fragment((nbuf7,), cutlass.Float32) + ri7 = cute.make_fragment((nbuf7,), cutlass.Int32) + if cutlass.const_expr(_P4_TAIL_DBG or _P4_SUB_DBG): if tidx == cutlass.Int32(0): s_thr[1] = cutlass.Float32(cnt_strad) s_thr[2] = cutlass.Float32(need0) + fast_done = cutlass.Int32(1) if cnt_strad > need0 and need0 > cutlass.Int32(0): - if cnt_strad <= cutlass.Int32(128): - # [p4tt] SMEM: (value_bits, cand_idx) pairs at - # smem_hist[2*o]/[2*o+1], o < 128 (slots 0..255). - # The 256 digit bins are dead here (the fast path - # replaces the radix levels that used them); the - # sb_star/ra staging in slots 2/3 was read by - # every thread before the pre-scatter barrier. - # Persistent radix scalars [256..258] untouched. - # Collect counter = s_iscalars[0] (dead after the - # scatter; same reuse as the radix rewrite pass). + fast_done = cutlass.Int32(0) + # [p4tt-v3] block-wide pure-tie check, ANY class + # size: min/max order key over the (b*, sb*) class. + # A pure-tie class needs NO repair — the scatter's + # arrival fill of bit-equal values is already + # value-set exact. Real fp8-lineage logits tie in + # the thousands, which used to take the full radix. + # Staging mirrors the head min/max (wcnt + hist + # slots [0..31], both dead here; pairs live at + # 260+). + kmn6 = cutlass.Int32(2147483647) + kmx6 = cutlass.Int32(-2147483648) + it6 = tidx + while it6 < cand_count: + v6 = smem_keys[it6] + b6 = cutlass.Int32((v6 - bmin_r) * inv1) + if b6 < cutlass.Int32(0): + b6 = cutlass.Int32(0) + if b6 > cutlass.Int32(kBins - 1): + b6 = cutlass.Int32(kBins - 1) + if b6 == b_star: + s6 = cutlass.Int32((v6 - f_lo) * finv) + if s6 < cutlass.Int32(0): + s6 = cutlass.Int32(0) + if s6 > cutlass.Int32(fbins - 1): + s6 = cutlass.Int32(fbins - 1) + if s6 == sb_star: + k6 = f32_order_key(v6) ^ cutlass.Int32(-2147483648) + if k6 < kmn6: + kmn6 = k6 + if k6 > kmx6: + kmx6 = k6 + it6 = it6 + cutlass.Int32(num_threads) + kmn6 = cute.arch.warp_redux_sync(kmn6, "min") + kmx6 = cute.arch.warp_redux_sync(kmx6, "max") + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = kmn6 + smem_hist[warp_id] = kmx6 + cute.arch.barrier() + kmn7 = cutlass.Int32(2147483647) + kmx7 = cutlass.Int32(-2147483648) + for w8 in cutlass.range_constexpr(self.num_warps): + pa8 = smem_wcnt[w8] + pb8 = smem_hist[w8] + if pa8 < kmn7: + kmn7 = pa8 + if pb8 > kmx7: + kmx7 = pb8 + if kmn7 == kmx7: + fast_done = cutlass.Int32(1) + if fast_done == cutlass.Int32(0): + # [p4tt-v3] mixed class: compact it IN PLACE + # into smem_keys/vals[0..cnt_strad) with a + # register-buffered two-phase pass (every + # thread reads its strided candidates first, + # ONE barrier, then claimed compact writes — + # no read/write overlap by construction). The + # candidate array has no readers after the + # tail, and compaction makes the repair cost a + # function of the CLASS size only, for ANY + # class size up to cand_count (the old full- + # candidate radix fallback is gone). if tidx == cutlass.Int32(0): s_iscalars[0] = cutlass.Int32(0) + nh7 = cutlass.Int32(0) + it7 = tidx + while it7 < cand_count: + v7 = smem_keys[it7] + b7 = cutlass.Int32((v7 - bmin_r) * inv1) + if b7 < cutlass.Int32(0): + b7 = cutlass.Int32(0) + if b7 > cutlass.Int32(kBins - 1): + b7 = cutlass.Int32(kBins - 1) + if b7 == b_star: + s7 = cutlass.Int32((v7 - f_lo) * finv) + if s7 < cutlass.Int32(0): + s7 = cutlass.Int32(0) + if s7 > cutlass.Int32(fbins - 1): + s7 = cutlass.Int32(fbins - 1) + if s7 == sb_star: + # static predicated fragment write + # (dodges dynamic register indexing) + for sl7 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl7) == nh7: + rv7[sl7] = v7 + ri7[sl7] = smem_vals[it7] + nh7 = nh7 + cutlass.Int32(1) + it7 = it7 + cutlass.Int32(num_threads) cute.arch.barrier() - itc = tidx - while itc < cand_count: - tv = smem_keys[itc] - tb = cutlass.Int32((tv - bmin_r) * inv1) - if tb < cutlass.Int32(0): - tb = cutlass.Int32(0) - if tb > cutlass.Int32(kBins - 1): - tb = cutlass.Int32(kBins - 1) - if tb == b_star: - ts = cutlass.Int32((tv - f_lo) * finv) - if ts < cutlass.Int32(0): - ts = cutlass.Int32(0) - if ts > cutlass.Int32(fbins - 1): - ts = cutlass.Int32(fbins - 1) - if ts == sb_star: - to = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), cutlass.Int32(1) - ) - if to < cutlass.Int32(128): - smem_hist[to + to] = float_as_int32(tv) - smem_hist[to + to + cutlass.Int32(1)] = smem_vals[itc] - itc = itc + cutlass.Int32(num_threads) - cute.arch.barrier() - # [p4tt] thread0 exact top-need0 select rewriting - # positions [rank_above_fine, kK). Consumed flag = - # the cand_idx slot set to -1 (indices are always - # >= 0), so a genuine -FLT_MAX value in the class - # remains selectable (no value sentinel). Ties - # (bit-equal values) pick arbitrarily: value-set - # exact. - if tidx == cutlass.Int32(0): - tj = cutlass.Int32(0) - while tj < need0: - tbv = cutlass.Float32(self.NEG_FLT_MAX) - tbi = cutlass.Int32(-1) - ti = cutlass.Int32(0) - while ti < cnt_strad: - tvi = smem_hist[ti + ti + cutlass.Int32(1)] - if tvi >= cutlass.Int32(0): - tvb = smem_hist[ti + ti] - tvv = cutlass.Float32( - llvm.bitcast( - cutlass.Float32.mlir_type, - tvb.ir_value(), - ) - ) - take = cutlass.Int32(0) - if tbi < cutlass.Int32(0): - take = cutlass.Int32(1) - elif tvv > tbv: - take = cutlass.Int32(1) - if take == cutlass.Int32(1): - tbv = tvv - tbi = ti - ti = ti + cutlass.Int32(1) - pos = rank_above_fine + tj - if cutlass.const_expr(self.return_output_values): - output_values_row[pos] = self.dtype(tbv) - output_indices_row[pos] = smem_hist[ - tbi + tbi + cutlass.Int32(1) - ] - smem_hist[tbi + tbi + cutlass.Int32(1)] = cutlass.Int32(-1) - tj = tj + cutlass.Int32(1) - cute.arch.barrier() - else: - # Persistent scalars live above the 256 digit bins - # (kNumBins >= 512 always): [256] key prefix (chosen - # digits, remaining bits 0), [257] slots still to fill - # inside the current equal-prefix set, [258] ties - # strictly above the prefix (their slots precede it). - if tidx == cutlass.Int32(0): - smem_hist[256] = cutlass.Int32(0) - smem_hist[257] = need0 - smem_hist[258] = cutlass.Int32(0) + # warp-aggregated claim: intra-warp exclusive + # prefix via shfl scan + ONE atomic per warp + # (a thousand same-address claims serialize + # and scale with the class size) + pf7 = nh7 + for so3 in cutlass.range_constexpr(5): + oth3 = cute.arch.shuffle_sync_up( + pf7, cutlass.Int32(1 << so3), mask_and_clamp=0 + ) + if lane >= cutlass.Int32(1 << so3): + pf7 = pf7 + oth3 + tot7 = cute.arch.shuffle_sync(pf7, cutlass.Int32(31)) + wb7 = cutlass.Int32(0) + if lane == cutlass.Int32(31): + if tot7 > cutlass.Int32(0): + wb7 = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), tot7) + wb7 = cute.arch.shuffle_sync(wb7, cutlass.Int32(31)) + bs7 = wb7 + pf7 - nh7 + for sl8 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl8) < nh7: + smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] + smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] cute.arch.barrier() - for lvl in cutlass.range_constexpr(4): - shift = cutlass.const_expr(24 - 8 * lvl) - iz2 = tidx - while iz2 < cutlass.Int32(256): - smem_hist[iz2] = cutlass.Int32(0) - iz2 = iz2 + cutlass.Int32(num_threads) - cute.arch.barrier() - uthr_cur = smem_hist[256] - it2 = tidx - while it2 < cand_count: - vt = smem_keys[it2] - bt = cutlass.Int32((vt - bmin_r) * inv1) - if bt < cutlass.Int32(0): - bt = cutlass.Int32(0) - if bt > cutlass.Int32(kBins - 1): - bt = cutlass.Int32(kBins - 1) - if bt == b_star: - st2 = cutlass.Int32((vt - f_lo) * finv) - if st2 < cutlass.Int32(0): - st2 = cutlass.Int32(0) - if st2 > cutlass.Int32(fbins - 1): - st2 = cutlass.Int32(fbins - 1) - if st2 == sb_star: - uk = f32_order_key(vt) - pmatch = cutlass.Int32(1) - if cutlass.const_expr(lvl > 0): - if (uk >> cutlass.Int32(shift + 8)) != ( - uthr_cur >> cutlass.Int32(shift + 8) - ): - pmatch = cutlass.Int32(0) - if pmatch == cutlass.Int32(1): - dg = (uk >> cutlass.Int32(shift)) & cutlass.Int32( - 0xFF - ) - atomicAdd(smem_hist.iterator + dg, cutlass.Int32(1)) - it2 = it2 + cutlass.Int32(num_threads) + if cnt_strad <= cutlass.Int32(128): + # warp0 exact pairwise rank (rank = #{key + # greater} + #{key equal, earlier slot} is + # unique in [0, class)) rewrites every + # winner slot in [raf, raf + need0) once. + if warp_id == cutlass.Int32(0): + ie5 = lane + while ie5 < cnt_strad: + vi5 = smem_keys[ie5] + ki5 = f32_order_key(vi5) ^ cutlass.Int32(-2147483648) + r5 = cutlass.Int32(0) + j5 = cutlass.Int32(0) + while j5 < cnt_strad: + vj5 = smem_keys[j5] + kj5 = f32_order_key(vj5) ^ cutlass.Int32(-2147483648) + if kj5 > ki5: + r5 = r5 + cutlass.Int32(1) + elif kj5 == ki5 and j5 < ie5: + r5 = r5 + cutlass.Int32(1) + j5 = j5 + cutlass.Int32(1) + if r5 < need0: + pos = rank_above_fine + r5 + if pos < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[pos] = self.dtype(vi5) + output_indices_row[pos] = smem_vals[ie5] + ie5 = ie5 + cutlass.Int32(32) cute.arch.barrier() - # Two-stage descending digit scan (mirrors the - # fine 3-step search): per-warp partial sums, - # thread0 picks the target warp, its lane0 walks - # the warp's digit range — 2*num_warps serial - # steps instead of 256. - fdw = cutlass.const_expr(256 // self.num_warps) - wsum2 = cutlass.Int32(0) - for jd in cutlass.range_constexpr(fdw): - dix = ( - cutlass.Int32(255) - - warp_id * cutlass.Int32(fdw) - - cutlass.Int32(jd) - ) - wsum2 = wsum2 + smem_hist[dix] - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = wsum2 + else: + # block-parallel 4-level MSB radix over the + # compacted class (scans touch class pairs + # only; warp0 shuffle-scan digit search — 3 + # block barriers per level instead of 5). + if tidx == cutlass.Int32(0): + smem_hist[256] = cutlass.Int32(0) + smem_hist[257] = need0 + smem_hist[258] = cutlass.Int32(0) cute.arch.barrier() + for lvl2 in cutlass.range_constexpr(4): + shift2 = cutlass.const_expr(24 - 8 * lvl2) + iz3 = tidx + while iz3 < cutlass.Int32(256): + smem_hist[iz3] = cutlass.Int32(0) + iz3 = iz3 + cutlass.Int32(num_threads) + cute.arch.barrier() + uthr_c2 = smem_hist[256] + ic2 = tidx + while ic2 < cnt_strad: + uk3 = f32_order_key(smem_keys[ic2]) + pm2 = cutlass.Int32(1) + if cutlass.const_expr(lvl2 > 0): + if (uk3 >> cutlass.Int32(shift2 + 8)) != ( + uthr_c2 >> cutlass.Int32(shift2 + 8) + ): + pm2 = cutlass.Int32(0) + if pm2 == cutlass.Int32(1): + dg2 = (uk3 >> cutlass.Int32(shift2)) & cutlass.Int32( + 0xFF + ) + atomicAdd(smem_hist.iterator + dg2, cutlass.Int32(1)) + ic2 = ic2 + cutlass.Int32(num_threads) + cute.arch.barrier() + if warp_id == cutlass.Int32(0): + ws3 = cutlass.Int32(0) + for jd3 in cutlass.range_constexpr(8): + di3 = ( + cutlass.Int32(255) + - lane * cutlass.Int32(8) + - cutlass.Int32(jd3) + ) + ws3 = ws3 + smem_hist[di3] + pre6 = ws3 + for so2 in cutlass.range_constexpr(5): + oth2 = cute.arch.shuffle_sync_up( + pre6, + cutlass.Int32(1 << so2), + mask_and_clamp=0, + ) + if lane >= cutlass.Int32(1 << so2): + pre6 = pre6 + oth2 + needl3 = smem_hist[257] + if pre6 >= needl3 and (pre6 - ws3) < needl3: + base5 = pre6 - ws3 + dstar2 = cutlass.Int32(0) + above5 = base5 + sd5 = cutlass.Int32(0) + for jd4 in cutlass.range_constexpr(8): + di4 = ( + cutlass.Int32(255) + - lane * cutlass.Int32(8) + - cutlass.Int32(jd4) + ) + ra5 = base5 + base5 = base5 + smem_hist[di4] + if base5 >= needl3 and sd5 == cutlass.Int32(0): + dstar2 = di4 + above5 = ra5 + sd5 = cutlass.Int32(1) + smem_hist[256] = uthr_c2 | ( + dstar2 << cutlass.Int32(shift2) + ) + smem_hist[257] = needl3 - above5 + smem_hist[258] = smem_hist[258] + above5 + cute.arch.barrier() + u_thr2 = smem_hist[256] + cnt_ab2 = smem_hist[258] + need_eq2 = smem_hist[257] + kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) if tidx == cutlass.Int32(0): - needl = smem_hist[257] - cw = cutlass.Int32(0) - tw3 = cutlass.Int32(num_warps - 1) - f3 = cutlass.Int32(0) - for w4 in cutlass.range_constexpr(self.num_warps): - cw = cw + smem_wcnt[w4] - if cw >= needl and f3 == cutlass.Int32(0): - tw3 = cutlass.Int32(w4) - f3 = cutlass.Int32(1) - pre3 = cutlass.Int32(0) - for w5 in cutlass.range_constexpr(self.num_warps): - if cutlass.Int32(w5) < tw3: - pre3 = pre3 + smem_wcnt[w5] - s_iscalars[4] = pre3 # prefix above target warp - s_iscalars[0] = tw3 # target warp + s_iscalars[4] = cutlass.Int32(0) + s_iscalars[0] = cutlass.Int32(0) cute.arch.barrier() - pre4 = s_iscalars[4] - tw4 = s_iscalars[0] - if warp_id == tw4 and lane == cutlass.Int32(0): - needl2 = smem_hist[257] - base4 = pre4 - dstar = cutlass.Int32(0) - above_d = pre4 - sd4 = cutlass.Int32(0) - for jd2 in cutlass.range_constexpr(fdw): - dix2 = ( - cutlass.Int32(255) - - tw4 * cutlass.Int32(fdw) - - cutlass.Int32(jd2) + ir3 = tidx + while ir3 < cnt_strad: + vv3 = smem_keys[ir3] + uk4 = f32_order_key(vv3) + ks4 = uk4 ^ cutlass.Int32(-2147483648) + if ks4 > kthr2: + o4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(4), + cutlass.Int32(1), ) - ra4 = base4 - base4 = base4 + smem_hist[dix2] - if base4 >= needl2 and sd4 == cutlass.Int32(0): - dstar = dix2 - above_d = ra4 - sd4 = cutlass.Int32(1) - smem_hist[256] = uthr_cur | (dstar << cutlass.Int32(shift)) - smem_hist[257] = needl2 - above_d - smem_hist[258] = smem_hist[258] + above_d - cute.arch.barrier() - # Rewrite the tie slot range: ties with key > u_thr - # first (there are exactly cnt_ab of them), then the - # first need_eq bitwise-equal-to-u_thr ties in arrival - # order (value-exact by construction). Signed compare - # needs the top bit flipped (unsigned-monotonic key). - u_thr = smem_hist[256] - cnt_ab = smem_hist[258] - need_eq = smem_hist[257] - ks_thr = u_thr ^ cutlass.Int32(-2147483648) - if tidx == cutlass.Int32(0): - s_iscalars[4] = cutlass.Int32(0) # above-writer ctr - s_iscalars[0] = cutlass.Int32(0) # equal-writer ctr - cute.arch.barrier() - ir2 = tidx - while ir2 < cand_count: - vr = smem_keys[ir2] - br = cutlass.Int32((vr - bmin_r) * inv1) - if br < cutlass.Int32(0): - br = cutlass.Int32(0) - if br > cutlass.Int32(kBins - 1): - br = cutlass.Int32(kBins - 1) - if br == b_star: - sr = cutlass.Int32((vr - f_lo) * finv) - if sr < cutlass.Int32(0): - sr = cutlass.Int32(0) - if sr > cutlass.Int32(fbins - 1): - sr = cutlass.Int32(fbins - 1) - if sr == sb_star: - uk2 = f32_order_key(vr) - ks2 = uk2 ^ cutlass.Int32(-2147483648) - if ks2 > ks_thr: - o2 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(4), - cutlass.Int32(1), - ) - pos = rank_above_fine + o2 + pos = rank_above_fine + o4 + if pos < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[pos] = self.dtype(vv3) + output_indices_row[pos] = smem_vals[ir3] + elif ks4 == kthr2: + q4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if q4 < need_eq2: + pos = rank_above_fine + cnt_ab2 + q4 if pos < cutlass.Int32(kK): if cutlass.const_expr(self.return_output_values): - output_values_row[pos] = self.dtype(vr) - output_indices_row[pos] = smem_vals[ir2] - elif ks2 == ks_thr: - q2 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), - cutlass.Int32(1), - ) - if q2 < need_eq: - pos = rank_above_fine + cnt_ab + q2 - if pos < cutlass.Int32(kK): - if cutlass.const_expr( - self.return_output_values - ): - output_values_row[pos] = self.dtype(vr) - output_indices_row[pos] = smem_vals[ir2] - ir2 = ir2 + cutlass.Int32(num_threads) - cute.arch.barrier() + output_values_row[pos] = self.dtype(vv3) + output_indices_row[pos] = smem_vals[ir3] + ir3 = ir3 + cutlass.Int32(num_threads) + cute.arch.barrier() elif cutlass.const_expr(self.p4_exact_tail): # [p4tt] if->elif only need0 = cutlass.Int32(kK) - rank_above_fine if cnt_strad > need0 and need0 > cutlass.Int32(0): @@ -3628,6 +3674,17 @@ def phase4_rank_scatter( output_values_row[ipad] = self.dtype(self.NEG_FLT_MAX) output_indices_row[ipad] = cutlass.Int32(-1) ipad = ipad + cutlass.Int32(num_threads) + if cutlass.const_expr(_P4_SUB_DBG): + # smem_wcnt slots [8..13] are dead after the last warp-sum + # use above; the take-block publish copies them to xstate. + sc6 = cute.arch.clock64() + if tidx == cutlass.Int32(0): + smem_wcnt[8] = cutlass.Int32(sc1 - sc0) # minmax + smem_wcnt[9] = cutlass.Int32(sc2 - sc1) # hist build + smem_wcnt[10] = cutlass.Int32(sc3 - sc2) # coarse search + smem_wcnt[11] = cutlass.Int32(sc4 - sc3) # fine recursion + smem_wcnt[12] = cutlass.Int32(sc5 - sc4) # scatter + smem_wcnt[13] = cutlass.Int32(sc6 - sc5) # tail repair+pad else: i10 = tidx while i10 < cand_count: @@ -5779,6 +5836,10 @@ def _run_phases( warp_id, lane, ) + ck_sw0 = cutlass.Int64(0) + ck_sw1 = cutlass.Int64(0) + if cutlass.const_expr(_P4_SUB_DBG): + ck_sw0 = cute.arch.clock64() if cutlass.const_expr( self.use_ext_cand and self.use_ext_counts and self.dtype == cutlass.Float32 ): @@ -5806,6 +5867,8 @@ def _run_phases( )[0] io_r = io_r + cutlass.Int32(num_threads) cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + ck_sw1 = cute.arch.clock64() if cutlass.const_expr(self.emit_xstate): # Closed-loop state (interface v2): [0] valid, [1] kth # proxy (= accepted threshold; the tie-fill makes it a @@ -5832,6 +5895,19 @@ def _run_phases( xstate_row[5] = cutlass.Float32(cutlass.Int32(ck2 - ck1)) # P2/P3 gap xstate_row[6] = cutlass.Float32(cutlass.Int32(ck3 - ck2)) # Phase 4 xstate_row[7] = s_thr[1] # cnt_strad + if cutlass.const_expr(_P4_SUB_DBG): + # P4 sub-phase cycles staged by rank_scatter. + # Chain-safe layout: [2] (closed-loop anchor) + # untouched; [1] cnt_strad (tail class size), + # [4] fine, [5] scatter, [6] tail, [7] deferred- + # position swap. The small C-predictable phases + # (minmax/hist/coarse, wcnt[8..10]) are not + # published. + xstate_row[1] = s_thr[1] + xstate_row[4] = cutlass.Float32(smem_wcnt[11]) + xstate_row[5] = cutlass.Float32(smem_wcnt[12]) + xstate_row[6] = cutlass.Float32(smem_wcnt[13]) + xstate_row[7] = cutlass.Float32(cutlass.Int32(ck_sw1 - ck_sw0)) # cand_count_p4 = pre-P4 snapshot (P4 repurposes # the s_iscalars slots). xstate_row[3] = cutlass.Float32(cand_count_p4) From da75cd82f18aed4b22b630a58d155d29a71d4635 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:35:55 -0700 Subject: [PATCH 022/117] [None][feat] GVR self_scan: fused self-contained closed-loop top-k mode New self_scan mode: the kernel itself streams the row ONCE against the three closed-loop seed lines and buckets candidates on the fly - no external emitter, no indexer-side changes, no gmem candidate values. One CTA per row, four phases: (0) scan-bucket - VALUES land in on-chip segments (A/B/C at bases 0/B*/2B*, values-only 4B/entry, spill to the looser segment, cursor totals ARE the line counts), POSITIONS stream to a write-only gmem column reusing the cand_idx slot; (1) the v5 cut state machine unchanged (a line cut compacts winning segment runs to the smem prefix and fills smem_vals with segment coordinates, so P4, the tail repair and the deferred K-gather run verbatim); ineligible rows take the stock in-kernel fallback. Scan-loop lessons baked in (each measured): per-element warp ballots serialize every load (~1.8us/k); 16-wide register lists spill at 1024 threads (64 regs/thread ceiling) - values re-read from the load fragments, positions derived arithmetically, classes recomputed; warp-collective claim prefixes cap in-flight loads at 2/warp (ncu: 0.19% memory throughput) - final form claims passers with per-element smem atomics, which do not synchronize the warp and hide under the read stream (0.13us/k comp). Exactness: 25-cell REPORT-S4 dataset x B in {1,2,4,8} = 100/100 bit-exact (flash/pro/v32 incl. K=2048, tiny-N and straddle fallbacks). Perf vs PR16457 tip (same node, cold kernel-sum): geomean 0.64-0.71x, short rows 0.8-1.05x, long rows 0.4-0.8x - the single-CTA read wall by design; stage 2 (block-max skip) attacks the read itself. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 308 +++++++++++++++++- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 42 ++- 2 files changed, 345 insertions(+), 5 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index c9b3a6800f40..194a2c1bd599 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -269,6 +269,8 @@ def __init__( enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, + self_scan: bool = False, + cap_c: "int | None" = None, r0_qfracs: Optional[tuple] = None, mt_unroll: int = 4, p1b_cache: Optional[bool] = None, @@ -590,6 +592,38 @@ def __init__( # segment that can void). self.list_cap = max(0, int(cand_cap) - 2 * self.accept_cap) self.cand_rung = int(cand_rung) + # self_scan (fused self-contained mode): the kernel itself streams + # the row ONCE against the three closed-loop lines, bucketing + # VALUES into on-chip segments (A >= t2 / B [t1,t2) / C [t0,t1), + # bases 0 / accept_cap / 2*accept_cap inside an enlarged + # smem_keys) and POSITIONS into the cand_idx tensor (write-only + # until the deferred K-gather). No external emitter, no candidate + # value column in gmem. A line cut compacts the winning segment + # runs to the smem_keys prefix and fills smem_vals with each + # entry's SEGMENT COORDINATE — from there on the v5 consumer + # (P4, tail repair, deferred gather via cand_idx[coord]) runs + # unchanged. Ineligible rows take the stock fallback, whose + # P3/P4 use smem_keys[:kC]/smem_vals verbatim. + self.self_scan = bool(self_scan) + if self.self_scan: + if not use_ext_counts: + raise ValueError("self_scan requires use_ext_counts") + if use_ext_cand: + raise ValueError("self_scan and use_ext_cand are exclusive") + if dtype != cutlass.Float32: + raise ValueError("self_scan is fp32-only (v1)") + if self.enable_smem_cache: + raise ValueError( + "self_scan and enable_smem_cache exceed the CTA smem budget together" + ) + # on-chip segment budget: values only, 4B/entry; C sized so + # keys(160KB) + vals(32KB) + hist + scratch stay under the + # 227KB CTA limit with room for the stage-2 skip list. + self.seg_total = 2 * self.accept_cap + int(cap_c if cap_c is not None else 24576) + self.cap_c = self.seg_total - 2 * self.accept_cap + else: + self.seg_total = self.kC + self.cap_c = 0 if use_ext_cand and not use_ext_counts: raise ValueError("use_ext_cand requires use_ext_counts") self.use_ext_counts = bool(use_ext_counts) and bool(enable_r0) @@ -1200,6 +1234,164 @@ def phase1b_hspace_rungs_cached( # → warp reduce → block reduce → s_iscalars[0] = cand_count. # Optionally DSMEM-aggregates across the cluster. # ------------------------------------------------------------------ + @cute.jit + def phase0_scan_bucket( + self, + input_row, # cute.Tensor [N] dtype (fp32; full row, cs==1 only) + N, # int32 valid length (pad tail beyond N is never read) + seed_thr_row, # [3] fp32 closed-loop lines, ascending t0 < t1 < t2 + smem_keys, # [seg_total] fp32 value segments (A @0 / B @segA / C @2segA) + cand_idx_row, # [seg_total] int32 gmem POSITION column (write-only here) + s_seg, # [>=7] int32 scratch (reuses smem_wcnt_p1: P1 never runs + # on a row this phase succeeded on): [0..2] A/B/C claim + # cursors, [3] void, [4] n0, [5] n1, [6] n2 + tidx, + warp_id, + lane, + ): + """self_scan phase 0: ONE streaming pass buckets every element + >= t0 into the on-chip value segments (tightest line passed picks + the segment; a full segment spills to the next looser one) and + writes each entry's POSITION to the same coordinate of the gmem + column. The final cursor values ARE the line counts (attempts, + uncapped), so {n0, void, n1, n2} fall out for free — the same + contract the v5 emitter produced externally. + + Perf shape (v3): per round each thread front-loads TWO vec_w + vectors (independent LDGs, latency overlapped), classifies + branchlessly in registers, and the whole round pays ONE packed + warp shfl-prefix (all three segment counts in 10-bit fields) + + at most three warp atomics. Segment overflow is marked and + resolved by a RARE per-element direct-atomic pass (a segment + overflows at most once per row, and divergent scalar atomics + need no warp coordination).""" + num_threads = cutlass.const_expr(self.num_threads) + segA = cutlass.const_expr(self.accept_cap) + capC = cutlass.const_expr(self.cap_c) + vec_w = cutlass.const_expr(self.vec_bits // self.dtype.width) + elem_bytes = cutlass.const_expr(self.dtype.width // 8) + vec_align = cutlass.const_expr(self.vec_align_bytes) + if tidx == cutlass.Int32(0): + s_seg[0] = cutlass.Int32(0) + s_seg[1] = cutlass.Int32(0) + s_seg[2] = cutlass.Int32(0) + cute.arch.barrier() + t0_s = seed_thr_row[0] + t1_s = seed_thr_row[1] + t2_s = seed_thr_row[2] + row_addr = input_row.iterator.toint() + copy_atom = self._make_load_copy_atom() + frag_a = cute.make_fragment((vec_w,), self.dtype) + frag_b = cute.make_fragment((vec_w,), self.dtype) + step1 = cutlass.const_expr(num_threads * vec_w) + step2 = cutlass.const_expr(2 * step1) + st2log = cutlass.const_expr((2 * step1).bit_length() - 1) + # FULL-vector rounds only in the hot loop: nfull is warp-uniform + # (every lane's two vectors are in bounds by construction), so the + # warp collectives inside are legal and the hot path carries no + # bounds checks at all. The remainder (< 2*step1 elements) takes + # the scalar tail below with per-element direct atomics. + nfull = N >> st2log + it0 = cutlass.Int32(0) + while it0 < nfull: + ia0 = (it0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( + vec_w + ) + ib0 = ia0 + cutlass.Int32(step1) + for _fq in cutlass.range_constexpr(2): + fq0 = ia0 if _fq == 0 else ib0 + pq0 = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(fq0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + copy_atom, + cute.make_tensor(pq0, cute.make_layout((vec_w,))), + frag_a if _fq == 0 else frag_b, + ) + # v6: per-element DIRECT atomic claims for passers — smem + # atomics do NOT synchronize the warp, so the vector loads of + # later rounds keep flowing (the packed shfl-prefix design + # capped in-flight loads at 2/warp: ncu showed 0.19% memory + # throughput, pure latency bound). Same-address service is + # ~0.5ns effective and n0 is line-bounded, so the claim queue + # hides completely under the read stream. + for _jh in cutlass.range_constexpr(2): + for _jv in cutlass.range_constexpr(vec_w): + v0 = cutlass.Float32(frag_a[_jv]) if _jh == 0 else cutlass.Float32(frag_b[_jv]) + if v0 >= t0_s: + pos0 = (ia0 if _jh == 0 else ib0) + cutlass.Int32(_jv) + c0 = cutlass.Int32(2) + if v0 >= t1_s: + c0 = cutlass.Int32(1) + if v0 >= t2_s: + c0 = cutlass.Int32(0) + while c0 >= cutlass.Int32(0) and c0 <= cutlass.Int32(2): + cap0 = cutlass.Int32(segA) + if c0 == cutlass.Int32(2): + cap0 = cutlass.Int32(capC) + sl0 = atomicAdd(s_seg.iterator + c0, cutlass.Int32(1)) + if sl0 < cap0: + cd0 = c0 * cutlass.Int32(segA) + sl0 + smem_keys[cd0] = v0 + cand_idx_row[cd0] = pos0 + c0 = cutlass.Int32(-1) + else: + c0 = c0 + cutlass.Int32(1) + it0 = it0 + cutlass.Int32(1) + # scalar tail (< 2*step1 elements): per-element DIRECT atomic + # claims — divergent-safe, no warp collectives + pt0 = (N >> st2log) * cutlass.Int32(step2) + tidx + while pt0 < N: + spt = cute.make_ptr( + cutlass.Float32, + row_addr + cutlass.Int64(pt0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + vt0 = cute.make_tensor(spt, cute.make_layout((1,)))[0] + if vt0 >= t0_s: + ct0 = cutlass.Int32(2) + if vt0 >= t1_s: + ct0 = cutlass.Int32(1) + if vt0 >= t2_s: + ct0 = cutlass.Int32(0) + while ct0 >= cutlass.Int32(0) and ct0 <= cutlass.Int32(2): + capt = cutlass.Int32(segA) + if ct0 == cutlass.Int32(2): + capt = cutlass.Int32(capC) + slt = atomicAdd(s_seg.iterator + ct0, cutlass.Int32(1)) + if slt < capt: + cdt = ct0 * cutlass.Int32(segA) + slt + smem_keys[cdt] = vt0 + cand_idx_row[cdt] = pt0 + ct0 = cutlass.Int32(-1) + else: + ct0 = ct0 + cutlass.Int32(1) + pt0 = pt0 + cutlass.Int32(num_threads) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + curA0 = s_seg[0] + curB0 = s_seg[1] + curC0 = s_seg[2] + spA0 = curA0 - cutlass.Int32(segA) + if spA0 < cutlass.Int32(0): + spA0 = cutlass.Int32(0) + spB0 = curB0 - cutlass.Int32(segA) + if spB0 < cutlass.Int32(0): + spB0 = cutlass.Int32(0) + n1_0 = curA0 + curB0 - spA0 + n0_0 = n1_0 + curC0 - spB0 + s_seg[3] = cutlass.Int32(0) + if curC0 > cutlass.Int32(capC): + s_seg[3] = cutlass.Int32(1) + s_seg[4] = n0_0 + s_seg[5] = n1_0 + s_seg[6] = curA0 + cute.arch.barrier() + @cute.jit def block_count_ge( self, @@ -4387,6 +4579,12 @@ def run_one_row( cand_vals_row = cand_vals[row_idx, None] cand_idx_row = cand_idx[row_idx, None] cand_ctl_row = cand_ctl[row_idx, None] + elif cutlass.const_expr(self.self_scan and cand_idx is not None): + # self_scan: only the POSITION column exists (values live in + # smem from birth; counts come from the phase-0 cursors) + cand_vals_row = None + cand_idx_row = cand_idx[row_idx, None] + cand_ctl_row = None else: cand_vals_row = None cand_idx_row = None @@ -4407,12 +4605,18 @@ def run_one_row( smem = SmemAllocator() # keys[kC] fp32 (P3 candidate values; smem keys always fp32 even for half-prec) # Use fp32 even for half-prec to make secant search algorithm keep the accuracy/precision and converge faster. + # self_scan: enlarged to seg_total (three value segments at bases + # 0 / accept_cap / 2*accept_cap); every later consumer only ever + # touches a <= kC prefix after cut compaction. smem_keys = smem.allocate_tensor( element_type=cutlass.Float32, - layout=cute.make_ordered_layout((kC,), order=(0,)), + layout=cute.make_ordered_layout((cutlass.const_expr(self.seg_total),), order=(0,)), byte_alignment=128, ) - # vals[kC] int32 (P3 candidate indices) + # vals[kC] int32 (P3 candidate indices). self_scan: holds the + # SEGMENT COORDINATE of each compacted candidate (identity for the + # deferred position gather via cand_idx[coord]) — every consumer + # (P4, tail repair, gather) works unchanged. smem_vals = smem.allocate_tensor( element_type=cutlass.Int32, layout=cute.make_ordered_layout((kC,), order=(0,)), @@ -4900,6 +5104,34 @@ def _run_phases( and seed_thr_row[0] < cutlass.Float32(1e37) ): ext_row = cutlass.Int32(1) + # ---- self_scan phase 0: fused scan-bucket ---- + # The kernel streams the row itself (no external emitter); + # eligibility mirrors the list contract: nothing dropped + # (void == 0) and the loosest line provably covers the top-K + # (n0 >= K, exact counts - no sentinel slack needed). The + # seed-thr finite guard keeps the branch CTA-uniform, so the + # barriers/ballots inside phase 0 stay convergent. Cursor + # scratch = smem_wcnt_p1 (P1 only runs when this row is NOT + # taken, so the reuse never overlaps live data). + if cutlass.const_expr( + self.self_scan and cluster_size == 1 and self.dtype == cutlass.Float32 + ): + if seed_thr_row[0] < cutlass.Float32(1e37): + self.phase0_scan_bucket( + input_row, + N, + seed_thr_row, + smem_keys, + cand_idx_row, + smem_wcnt_p1, + tidx, + warp_id, + lane, + ) + if smem_wcnt_p1[3] == cutlass.Int32(0) and smem_wcnt_p1[4] >= cutlass.Int32( + self.top_k + ): + ext_row = cutlass.Int32(1) # ---- Phase 1: preIdx Min/Max/Mean ---- # ext counts: P1's only surviving products are the [v_lo, v_hi] @@ -5377,6 +5609,74 @@ def _run_phases( if cutlass.const_expr(_P4_TAIL_DBG): ck1 = cute.arch.clock64() + # ---- self_scan take: cut straight from the phase-0 cursors ---- + # Same admission state machine as the v5 list (tightest line + # whose count fits [K, B*] wins; anchor = loosest in-band + # line), but the candidates already LIVE in smem: a line cut + # is a same-buffer run compaction (sources at >= segA, + # destinations below it - disjoint) plus the segment-coordinate + # fill of smem_vals that the unchanged P4 / tail repair / + # deferred position gather consume. Straddle / overshoot rows + # (no line in band) fall through to the stock fallback (v1). + if cutlass.const_expr( + self.self_scan and cluster_size == 1 and self.dtype == cutlass.Float32 + ): + segA_f = cutlass.const_expr(self.accept_cap) + if cutlass.const_expr(_P4_TAIL_DBG): + ck0 = cute.arch.clock64() + ck1 = ck0 + n0_f = smem_wcnt_p1[4] + n1_f = smem_wcnt_p1[5] + n2_f = smem_wcnt_p1[6] + kK_f = cutlass.Int32(self.top_k) + bs_f = cutlass.Int32(segA_f) + cut_n = cutlass.Int32(0) + have_f = cutlass.Int32(0) + anch_f = cutlass.Float32(0.0) + if ext_row == cutlass.Int32(1): + if n2_f >= kK_f and n2_f <= bs_f: + if have_f == cutlass.Int32(0): + cut_n = n2_f + anch_f = seed_thr_row[2] + have_f = cutlass.Int32(1) + if n1_f >= kK_f and n1_f <= bs_f: + if have_f == cutlass.Int32(0): + cut_n = n1_f + anch_f = seed_thr_row[1] + have_f = cutlass.Int32(1) + if n0_f <= bs_f: + if have_f == cutlass.Int32(0): + cut_n = n0_f + anch_f = seed_thr_row[0] + have_f = cutlass.Int32(1) + if have_f == cutlass.Int32(1): + take_cand = cutlass.Int32(1) + list_used = cutlass.Int32(1) + if tidx == cutlass.Int32(0): + s_iscalars[0] = cut_n + s_thr[0] = anch_f + s_iscalars[1] = cutlass.Int32(1) # done + cute.arch.barrier() + # line cut => no segment spilled (n_cut <= B* bounds + # every tighter count too) => lenA = n2, lenB = n1-n2 + j_f = tidx + while j_f < cut_n: + for _jw in cutlass.range_constexpr(4): + q_f = j_f + cutlass.Int32(_jw * num_threads) + if q_f < cut_n: + src_f = q_f + if q_f >= n2_f: + src_f = cutlass.Int32(segA_f) + q_f - n2_f + if q_f >= n1_f: + src_f = cutlass.Int32(2 * segA_f) + q_f - n1_f + if src_f != q_f: + smem_keys[q_f] = smem_keys[src_f] + smem_vals[q_f] = src_f + j_f = j_f + cutlass.Int32(4 * num_threads) + cute.arch.barrier() + if cutlass.const_expr(_P4_TAIL_DBG): + ck1 = cute.arch.clock64() + if take_cand == cutlass.Int32(0): # Stage this CTA's slice into SMEM once before Phase 2's # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses @@ -5841,7 +6141,9 @@ def _run_phases( if cutlass.const_expr(_P4_SUB_DBG): ck_sw0 = cute.arch.clock64() if cutlass.const_expr( - self.use_ext_cand and self.use_ext_counts and self.dtype == cutlass.Float32 + (self.use_ext_cand or self.self_scan) + and self.use_ext_counts + and self.dtype == cutlass.Float32 ): # List rows: the compact stored LIST INDICES in the # vals slots (saving a second cold gmem pass over the diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 70a64d2e0e28..9f7e5ac0997d 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -72,6 +72,8 @@ def _compile( cand_cap: int = 5120, accept_cap: "int | None" = None, kc_override: "int | None" = None, + self_scan: bool = False, + cap_c: "int | None" = None, ): """JIT-compile the GVR kernel for a specific knob combination. @@ -165,7 +167,7 @@ def _compile( cute.runtime.make_fake_compact_tensor( cutlass.Int32, (n_rows, cand_cap), stride_order=(1, 0), assumed_align=4 ) - if use_ext_cand + if (use_ext_cand or self_scan) else None ) cand_ctl_fake = ( @@ -207,6 +209,8 @@ def _compile( cand_cap=cand_cap, accept_cap=accept_cap, kc_override=kc_override, + self_scan=self_scan, + cap_c=cap_c, # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The # qfrac VALUES are irrelevant on this path (P1b is skipped) — only # the slot count matters. @@ -647,6 +651,8 @@ def gvr_topk_decode( cand_vals: Optional[torch.Tensor] = None, cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, + self_scan: bool = False, + cap_c: Optional[int] = None, ) -> tuple[torch.Tensor, torch.Tensor]: """CuTe DSL GVR Top-K wrapper with every tuning knob exposed. @@ -729,6 +735,32 @@ def gvr_topk_decode( # cs1/cs8 vs stock cs8 19.7us) -> keep the stock path. if block_max is not None and num_rows < 8 and top_k > 512: block_max = None + if self_scan: + # fused self-contained mode: kernel scans/buckets the row itself. + # Inputs: seed_thr (three closed-loop lines) + a write-only gmem + # POSITION column passed through the cand_idx slot. seed_counts is + # a dummy (the ext-counts preview reads it but zeros never pass + # the [K, kC] admission, so routing is owned by the phase-0 gate). + assert seed_thr is not None, "self_scan requires seed_thr" + assert cand_vals is None and cand_ctl is None, ( + "self_scan excludes external candidate values/control" + ) + assert "GVR_BSTAR" in os.environ, ( + "self_scan requires GVR_BSTAR (accept_cap) to size the position column" + ) + if seed_counts is None: + seed_counts = torch.zeros((num_rows, 3), dtype=torch.int32, device=logits.device) + _bstar = int(os.environ["GVR_BSTAR"]) + _capc = cap_c if cap_c is not None else int(os.environ.get("GVR_CAPC", "24576")) + _segtot = 2 * _bstar + _capc + if cand_idx is None: + cand_idx = torch.empty((num_rows, _segtot), dtype=torch.int32, device=logits.device) + assert ( + cand_idx.dtype == torch.int32 + and cand_idx.is_cuda + and cand_idx.is_contiguous() + and cand_idx.shape == (num_rows, _segtot) + ), f"self_scan position column must be int32 [num_rows, {_segtot}]" use_ext_counts = seed_thr is not None and seed_counts is not None if use_ext_counts: assert ( @@ -745,6 +777,8 @@ def gvr_topk_decode( ), "seed_counts must be contiguous CUDA int32 [num_rows, 3]" use_ext_cand = cand_vals is not None and cand_idx is not None and cand_ctl is not None cand_cap = 5120 + if self_scan: + cand_cap = cand_idx.shape[1] if use_ext_cand: assert ( cand_vals.dtype == torch.float32 @@ -858,6 +892,10 @@ def gvr_topk_decode( cand_cap, int(os.environ["GVR_BSTAR"]) if "GVR_BSTAR" in os.environ else None, int(os.environ["GVR_KC"]) if "GVR_KC" in os.environ else None, + self_scan, + cap_c + if cap_c is not None + else (int(os.environ.get("GVR_CAPC", "24576")) if self_scan else None), ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. @@ -875,7 +913,7 @@ def gvr_topk_decode( seed_counts if use_ext_counts else None, xstate if emit_xstate else None, cand_vals if use_ext_cand else None, - cand_idx if use_ext_cand else None, + cand_idx if (use_ext_cand or self_scan) else None, cand_ctl if use_ext_cand else None, ) if return_output_values: From 7970473b72104c94a1c7f29c8e12377d27bb5288 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:12:00 -0700 Subject: [PATCH 023/117] [None][feat] GVR self_scan stage 2: block-max skip, tightest-line single band Phase 0 gains a block-skip variant (enable_block_skip + self_scan): per-32-position maxima from the GEMM tail gate whole blocks out of the scan. Measured design pivot: skipping against the LOOSE collection line can never pay (n0/N ~5-12% density -> ~80-99% of blocks contain a passer; benched 0.16-0.39x), so the skip mode collects a SINGLE BAND against the TIGHTEST line (density 0.4-0.8% -> 12-22% pass): only segment A fills, the cursor keeps exact attempt counts, and the v5 state machine runs unchanged fed n0 == n1 == n2 - a cut lands on t2 (common), the sample-hist path absorbs over-B* rows (the A prefix stays a value-blind sample), under-K rows take the stock fallback. The small-batch block_max gate in the wrapper is bypassed for self_scan (stage 2 owns its own skip economics). Exactness: 25-cell REPORT-S4 x B in {1,2,4,8} = 100/100 bit-exact, plus forced under-K fallback cells. Perf state (B=1 vs PR16457 tip, same node): long rows improve markedly over the dense scan (flash 512k 33.9 -> 24.5us = 0.87x of tip; 1024k 46.7 -> 38.7; pro 1M 52 -> 44) while short/mid rows should route to the dense scan (host picks by expected block pass rate). Known remaining work, measured and documented: the block loop is still latency-bound on the bmax stream (8 scalar loads/warp round); a lane-per-block + ballot variant was tried and loses at high pass rates - loop shape per density regime is the open optimization, along with a t2-only closed-loop line-derive for chains. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 106 +++++++++++++++--- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 +- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 194a2c1bd599..9973af5387e0 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1242,6 +1242,7 @@ def phase0_scan_bucket( seed_thr_row, # [3] fp32 closed-loop lines, ascending t0 < t1 < t2 smem_keys, # [seg_total] fp32 value segments (A @0 / B @segA / C @2segA) cand_idx_row, # [seg_total] int32 gmem POSITION column (write-only here) + block_max_row, # [nb_pad] fp32 per-32-position maxima, or None s_seg, # [>=7] int32 scratch (reuses smem_wcnt_p1: P1 never runs # on a row this phase succeeded on): [0..2] A/B/C claim # cursors, [3] void, [4] n0, [5] n1, [6] n2 @@ -1280,6 +1281,61 @@ def phase0_scan_bucket( t1_s = seed_thr_row[1] t2_s = seed_thr_row[2] row_addr = input_row.iterator.toint() + # ---- stage-2 BLOCK-SKIP variant: the GEMM tail left per-32- + # position maxima; a block whose max < t0 contributes nothing to + # any count or segment, so it is never read. One warp per block: + # the bmax compare is warp-uniform (all lanes read the same + # scalar), a passing block is one coalesced 128B load, claims + # are the same non-synchronizing per-element atomics as the + # dense loop - so the block loop needs no uniform trip counts. + if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): + # v8.2: 8 blocks per warp iteration. Every lane vector-loads + # the SAME 8 bmax values (L1 broadcast - the pass decisions + # are warp-uniform registers), then the passing blocks are + # loaded back-to-back (independent 128B coalesced loads, so + # the memory level parallelism the naive one-block loop + # lacked is restored; at high skip rates an iteration is + # just the one 32B bmax vector). + bm_addr = block_max_row.iterator.toint() + nb0 = (N + cutlass.Int32(31)) >> cutlass.Int32(5) + nwp = cutlass.const_expr(self.num_warps) + frag_m = cute.make_fragment((8,), cutlass.Float32) + bg0 = warp_id * cutlass.Int32(8) + while bg0 < nb0: + for _jm in cutlass.range_constexpr(8): + frag_m[_jm] = cutlass.Float32(self.NEG_FLT_MAX) + if bg0 + cutlass.Int32(_jm) < nb0: + bps = cute.make_ptr( + cutlass.Float32, + bm_addr + cutlass.Int64(bg0 + cutlass.Int32(_jm)) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + frag_m[_jm] = cute.make_tensor(bps, cute.make_layout((1,)))[0] + # SINGLE-BAND collection against the TIGHTEST line: the + # loose band's pass fraction (~80% of blocks) can never + # pay for skipping, the accepted band's (~12-22%) can. + # Only segment A is filled; overflow just drops (the + # cursor keeps counting, and the A prefix remains a + # value-blind SAMPLE - the sample-cut path handles the + # over-B* case exactly as with the external list). + for _jb in cutlass.range_constexpr(8): + if cutlass.Float32(frag_m[_jb]) >= t2_s: + pos0 = ((bg0 + cutlass.Int32(_jb)) << cutlass.Int32(5)) + lane + if pos0 < N: + vp0 = cute.make_ptr( + cutlass.Float32, + row_addr + cutlass.Int64(pos0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + v0 = cute.make_tensor(vp0, cute.make_layout((1,)))[0] + if v0 >= t2_s: + sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) + if sl0 < cutlass.Int32(segA): + smem_keys[sl0] = v0 + cand_idx_row[sl0] = pos0 + bg0 = bg0 + cutlass.Int32(nwp * 8) copy_atom = self._make_load_copy_atom() frag_a = cute.make_fragment((vec_w,), self.dtype) frag_b = cute.make_fragment((vec_w,), self.dtype) @@ -1292,6 +1348,8 @@ def phase0_scan_bucket( # bounds checks at all. The remainder (< 2*step1 elements) takes # the scalar tail below with per-element direct atomics. nfull = N >> st2log + if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): + nfull = cutlass.Int32(0) it0 = cutlass.Int32(0) while it0 < nfull: ia0 = (it0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( @@ -1344,6 +1402,8 @@ def phase0_scan_bucket( # scalar tail (< 2*step1 elements): per-element DIRECT atomic # claims — divergent-safe, no warp collectives pt0 = (N >> st2log) * cutlass.Int32(step2) + tidx + if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): + pt0 = N while pt0 < N: spt = cute.make_ptr( cutlass.Float32, @@ -1373,23 +1433,34 @@ def phase0_scan_bucket( pt0 = pt0 + cutlass.Int32(num_threads) cute.arch.barrier() if tidx == cutlass.Int32(0): - curA0 = s_seg[0] - curB0 = s_seg[1] - curC0 = s_seg[2] - spA0 = curA0 - cutlass.Int32(segA) - if spA0 < cutlass.Int32(0): - spA0 = cutlass.Int32(0) - spB0 = curB0 - cutlass.Int32(segA) - if spB0 < cutlass.Int32(0): - spB0 = cutlass.Int32(0) - n1_0 = curA0 + curB0 - spA0 - n0_0 = n1_0 + curC0 - spB0 - s_seg[3] = cutlass.Int32(0) - if curC0 > cutlass.Int32(capC): - s_seg[3] = cutlass.Int32(1) - s_seg[4] = n0_0 - s_seg[5] = n1_0 - s_seg[6] = curA0 + if cutlass.const_expr(self.enable_block_skip): + # single-band mode: every count is the t2 cursor; a cut + # can only land on t2 (in band), the sample-hist (over) + # or the fallback (under) - exactly the v5 state machine + # fed with n0 == n1 == n2 + curT0 = s_seg[0] + s_seg[3] = cutlass.Int32(0) + s_seg[4] = curT0 + s_seg[5] = curT0 + s_seg[6] = curT0 + if cutlass.const_expr(not self.enable_block_skip): + curA0 = s_seg[0] + curB0 = s_seg[1] + curC0 = s_seg[2] + spA0 = curA0 - cutlass.Int32(segA) + if spA0 < cutlass.Int32(0): + spA0 = cutlass.Int32(0) + spB0 = curB0 - cutlass.Int32(segA) + if spB0 < cutlass.Int32(0): + spB0 = cutlass.Int32(0) + n1_0 = curA0 + curB0 - spA0 + n0_0 = n1_0 + curC0 - spB0 + s_seg[3] = cutlass.Int32(0) + if curC0 > cutlass.Int32(capC): + s_seg[3] = cutlass.Int32(1) + s_seg[4] = n0_0 + s_seg[5] = n1_0 + s_seg[6] = curA0 cute.arch.barrier() @cute.jit @@ -5123,6 +5194,7 @@ def _run_phases( seed_thr_row, smem_keys, cand_idx_row, + block_max_row, smem_wcnt_p1, tidx, warp_id, diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 9f7e5ac0997d..0b4a3e186402 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -733,7 +733,9 @@ def gvr_topk_decode( # (kC/K = 6 vs 10), the bounds prune less, and the row-split configs # win outright (cold protocol, pro 262k BS1: skip 21.3-21.6us at # cs1/cs8 vs stock cs8 19.7us) -> keep the stock path. - if block_max is not None and num_rows < 8 and top_k > 512: + if block_max is not None and num_rows < 8 and top_k > 512 and not self_scan: + # self_scan stage 2 owns its own skip economics (phase-0 block + # loop) — the stock-path small-batch gate does not apply block_max = None if self_scan: # fused self-contained mode: kernel scans/buckets the row itself. From 579602117f8262d4cfff0cc1b758ca21dfb5a8ca Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:46:43 -0700 Subject: [PATCH 024/117] [None][perf] GVR self_scan skip: two-pass compact-then-gather block scan The skip scan is restructured into two passes that both run at the tuned dense-loop shape: (1) DENSE-vector-scan the block-max array itself (1/32 of the row, 128-bit vectors - the bmax row base is only 16B aligned) and compact the PASSING BLOCK IDS into the idle C segment (single-band mode never fills C; ids store exactly as floats); (2) walk the compact list, eight listed blocks per warp round issued back-to-back - every element read is useful and the loads pipeline. A list overflow (pass rate too high for skipping to ever pay) falls back to a dense full scan of the row inside the same phase. This removes both latency walls the one-pass shapes hit (8-scalar bmax rounds; serial per-block walks): flash 512k drops 25 -> 20us and BEATS the PR16457 tip (1.06x) - first cell where the fused self-contained kernel wins outright; flash 1024k 47 -> 26us (0.72x of tip), pro 1M 53 -> 36us, v32 128k+ 28us. Dense/skip best-of geomean 0.69 -> 0.73-0.75x across the 25-cell REPORT-S4 dataset, all 100 cells bit-exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 157 +++++++++++++----- 1 file changed, 120 insertions(+), 37 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 9973af5387e0..84616b1e573c 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1298,44 +1298,127 @@ def phase0_scan_bucket( # just the one 32B bmax vector). bm_addr = block_max_row.iterator.toint() nb0 = (N + cutlass.Int32(31)) >> cutlass.Int32(5) - nwp = cutlass.const_expr(self.num_warps) - frag_m = cute.make_fragment((8,), cutlass.Float32) - bg0 = warp_id * cutlass.Int32(8) - while bg0 < nb0: - for _jm in cutlass.range_constexpr(8): - frag_m[_jm] = cutlass.Float32(self.NEG_FLT_MAX) - if bg0 + cutlass.Int32(_jm) < nb0: - bps = cute.make_ptr( - cutlass.Float32, - bm_addr + cutlass.Int64(bg0 + cutlass.Int32(_jm)) * cutlass.Int64(4), - cute.AddressSpace.gmem, - assumed_align=4, - ) - frag_m[_jm] = cute.make_tensor(bps, cute.make_layout((1,)))[0] - # SINGLE-BAND collection against the TIGHTEST line: the - # loose band's pass fraction (~80% of blocks) can never - # pay for skipping, the accepted band's (~12-22%) can. - # Only segment A is filled; overflow just drops (the - # cursor keeps counting, and the A prefix remains a - # value-blind SAMPLE - the sample-cut path handles the - # over-B* case exactly as with the external list). - for _jb in cutlass.range_constexpr(8): - if cutlass.Float32(frag_m[_jb]) >= t2_s: - pos0 = ((bg0 + cutlass.Int32(_jb)) << cutlass.Int32(5)) + lane - if pos0 < N: - vp0 = cute.make_ptr( - cutlass.Float32, - row_addr + cutlass.Int64(pos0) * cutlass.Int64(4), - cute.AddressSpace.gmem, - assumed_align=4, + # v9 two-pass skip: (1) DENSE-scan the bmax array itself (it + # is 1/32 of the row) with the tuned vector loop, compacting + # PASSING BLOCK IDS into the idle C segment (single-band mode + # never fills C; ids < 2^23 store exactly as floats); + # (2) walk the compact list, 8 blocks per warp round issued + # unguarded back-to-back - every element read is useful and + # the loads pipeline. If the list overflows capC the row + # falls back to the dense full scan (routing should have + # sent it there anyway). + # pass-1 vectors: 128-bit (the bmax row base is only 16B + # aligned: nb_pad %% 4) + pass1_atom = cute.make_copy_atom( + cute.nvgpu.CopyUniversalOp(), + cutlass.Float32, + num_bits_per_copy=128, + ) + p1w = cutlass.const_expr(4) + frag_p = cute.make_fragment((p1w,), cutlass.Float32) + nfb0 = nb0 >> cutlass.const_expr((num_threads * 4).bit_length() - 1) + itp0 = cutlass.Int32(0) + while itp0 < nfb0: + ip0 = (itp0 * cutlass.Int32(num_threads) + tidx) * cutlass.Int32(p1w) + pp0 = cute.make_ptr( + cutlass.Float32, + bm_addr + cutlass.Int64(ip0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=16, + ) + cute.copy( + pass1_atom, + cute.make_tensor(pp0, cute.make_layout((p1w,))), + frag_p, + ) + for _jp in cutlass.range_constexpr(p1w): + if cutlass.Float32(frag_p[_jp]) >= t2_s: + slp0 = atomicAdd(s_seg.iterator + cutlass.Int32(1), cutlass.Int32(1)) + if slp0 < cutlass.Int32(capC): + smem_keys[cutlass.Int32(2 * segA) + slp0] = cutlass.Float32( + ip0 + cutlass.Int32(_jp) ) - v0 = cute.make_tensor(vp0, cute.make_layout((1,)))[0] - if v0 >= t2_s: - sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) - if sl0 < cutlass.Int32(segA): - smem_keys[sl0] = v0 - cand_idx_row[sl0] = pos0 - bg0 = bg0 + cutlass.Int32(nwp * 8) + itp0 = itp0 + cutlass.Int32(1) + ptb0 = nfb0 * cutlass.Int32(num_threads * 4) + tidx + while ptb0 < nb0: + bpt0 = cute.make_ptr( + cutlass.Float32, + bm_addr + cutlass.Int64(ptb0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + if cute.make_tensor(bpt0, cute.make_layout((1,)))[0] >= t2_s: + slp0 = atomicAdd(s_seg.iterator + cutlass.Int32(1), cutlass.Int32(1)) + if slp0 < cutlass.Int32(capC): + smem_keys[cutlass.Int32(2 * segA) + slp0] = cutlass.Float32(ptb0) + ptb0 = ptb0 + cutlass.Int32(num_threads) + cute.arch.barrier() + nlist0 = s_seg[1] + if nlist0 <= cutlass.Int32(capC): + # pass 2: 8 listed blocks per warp round + nwp = cutlass.const_expr(self.num_warps) + lb0 = warp_id * cutlass.Int32(8) + while lb0 < nlist0: + for _jb in cutlass.range_constexpr(8): + li0 = lb0 + cutlass.Int32(_jb) + if li0 < nlist0: + bid0 = cutlass.Int32(smem_keys[cutlass.Int32(2 * segA) + li0]) + pos0 = (bid0 << cutlass.Int32(5)) + lane + if pos0 < N: + vp0 = cute.make_ptr( + cutlass.Float32, + row_addr + cutlass.Int64(pos0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + v0 = cute.make_tensor(vp0, cute.make_layout((1,)))[0] + if v0 >= t2_s: + sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) + if sl0 < cutlass.Int32(segA): + smem_keys[sl0] = v0 + cand_idx_row[sl0] = pos0 + lb0 = lb0 + cutlass.Int32(nwp * 8) + if nlist0 > cutlass.Int32(capC): + # list overflow (pass rate too high for skip): dense full + # scan backup - nothing was read yet, plain re-run + itd0 = cutlass.Int32(0) + nfd0 = N >> cutlass.const_expr((num_threads * 4).bit_length() - 1) + while itd0 < nfd0: + idd0 = (itd0 * cutlass.Int32(num_threads) + tidx) * cutlass.Int32(p1w) + pd0 = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(idd0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + pass1_atom, + cute.make_tensor(pd0, cute.make_layout((p1w,))), + frag_p, + ) + for _jd in cutlass.range_constexpr(p1w): + vd0 = cutlass.Float32(frag_p[_jd]) + if vd0 >= t2_s: + sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) + if sl0 < cutlass.Int32(segA): + smem_keys[sl0] = vd0 + cand_idx_row[sl0] = idd0 + cutlass.Int32(_jd) + itd0 = itd0 + cutlass.Int32(1) + ptd0 = nfd0 * cutlass.Int32(num_threads * 4) + tidx + while ptd0 < N: + pe0 = cute.make_ptr( + cutlass.Float32, + row_addr + cutlass.Int64(ptd0) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + ve0 = cute.make_tensor(pe0, cute.make_layout((1,)))[0] + if ve0 >= t2_s: + sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) + if sl0 < cutlass.Int32(segA): + smem_keys[sl0] = ve0 + cand_idx_row[sl0] = ptd0 + ptd0 = ptd0 + cutlass.Int32(num_threads) copy_atom = self._make_load_copy_atom() frag_a = cute.make_fragment((vec_w,), self.dtype) frag_b = cute.make_fragment((vec_w,), self.dtype) From 6b117ca6fbab2a091d6a5f8efbd8ccc31c7073fa Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:02:24 -0700 Subject: [PATCH 025/117] [None][perf] GVR self_scan skip: split pass-2 into load-then-claim phases The pass-2 gather interleaved each block load with its smem atomic claims; atomics are memory-ordered, so the compiler could not overlap the next block load and the eight-block round degenerated into a serial latency chain (phase-0 stamp: 17.7us at flash-1024k against a ~5us budget). Loading all eight listed blocks into registers first and claiming afterwards restores the in-flight parallelism: phase 0 drops to 10.4us and the 25-cell table moves decisively - flash 512k 1.39x over the PR16457 tip, 1024k parity (19us), 256k 0.93x; v32 64k parity, 128k+ 0.90-0.92x; pro 1M 0.85x. Dense/skip best-of geomean 0.73 -> 0.84-0.86x, still 100/100 bit-exact. Remaining gap concentrates in the mid-row dense regime (64-128k, 0.63-0.72x), where the dense scan's single-CTA latency wall stands (cp.async staging is the known next lever). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 84616b1e573c..0153e0fb337a 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1272,6 +1272,9 @@ def phase0_scan_bucket( vec_w = cutlass.const_expr(self.vec_bits // self.dtype.width) elem_bytes = cutlass.const_expr(self.dtype.width // 8) vec_align = cutlass.const_expr(self.vec_align_bytes) + p0ck0 = cutlass.Int64(0) + if cutlass.const_expr(_P4_SUB_DBG): + p0ck0 = cute.arch.clock64() if tidx == cutlass.Int32(0): s_seg[0] = cutlass.Int32(0) s_seg[1] = cutlass.Int32(0) @@ -1358,9 +1361,23 @@ def phase0_scan_bucket( # pass 2: 8 listed blocks per warp round nwp = cutlass.const_expr(self.num_warps) lb0 = warp_id * cutlass.Int32(8) + frag_v = cute.make_fragment((8,), cutlass.Float32) while lb0 < nlist0: + # LOAD phase first: eight independent block loads in + # flight before any atomic (claims are memory-ordered + # and would serialize the blocks otherwise) + p2b0 = cutlass.Int32(0) + p2b1 = cutlass.Int32(0) + p2b2 = cutlass.Int32(0) + p2b3 = cutlass.Int32(0) + p2b4 = cutlass.Int32(0) + p2b5 = cutlass.Int32(0) + p2b6 = cutlass.Int32(0) + p2b7 = cutlass.Int32(0) for _jb in cutlass.range_constexpr(8): li0 = lb0 + cutlass.Int32(_jb) + bid0 = cutlass.Int32(-1) + vv0 = cutlass.Float32(self.NEG_FLT_MAX) if li0 < nlist0: bid0 = cutlass.Int32(smem_keys[cutlass.Int32(2 * segA) + li0]) pos0 = (bid0 << cutlass.Int32(5)) + lane @@ -1371,12 +1388,51 @@ def phase0_scan_bucket( cute.AddressSpace.gmem, assumed_align=4, ) - v0 = cute.make_tensor(vp0, cute.make_layout((1,)))[0] - if v0 >= t2_s: - sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) - if sl0 < cutlass.Int32(segA): - smem_keys[sl0] = v0 - cand_idx_row[sl0] = pos0 + vv0 = cute.make_tensor(vp0, cute.make_layout((1,)))[0] + frag_v[_jb] = vv0 + if cutlass.const_expr(_jb == 0): + p2b0 = bid0 + elif cutlass.const_expr(_jb == 1): + p2b1 = bid0 + elif cutlass.const_expr(_jb == 2): + p2b2 = bid0 + elif cutlass.const_expr(_jb == 3): + p2b3 = bid0 + elif cutlass.const_expr(_jb == 4): + p2b4 = bid0 + elif cutlass.const_expr(_jb == 5): + p2b5 = bid0 + elif cutlass.const_expr(_jb == 6): + p2b6 = bid0 + else: + p2b7 = bid0 + # CLAIM phase + for _jb in cutlass.range_constexpr(8): + bidc = ( + p2b0 + if _jb == 0 + else p2b1 + if _jb == 1 + else p2b2 + if _jb == 2 + else p2b3 + if _jb == 3 + else p2b4 + if _jb == 4 + else p2b5 + if _jb == 5 + else p2b6 + if _jb == 6 + else p2b7 + ) + vvc = cutlass.Float32(frag_v[_jb]) + if bidc >= cutlass.Int32(0) and vvc >= t2_s: + posc = (bidc << cutlass.Int32(5)) + lane + if posc < N: + sl0 = atomicAdd(s_seg.iterator, cutlass.Int32(1)) + if sl0 < cutlass.Int32(segA): + smem_keys[sl0] = vvc + cand_idx_row[sl0] = posc lb0 = lb0 + cutlass.Int32(nwp * 8) if nlist0 > cutlass.Int32(capC): # list overflow (pass rate too high for skip): dense full @@ -1526,6 +1582,8 @@ def phase0_scan_bucket( s_seg[4] = curT0 s_seg[5] = curT0 s_seg[6] = curT0 + if cutlass.const_expr(_P4_SUB_DBG): + s_seg[7] = cutlass.Int32(cute.arch.clock64() - p0ck0) if cutlass.const_expr(not self.enable_block_skip): curA0 = s_seg[0] curB0 = s_seg[1] @@ -6352,6 +6410,8 @@ def _run_phases( xstate_row[5] = cutlass.Float32(cutlass.Int32(ck2 - ck1)) # P2/P3 gap xstate_row[6] = cutlass.Float32(cutlass.Int32(ck3 - ck2)) # Phase 4 xstate_row[7] = s_thr[1] # cnt_strad + if cutlass.const_expr(_P4_SUB_DBG): + xstate_row[2] = cutlass.Float32(smem_wcnt_p1[7]) if cutlass.const_expr(_P4_SUB_DBG): # P4 sub-phase cycles staged by rank_scatter. # Chain-safe layout: [2] (closed-loop anchor) From aa6c27585ffdb41c7858e48a870e12d595a22874 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 11:10:59 -0700 Subject: [PATCH 026/117] [None][perf] GVR self_scan: software-pipeline the dense scan rounds Preload the next round's two vectors into shadow fragments before the current round's atomic claims (the pass-2 lesson applied to the dense loop). Measured neutral-to-slightly-positive (phase-0 38.7 -> 37.3us at flash-1024k): unlike pass 2 the dense loop's wall is not the cross-round atomic ordering - documented for the record; the next dense-lane lever is cp.async/smem staging. Final 25-cell state (dense/skip best-of vs PR16457 tip, B=1..8 geomean 0.84-0.86x, 100/100 bit-exact): flash 512k 1.39x / 1024k 1.00x / 256k 0.93x; v32 64k 1.00x / 128k+ 0.90x; pro 1M 0.85x; remaining gap concentrated at the 64-128k dense regime. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 0153e0fb337a..8cb3183c8bd5 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1489,11 +1489,14 @@ def phase0_scan_bucket( nfull = N >> st2log if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): nfull = cutlass.Int32(0) - it0 = cutlass.Int32(0) - while it0 < nfull: - ia0 = (it0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( - vec_w - ) + # software pipeline: the claims are memory-ordered atomics, so + # without a preload the next round's vector loads serialize + # behind them (the same wall pass 2 hit). Preload round i+1 into + # the shadow fragments BEFORE claiming round i, then ping-pong. + frag_c = cute.make_fragment((vec_w,), self.dtype) + frag_d = cute.make_fragment((vec_w,), self.dtype) + if nfull > cutlass.Int32(0): + ia0 = tidx * cutlass.Int32(vec_w) ib0 = ia0 + cutlass.Int32(step1) for _fq in cutlass.range_constexpr(2): fq0 = ia0 if _fq == 0 else ib0 @@ -1508,13 +1511,34 @@ def phase0_scan_bucket( cute.make_tensor(pq0, cute.make_layout((vec_w,))), frag_a if _fq == 0 else frag_b, ) + it0 = cutlass.Int32(0) + while it0 < nfull: + ia0 = (it0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( + vec_w + ) + ib0 = ia0 + cutlass.Int32(step1) + nxt0 = it0 + cutlass.Int32(1) + if nxt0 < nfull: + ja0 = (nxt0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( + vec_w + ) + jb0 = ja0 + cutlass.Int32(step1) + for _fq in cutlass.range_constexpr(2): + fq0 = ja0 if _fq == 0 else jb0 + pq0 = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(fq0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=vec_align, + ) + cute.copy( + copy_atom, + cute.make_tensor(pq0, cute.make_layout((vec_w,))), + frag_c if _fq == 0 else frag_d, + ) # v6: per-element DIRECT atomic claims for passers — smem - # atomics do NOT synchronize the warp, so the vector loads of - # later rounds keep flowing (the packed shfl-prefix design - # capped in-flight loads at 2/warp: ncu showed 0.19% memory - # throughput, pure latency bound). Same-address service is - # ~0.5ns effective and n0 is line-bounded, so the claim queue - # hides completely under the read stream. + # atomics do NOT synchronize the warp; with the preload above + # they no longer stall the next round's loads either. for _jh in cutlass.range_constexpr(2): for _jv in cutlass.range_constexpr(vec_w): v0 = cutlass.Float32(frag_a[_jv]) if _jh == 0 else cutlass.Float32(frag_b[_jv]) @@ -1537,6 +1561,10 @@ def phase0_scan_bucket( c0 = cutlass.Int32(-1) else: c0 = c0 + cutlass.Int32(1) + if nxt0 < nfull: + for _jv in cutlass.range_constexpr(vec_w): + frag_a[_jv] = frag_c[_jv] + frag_b[_jv] = frag_d[_jv] it0 = it0 + cutlass.Int32(1) # scalar tail (< 2*step1 elements): per-element DIRECT atomic # claims — divergent-safe, no warp collectives From 26df9e6e230b8408a94936428cfc667097e1a5cc Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:14:19 -0700 Subject: [PATCH 027/117] [None][perf] GVR self_scan: cp.async deep-pipeline the dense scan Replace the register-preload dense scan with an LDGSTS staging pipeline: each thread streams one 16B vector per step into a private slot-major smem slot (no data registers, no scoreboard stall until the wait), keeping stage_slots rounds in flight. The staging buffer aliases smem_vals - written only after phase 0, with every non-empty cp.async group drained inside the loop - so depth 2 costs zero smem; trimming cap_c to <= 16384 frees 32KB of keys for depth 4. flash-1024k phase0 37.3 -> 34.8us; exactness unchanged (fused and skip smoke 6/6, 25-cell real-data sweep 50/50 bit-exact). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 233 +++++++++++------- 1 file changed, 139 insertions(+), 94 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 8cb3183c8bd5..1310f28c7c39 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -621,6 +621,14 @@ def __init__( # 227KB CTA limit with room for the stage-2 skip list. self.seg_total = 2 * self.accept_cap + int(cap_c if cap_c is not None else 24576) self.cap_c = self.seg_total - 2 * self.accept_cap + # cp.async staging depth for the phase-0 dense scan: 4 rounds + # in flight when the C segment is trimmed enough to pay for + # the extra 32KB, else the zero-cost 2 (staging then aliases + # smem_vals exactly). Rows are 16B slots, one per thread per + # in-flight round; never smaller than vals so the alias always + # covers it. + self.stage_slots = 4 if self.cap_c <= 16384 else 2 + self.stage_rows = max(self.stage_slots * self.num_threads, self.kC // 4) else: self.seg_total = self.kC self.cap_c = 0 @@ -1243,6 +1251,7 @@ def phase0_scan_bucket( smem_keys, # [seg_total] fp32 value segments (A @0 / B @segA / C @2segA) cand_idx_row, # [seg_total] int32 gmem POSITION column (write-only here) block_max_row, # [nb_pad] fp32 per-32-position maxima, or None + smem_stage, # [stage_rows, 4] fp32 cp.async staging (aliases smem_vals) s_seg, # [>=7] int32 scratch (reuses smem_wcnt_p1: P1 never runs # on a row this phase succeeded on): [0..2] A/B/C claim # cursors, [3] void, [4] n0, [5] n1, [6] n2 @@ -1258,18 +1267,19 @@ def phase0_scan_bucket( uncapped), so {n0, void, n1, n2} fall out for free — the same contract the v5 emitter produced externally. - Perf shape (v3): per round each thread front-loads TWO vec_w - vectors (independent LDGs, latency overlapped), classifies - branchlessly in registers, and the whole round pays ONE packed - warp shfl-prefix (all three segment counts in 10-bit fields) + - at most three warp atomics. Segment overflow is marked and - resolved by a RARE per-element direct-atomic pass (a segment - overflows at most once per row, and divergent scalar atomics - need no warp coordination).""" + Perf shape (v13): the dense scan is a cp.async pipeline — each + thread streams one 16B vector per step into its private + slot-major smem staging slot (LDGSTS: no data registers, no + scoreboard stall until the wait), keeping ``stage_slots`` steps + in flight; classification reads the staged values and claims + passers with per-element direct smem atomics (they don't + synchronize the warp and hide under the async copy stream). + Segment overflow is resolved in-claim by spilling to the next + looser segment (a segment overflows at most once per row, and + divergent scalar atomics need no warp coordination).""" num_threads = cutlass.const_expr(self.num_threads) segA = cutlass.const_expr(self.accept_cap) capC = cutlass.const_expr(self.cap_c) - vec_w = cutlass.const_expr(self.vec_bits // self.dtype.width) elem_bytes = cutlass.const_expr(self.dtype.width // 8) vec_align = cutlass.const_expr(self.vec_align_bytes) p0ck0 = cutlass.Int64(0) @@ -1475,100 +1485,110 @@ def phase0_scan_bucket( smem_keys[sl0] = ve0 cand_idx_row[sl0] = ptd0 ptd0 = ptd0 + cutlass.Int32(num_threads) - copy_atom = self._make_load_copy_atom() - frag_a = cute.make_fragment((vec_w,), self.dtype) - frag_b = cute.make_fragment((vec_w,), self.dtype) - step1 = cutlass.const_expr(num_threads * vec_w) - step2 = cutlass.const_expr(2 * step1) - st2log = cutlass.const_expr((2 * step1).bit_length() - 1) - # FULL-vector rounds only in the hot loop: nfull is warp-uniform - # (every lane's two vectors are in bounds by construction), so the - # warp collectives inside are legal and the hot path carries no - # bounds checks at all. The remainder (< 2*step1 elements) takes - # the scalar tail below with per-element direct atomics. - nfull = N >> st2log + cpw = cutlass.const_expr(4) # cp.async caps at 16B per copy + step1 = cutlass.const_expr(num_threads * cpw) + st1log = cutlass.const_expr(step1.bit_length() - 1) + n_stage = cutlass.const_expr(self.stage_slots) + n_smask = cutlass.const_expr(self.stage_slots - 1) + # v13 cp.async deep pipeline: LDGSTS stages each round's vector + # straight into the slot-major smem staging buffer — no data + # registers consumed, no scoreboard stall until the WAIT — so + # n_stage rounds stay in flight per thread (the register pipeline + # topped out at two). The staging buffer aliases smem_vals (only + # written after phase 0); every non-empty group is drained inside + # the loop (one commit per step, wait_group(n_stage-1) pops the + # oldest), so nothing is ever in flight once the alias is read. + # FULL-vector steps only in the hot loop; the remainder takes the + # scalar tail below. + nfull = N >> st1log if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): nfull = cutlass.Int32(0) - # software pipeline: the claims are memory-ordered atomics, so - # without a preload the next round's vector loads serialize - # behind them (the same wall pass 2 hit). Preload round i+1 into - # the shadow fragments BEFORE claiming round i, then ping-pong. - frag_c = cute.make_fragment((vec_w,), self.dtype) - frag_d = cute.make_fragment((vec_w,), self.dtype) - if nfull > cutlass.Int32(0): - ia0 = tidx * cutlass.Int32(vec_w) - ib0 = ia0 + cutlass.Int32(step1) - for _fq in cutlass.range_constexpr(2): - fq0 = ia0 if _fq == 0 else ib0 + g2s_atom = cute.make_copy_atom( + cute.nvgpu.cpasync.CopyG2SOp(cute.nvgpu.cpasync.LoadCacheMode.GLOBAL), + cutlass.Float32, + num_bits_per_copy=128, + ) + stage_addr = smem_stage.iterator.toint() + for _p in cutlass.range_constexpr(n_stage): + if cutlass.Int32(_p) < nfull: + pr0 = cutlass.Int32(_p) * cutlass.Int32(num_threads) + tidx + pp0 = pr0 * cutlass.Int32(cpw) pq0 = cute.make_ptr( self.dtype, - row_addr + cutlass.Int64(fq0) * cutlass.Int64(elem_bytes), + row_addr + cutlass.Int64(pp0) * cutlass.Int64(elem_bytes), cute.AddressSpace.gmem, - assumed_align=vec_align, + assumed_align=16, + ) + dq0 = cute.make_ptr( + cutlass.Float32, + stage_addr + cutlass.Int64(pr0 * cutlass.Int32(cpw)) * cutlass.Int64(4), + cute.AddressSpace.smem, + assumed_align=16, ) cute.copy( - copy_atom, - cute.make_tensor(pq0, cute.make_layout((vec_w,))), - frag_a if _fq == 0 else frag_b, + g2s_atom, + cute.make_tensor(pq0, cute.make_layout((cpw,))), + cute.make_tensor(dq0, cute.make_layout((cpw,))), ) + cute.arch.cp_async_commit_group() it0 = cutlass.Int32(0) while it0 < nfull: - ia0 = (it0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( - vec_w + cute.arch.cp_async_wait_group(n_smask) + sb0 = (it0 & cutlass.Int32(n_smask)) * cutlass.Int32(num_threads) + tidx + sq0 = cute.make_ptr( + cutlass.Float32, + stage_addr + cutlass.Int64(sb0 * cutlass.Int32(cpw)) * cutlass.Int64(4), + cute.AddressSpace.smem, + assumed_align=16, ) - ib0 = ia0 + cutlass.Int32(step1) - nxt0 = it0 + cutlass.Int32(1) - if nxt0 < nfull: - ja0 = (nxt0 * cutlass.Int32(2) * cutlass.Int32(num_threads) + tidx) * cutlass.Int32( - vec_w - ) - jb0 = ja0 + cutlass.Int32(step1) - for _fq in cutlass.range_constexpr(2): - fq0 = ja0 if _fq == 0 else jb0 - pq0 = cute.make_ptr( - self.dtype, - row_addr + cutlass.Int64(fq0) * cutlass.Int64(elem_bytes), - cute.AddressSpace.gmem, - assumed_align=vec_align, - ) - cute.copy( - copy_atom, - cute.make_tensor(pq0, cute.make_layout((vec_w,))), - frag_c if _fq == 0 else frag_d, - ) + srow = cute.make_tensor(sq0, cute.make_layout((cpw,))) + ia0 = (it0 * cutlass.Int32(num_threads) + tidx) * cutlass.Int32(cpw) # v6: per-element DIRECT atomic claims for passers — smem - # atomics do NOT synchronize the warp; with the preload above - # they no longer stall the next round's loads either. - for _jh in cutlass.range_constexpr(2): - for _jv in cutlass.range_constexpr(vec_w): - v0 = cutlass.Float32(frag_a[_jv]) if _jh == 0 else cutlass.Float32(frag_b[_jv]) - if v0 >= t0_s: - pos0 = (ia0 if _jh == 0 else ib0) + cutlass.Int32(_jv) - c0 = cutlass.Int32(2) - if v0 >= t1_s: - c0 = cutlass.Int32(1) - if v0 >= t2_s: - c0 = cutlass.Int32(0) - while c0 >= cutlass.Int32(0) and c0 <= cutlass.Int32(2): - cap0 = cutlass.Int32(segA) - if c0 == cutlass.Int32(2): - cap0 = cutlass.Int32(capC) - sl0 = atomicAdd(s_seg.iterator + c0, cutlass.Int32(1)) - if sl0 < cap0: - cd0 = c0 * cutlass.Int32(segA) + sl0 - smem_keys[cd0] = v0 - cand_idx_row[cd0] = pos0 - c0 = cutlass.Int32(-1) - else: - c0 = c0 + cutlass.Int32(1) - if nxt0 < nfull: - for _jv in cutlass.range_constexpr(vec_w): - frag_a[_jv] = frag_c[_jv] - frag_b[_jv] = frag_d[_jv] + # atomics do NOT synchronize the warp and hide under the + # async copy stream. + for _jv in cutlass.range_constexpr(cpw): + v0 = cutlass.Float32(srow[_jv]) + if v0 >= t0_s: + pos0 = ia0 + cutlass.Int32(_jv) + c0 = cutlass.Int32(2) + if v0 >= t1_s: + c0 = cutlass.Int32(1) + if v0 >= t2_s: + c0 = cutlass.Int32(0) + while c0 >= cutlass.Int32(0) and c0 <= cutlass.Int32(2): + cap0 = cutlass.Int32(segA) + if c0 == cutlass.Int32(2): + cap0 = cutlass.Int32(capC) + sl0 = atomicAdd(s_seg.iterator + c0, cutlass.Int32(1)) + if sl0 < cap0: + cd0 = c0 * cutlass.Int32(segA) + sl0 + smem_keys[cd0] = v0 + cand_idx_row[cd0] = pos0 + c0 = cutlass.Int32(-1) + else: + c0 = c0 + cutlass.Int32(1) + # reissue the just-consumed slot for step it0 + n_stage (the + # thread's own prior reads are ordered before the async write + # begins, so no fence is needed) + kn0 = it0 + cutlass.Int32(n_stage) + if kn0 < nfull: + jp0 = (kn0 * cutlass.Int32(num_threads) + tidx) * cutlass.Int32(cpw) + pq0 = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(jp0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=16, + ) + cute.copy( + g2s_atom, + cute.make_tensor(pq0, cute.make_layout((cpw,))), + srow, + ) + cute.arch.cp_async_commit_group() it0 = it0 + cutlass.Int32(1) - # scalar tail (< 2*step1 elements): per-element DIRECT atomic + # scalar tail (< step1 elements): per-element DIRECT atomic # claims — divergent-safe, no warp collectives - pt0 = (N >> st2log) * cutlass.Int32(step2) + tidx + pt0 = (N >> st1log) * cutlass.Int32(step1) + tidx if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): pt0 = N while pt0 < N: @@ -4857,11 +4877,31 @@ def run_one_row( # SEGMENT COORDINATE of each compacted candidate (identity for the # deferred position gather via cand_idx[coord]) — every consumer # (P4, tail repair, gather) works unchanged. - smem_vals = smem.allocate_tensor( - element_type=cutlass.Int32, - layout=cute.make_ordered_layout((kC,), order=(0,)), - byte_alignment=128, - ) + if cutlass.const_expr(self.self_scan): + # phase-0 cp.async staging, ALIASED over vals: vals is only + # written after phase 0 completes and every in-flight group + # is drained inside the dense loop, so the lifetimes never + # overlap. Slot-major (slot s of thread t at row + # s*num_threads + t): a warp's 16B reads/writes land on + # consecutive banks, conflict-free. + smem_stage = smem.allocate_tensor( + element_type=cutlass.Float32, + layout=cute.make_ordered_layout( + (cutlass.const_expr(self.stage_rows), 4), order=(1, 0) + ), + byte_alignment=128, + ) + smem_vals = cute.make_tensor( + cute.recast_ptr(smem_stage.iterator, dtype=cutlass.Int32), + cute.make_ordered_layout((kC,), order=(0,)), + ) + else: + smem_stage = None + smem_vals = smem.allocate_tensor( + element_type=cutlass.Int32, + layout=cute.make_ordered_layout((kC,), order=(0,)), + byte_alignment=128, + ) # histogram[kNumBins] int32 (P4 only) smem_hist = smem.allocate_tensor( element_type=cutlass.Int32, @@ -5139,6 +5179,7 @@ def run_one_row( cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, + smem_stage=smem_stage, ) else: # Short row: only CTA 0 scans the full row; the other @@ -5189,6 +5230,7 @@ def run_one_row( cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, + smem_stage=smem_stage, ) else: # cs=1: one CTA per row, no cluster sync. @@ -5236,6 +5278,7 @@ def run_one_row( cand_ctl_row=cand_ctl_row, smem_active=smem_active, s_active_cnt=s_active_cnt, + smem_stage=smem_stage, ) griddepcontrol_launch_dependents() @@ -5286,6 +5329,7 @@ def _run_phases( cand_ctl_row=None, # ext cand: this row's [2] int32 {claimed, void} smem_active=None, s_active_cnt=None, + smem_stage=None, # self_scan: [stage_rows, 4] fp32 cp.async staging ): """Run Phase 1-4 + final cluster barrier on a given row slice. @@ -5364,6 +5408,7 @@ def _run_phases( smem_keys, cand_idx_row, block_max_row, + smem_stage, smem_wcnt_p1, tidx, warp_id, From 2253600b6d8c50a220a5406b75d73ed499d64377 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:54:48 -0700 Subject: [PATCH 028/117] [None][perf] GVR self_scan: pin the CTA to 1024 threads in the wrapper The short-row 512-thread heuristic is tuned for the stock multi-pass kernel; under self_scan it silently halved the warp count of every N_dec < 65536 cell and cost ~5us/cell in the phase-0 scan (flash-128k p0 14.4 -> 9.4us at 1024 threads). Route self_scan to 1024 threads unconditionally. 25-cell x B{1,2,4,8} same-node sweep vs PR16457 tip: best-of geomean 0.838 -> 0.864 (B8 0.881), 100/100 bit-exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 0b4a3e186402..cdcc577cb012 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -837,11 +837,18 @@ def gvr_topk_decode( N_cols = logits.shape[1] N_dec = max_seq_len if max_seq_len is not None else N_cols if num_threads_per_block is None: - if max_seq_len is not None and logits.dtype != torch.float32: - n_thresh_t = 131072 + if self_scan: + # self_scan owns the whole row scan in one CTA: the phase-0 + # cp.async pipeline scales with warp count at every N (the + # 512-thread short-row heuristic below is tuned for the + # stock multi-pass kernel and costs ~5us/cell here). + num_threads_per_block = 1024 else: - n_thresh_t = 65536 - num_threads_per_block = 1024 if (num_rows <= num_sms and N_dec >= n_thresh_t) else 512 + if max_seq_len is not None and logits.dtype != torch.float32: + n_thresh_t = 131072 + else: + n_thresh_t = 65536 + num_threads_per_block = 1024 if (num_rows <= num_sms and N_dec >= n_thresh_t) else 512 if use_256bit_load is None: use_256bit_load = logits.dtype == torch.float32 and N_dec >= 16384 if enable_warp_parallel_reduce is None: From 8ab4f74c87e9769a9b13bae25c1f930ad776699e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:07:15 -0700 Subject: [PATCH 029/117] [None][perf] GVR self_scan: pair-step the cp.async scan pipeline The dense scan is instruction-issue bound (pcsamp: no_instructions + fixed-latency wait dominate; long_scoreboard is 6%), so each pipeline step now processes two 16B vectors per thread - loop, wait, commit and address arithmetic amortize over 8 elements while the in-flight byte count stays at 2 pairs x 32B across the 4 staging slots. The pair shape needs all 4 slot rows, and the 64KB staging fits the CTA budget only with the C segment trimmed, so self_scan now defaults cap_c to 16384 and rejects anything larger (validated bit-exact across the 25-cell x B{1,2,4,8} sweep). flash p0 (warm, 1024 threads): 512k 17.8 -> 16.8us, 1024k 33.5 -> 32.4us; smoke 6/6 exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 197 ++++++++++-------- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 +- 2 files changed, 112 insertions(+), 89 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 1310f28c7c39..42bf4507c436 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -619,15 +619,19 @@ def __init__( # on-chip segment budget: values only, 4B/entry; C sized so # keys(160KB) + vals(32KB) + hist + scratch stay under the # 227KB CTA limit with room for the stage-2 skip list. - self.seg_total = 2 * self.accept_cap + int(cap_c if cap_c is not None else 24576) + self.seg_total = 2 * self.accept_cap + int(cap_c if cap_c is not None else 16384) self.cap_c = self.seg_total - 2 * self.accept_cap - # cp.async staging depth for the phase-0 dense scan: 4 rounds - # in flight when the C segment is trimmed enough to pay for - # the extra 32KB, else the zero-cost 2 (staging then aliases - # smem_vals exactly). Rows are 16B slots, one per thread per - # in-flight round; never smaller than vals so the alias always + # cp.async staging for the phase-0 dense scan: the pair-step + # pipeline keeps 2 pairs x 2 slots in flight per thread, so + # exactly 4 slot rows are required. The 64KB staging fits the + # CTA budget only with the C segment trimmed to <= 16384 + # (keys 128KB + staging 64KB + hist/scratch); larger C would + # silently overrun the alias, so reject it outright. Rows are + # 16B slots; never fewer than vals holds so the alias always # covers it. - self.stage_slots = 4 if self.cap_c <= 16384 else 2 + if self.cap_c > 16384: + raise ValueError("self_scan requires cap_c <= 16384 (staging budget)") + self.stage_slots = 4 self.stage_rows = max(self.stage_slots * self.num_threads, self.kC // 4) else: self.seg_total = self.kC @@ -1487,20 +1491,19 @@ def phase0_scan_bucket( ptd0 = ptd0 + cutlass.Int32(num_threads) cpw = cutlass.const_expr(4) # cp.async caps at 16B per copy step1 = cutlass.const_expr(num_threads * cpw) - st1log = cutlass.const_expr(step1.bit_length() - 1) - n_stage = cutlass.const_expr(self.stage_slots) - n_smask = cutlass.const_expr(self.stage_slots - 1) - # v13 cp.async deep pipeline: LDGSTS stages each round's vector - # straight into the slot-major smem staging buffer — no data - # registers consumed, no scoreboard stall until the WAIT — so - # n_stage rounds stay in flight per thread (the register pipeline - # topped out at two). The staging buffer aliases smem_vals (only - # written after phase 0); every non-empty group is drained inside - # the loop (one commit per step, wait_group(n_stage-1) pops the - # oldest), so nothing is ever in flight once the alias is read. - # FULL-vector steps only in the hot loop; the remainder takes the - # scalar tail below. - nfull = N >> st1log + st2log = cutlass.const_expr((2 * step1).bit_length() - 1) + # v14 pair-step cp.async pipeline: the scan is instruction-issue + # bound (pcsamp: no_instructions + wait dominate; long_scoreboard + # is 6%), so each step processes TWO 16B vectors per thread — + # loop/wait/commit/address overhead amortizes over 8 elements + # instead of 4 while the in-flight byte count stays put (2 pairs + # x 32B across the 4 staging slots). One commit group per pair; + # wait_group(1) pops the oldest pair. The staging buffer aliases + # smem_vals (only written after phase 0); every non-empty group + # is drained inside the loop, so nothing is in flight once the + # alias is read. FULL-pair steps only in the hot loop; the + # remainder takes the scalar tail below. + nfull = N >> st2log if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): nfull = cutlass.Int32(0) g2s_atom = cute.make_copy_atom( @@ -1509,86 +1512,106 @@ def phase0_scan_bucket( num_bits_per_copy=128, ) stage_addr = smem_stage.iterator.toint() - for _p in cutlass.range_constexpr(n_stage): + for _p in cutlass.range_constexpr(2): if cutlass.Int32(_p) < nfull: - pr0 = cutlass.Int32(_p) * cutlass.Int32(num_threads) + tidx - pp0 = pr0 * cutlass.Int32(cpw) - pq0 = cute.make_ptr( - self.dtype, - row_addr + cutlass.Int64(pp0) * cutlass.Int64(elem_bytes), - cute.AddressSpace.gmem, - assumed_align=16, - ) - dq0 = cute.make_ptr( - cutlass.Float32, - stage_addr + cutlass.Int64(pr0 * cutlass.Int32(cpw)) * cutlass.Int64(4), - cute.AddressSpace.smem, - assumed_align=16, - ) - cute.copy( - g2s_atom, - cute.make_tensor(pq0, cute.make_layout((cpw,))), - cute.make_tensor(dq0, cute.make_layout((cpw,))), - ) + for _v in cutlass.range_constexpr(2): + pr0 = cutlass.Int32(2 * _p + _v) * cutlass.Int32(num_threads) + tidx + pp0 = cutlass.Int32(2 * _p + _v) * cutlass.Int32(step1) + tidx * cutlass.Int32( + cpw + ) + pq0 = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(pp0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=16, + ) + dq0 = cute.make_ptr( + cutlass.Float32, + stage_addr + cutlass.Int64(pr0 * cutlass.Int32(cpw)) * cutlass.Int64(4), + cute.AddressSpace.smem, + assumed_align=16, + ) + cute.copy( + g2s_atom, + cute.make_tensor(pq0, cute.make_layout((cpw,))), + cute.make_tensor(dq0, cute.make_layout((cpw,))), + ) cute.arch.cp_async_commit_group() it0 = cutlass.Int32(0) while it0 < nfull: - cute.arch.cp_async_wait_group(n_smask) - sb0 = (it0 & cutlass.Int32(n_smask)) * cutlass.Int32(num_threads) + tidx - sq0 = cute.make_ptr( - cutlass.Float32, - stage_addr + cutlass.Int64(sb0 * cutlass.Int32(cpw)) * cutlass.Int64(4), - cute.AddressSpace.smem, - assumed_align=16, - ) - srow = cute.make_tensor(sq0, cute.make_layout((cpw,))) - ia0 = (it0 * cutlass.Int32(num_threads) + tidx) * cutlass.Int32(cpw) + cute.arch.cp_async_wait_group(1) + sp0 = (it0 & cutlass.Int32(1)) * cutlass.Int32(2 * num_threads) + tidx + ia0 = it0 * cutlass.Int32(2 * step1) + tidx * cutlass.Int32(cpw) # v6: per-element DIRECT atomic claims for passers — smem # atomics do NOT synchronize the warp and hide under the # async copy stream. - for _jv in cutlass.range_constexpr(cpw): - v0 = cutlass.Float32(srow[_jv]) - if v0 >= t0_s: - pos0 = ia0 + cutlass.Int32(_jv) - c0 = cutlass.Int32(2) - if v0 >= t1_s: - c0 = cutlass.Int32(1) - if v0 >= t2_s: - c0 = cutlass.Int32(0) - while c0 >= cutlass.Int32(0) and c0 <= cutlass.Int32(2): - cap0 = cutlass.Int32(segA) - if c0 == cutlass.Int32(2): - cap0 = cutlass.Int32(capC) - sl0 = atomicAdd(s_seg.iterator + c0, cutlass.Int32(1)) - if sl0 < cap0: - cd0 = c0 * cutlass.Int32(segA) + sl0 - smem_keys[cd0] = v0 - cand_idx_row[cd0] = pos0 - c0 = cutlass.Int32(-1) - else: - c0 = c0 + cutlass.Int32(1) - # reissue the just-consumed slot for step it0 + n_stage (the + for _jh in cutlass.range_constexpr(2): + sq0 = cute.make_ptr( + cutlass.Float32, + stage_addr + + cutlass.Int64( + (sp0 + cutlass.Int32(_jh) * cutlass.Int32(num_threads)) * cutlass.Int32(cpw) + ) + * cutlass.Int64(4), + cute.AddressSpace.smem, + assumed_align=16, + ) + srow = cute.make_tensor(sq0, cute.make_layout((cpw,))) + for _jv in cutlass.range_constexpr(cpw): + v0 = cutlass.Float32(srow[_jv]) + if v0 >= t0_s: + pos0 = ia0 + cutlass.Int32(_jh) * cutlass.Int32(step1) + cutlass.Int32(_jv) + c0 = cutlass.Int32(2) + if v0 >= t1_s: + c0 = cutlass.Int32(1) + if v0 >= t2_s: + c0 = cutlass.Int32(0) + while c0 >= cutlass.Int32(0) and c0 <= cutlass.Int32(2): + cap0 = cutlass.Int32(segA) + if c0 == cutlass.Int32(2): + cap0 = cutlass.Int32(capC) + sl0 = atomicAdd(s_seg.iterator + c0, cutlass.Int32(1)) + if sl0 < cap0: + cd0 = c0 * cutlass.Int32(segA) + sl0 + smem_keys[cd0] = v0 + cand_idx_row[cd0] = pos0 + c0 = cutlass.Int32(-1) + else: + c0 = c0 + cutlass.Int32(1) + # reissue the just-consumed slot pair for step it0 + 2 (the # thread's own prior reads are ordered before the async write # begins, so no fence is needed) - kn0 = it0 + cutlass.Int32(n_stage) + kn0 = it0 + cutlass.Int32(2) if kn0 < nfull: - jp0 = (kn0 * cutlass.Int32(num_threads) + tidx) * cutlass.Int32(cpw) - pq0 = cute.make_ptr( - self.dtype, - row_addr + cutlass.Int64(jp0) * cutlass.Int64(elem_bytes), - cute.AddressSpace.gmem, - assumed_align=16, - ) - cute.copy( - g2s_atom, - cute.make_tensor(pq0, cute.make_layout((cpw,))), - srow, - ) + for _jh in cutlass.range_constexpr(2): + jr0 = sp0 + cutlass.Int32(_jh) * cutlass.Int32(num_threads) + jp0 = ( + kn0 * cutlass.Int32(2 * step1) + + cutlass.Int32(_jh) * cutlass.Int32(step1) + + tidx * cutlass.Int32(cpw) + ) + pq0 = cute.make_ptr( + self.dtype, + row_addr + cutlass.Int64(jp0) * cutlass.Int64(elem_bytes), + cute.AddressSpace.gmem, + assumed_align=16, + ) + dq0 = cute.make_ptr( + cutlass.Float32, + stage_addr + cutlass.Int64(jr0 * cutlass.Int32(cpw)) * cutlass.Int64(4), + cute.AddressSpace.smem, + assumed_align=16, + ) + cute.copy( + g2s_atom, + cute.make_tensor(pq0, cute.make_layout((cpw,))), + cute.make_tensor(dq0, cute.make_layout((cpw,))), + ) cute.arch.cp_async_commit_group() it0 = it0 + cutlass.Int32(1) # scalar tail (< step1 elements): per-element DIRECT atomic # claims — divergent-safe, no warp collectives - pt0 = (N >> st1log) * cutlass.Int32(step1) + tidx + pt0 = (N >> st2log) * cutlass.Int32(2 * step1) + tidx if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): pt0 = N while pt0 < N: diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index cdcc577cb012..fc72170a1447 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -753,7 +753,7 @@ def gvr_topk_decode( if seed_counts is None: seed_counts = torch.zeros((num_rows, 3), dtype=torch.int32, device=logits.device) _bstar = int(os.environ["GVR_BSTAR"]) - _capc = cap_c if cap_c is not None else int(os.environ.get("GVR_CAPC", "24576")) + _capc = cap_c if cap_c is not None else int(os.environ.get("GVR_CAPC", "16384")) _segtot = 2 * _bstar + _capc if cand_idx is None: cand_idx = torch.empty((num_rows, _segtot), dtype=torch.int32, device=logits.device) @@ -904,7 +904,7 @@ def gvr_topk_decode( self_scan, cap_c if cap_c is not None - else (int(os.environ.get("GVR_CAPC", "24576")) if self_scan else None), + else (int(os.environ.get("GVR_CAPC", "16384")) if self_scan else None), ) # When return_output_values=False the kernel was compiled to skip # STG.value and accepts None for the value-output slot. From 5970b3e37197d4cb8d1876f4ec58745be51c7d01 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:41:16 -0700 Subject: [PATCH 030/117] [None][feat] GVR ext_rungs: two-pass variant B (count external rungs in-kernel) New ext_rungs mode: the host supplies only the three closed-loop rung THRESHOLDS (previous-step xstep lines); the kernel counts them itself through the stock R0 multi-count pass and admits the tightest rung with count in [K, kC], then collects and refines as usual. This is the fully self-contained two-pass shape: no emission of any kind, pass 1 = one fused 3-rung count (cluster-merge and block-skip compose unchanged), pass 2 = the stock single-line collect. Versus use_ext_counts (variant A) the only delta is where the counts come from; P1's preIdx gather and the P1b quantile rung derivation are both skipped (the seed lines carry the bracket). Smoke: 15/15 bit-exact across cs=1/4, block_max skip, and the thin (all rungs below K) and fat (all counts above kC) miss paths. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 43 ++++++++++++++++++- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 12 ++++-- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 42bf4507c436..5e1cd25a61d6 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -285,6 +285,7 @@ def __init__( p2_warp_redundant: bool = True, enable_block_skip: bool = False, use_ext_counts: bool = False, + ext_rungs: bool = False, use_ext_cand: bool = False, cand_cap: int = 5120, cand_rung: int = 0, @@ -638,12 +639,25 @@ def __init__( self.cap_c = 0 if use_ext_cand and not use_ext_counts: raise ValueError("use_ext_cand requires use_ext_counts") + # ext_rungs (two-pass variant B): closed-loop rung THRESHOLDS come + # from the host (previous-step xstep lines); the kernel counts them + # itself via the stock R0 multi-count and admits the tightest rung + # in [K, kC]. Exclusive with use_ext_counts (which also imports + # the counts and skips nothing else). + self.ext_rungs = bool(ext_rungs) and bool(enable_r0) + if self.ext_rungs and bool(use_ext_counts): + raise ValueError("ext_rungs is exclusive with use_ext_counts") self.use_ext_counts = bool(use_ext_counts) and bool(enable_r0) if self.use_ext_counts: if not self.fb_fix: raise ValueError("use_ext_counts requires fb_fix") if self.M_thr != 3: raise ValueError("use_ext_counts expects exactly 3 seed rungs") + if self.ext_rungs: + if not self.fb_fix: + raise ValueError("ext_rungs requires fb_fix") + if self.M_thr != 3: + raise ValueError("ext_rungs expects exactly 3 seed rungs") # cluster_size > 1 supported: the ext rungs/counts are # per-row (identical across the cluster), the stock multi # count pass cluster-merges as usual, and the L2 direct @@ -4846,6 +4860,9 @@ def run_one_row( ): seed_thr_row = seed_thr[row_idx, None] seed_counts_row = seed_counts[row_idx, None] + elif cutlass.const_expr(self.ext_rungs and seed_thr is not None): + seed_thr_row = seed_thr[row_idx, None] + seed_counts_row = None else: seed_thr_row = None seed_counts_row = None @@ -5449,6 +5466,19 @@ def _run_phases( # valid), so the preIdx gather is skipped wholesale. A miss whose # target lies outside [t_0, t_2] recovers via the refine loop's # 8x bracket expansion (same fail-soft as the stock path). + if cutlass.const_expr(self.ext_rungs): + # variant B: the rungs carry the bracket, so P1's gather buys + # nothing - same seed-line init as the ext-counts hit path. + if tidx == cutlass.Int32(0): + s_thr[0] = seed_thr_row[1] + s_thr[1] = seed_thr_row[0] + s_thr[2] = seed_thr_row[2] + s_iscalars[0] = cutlass.Int32(0) # cand_count + s_iscalars[1] = cutlass.Int32(0) # done + s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb seeding owns) + s_iscalars[3] = cutlass.Int32(-1) # cnt_hi + s_iscalars[4] = cutlass.Int32(0) # out_count + cute.arch.barrier() if cutlass.const_expr(self.use_ext_counts): if ext_row == cutlass.Int32(1): if tidx == cutlass.Int32(0): @@ -5480,7 +5510,7 @@ def _run_phases( smem_gath=smem_gath, # p1b_cache: stash gathered values (None-op OFF) s_mt_thr=s_mt_thr, # r0_vseed: park pmean in the last rung column ) - if cutlass.const_expr(not self.use_ext_counts): + if cutlass.const_expr(not (self.use_ext_counts or self.ext_rungs)): self.phase1_preidx_stats( input_row, N, @@ -6074,7 +6104,16 @@ def _run_phases( warp_id, lane, ) - if cutlass.const_expr(not self.use_ext_counts): + if cutlass.const_expr(self.ext_rungs): + # variant B: rung thresholds = the closed-loop seed + # lines verbatim; the stock multi-count measures + # them and the argmin admission below picks the + # tightest one in [K, kC]. + if tidx == cutlass.Int32(0): + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + s_mt_thr[m] = seed_thr_row[m] + cute.arch.barrier() + if cutlass.const_expr(not (self.use_ext_counts or self.ext_rungs)): if cutlass.const_expr(self.p1b_cache): # rungs from the SMEM gather-cache P1 stashed (no 2nd # GMEM gather); 16-bit only. diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index fc72170a1447..e540fc9814c3 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -69,6 +69,7 @@ def _compile( use_ext_counts: bool = False, emit_xstate: bool = False, use_ext_cand: bool = False, + ext_rungs: bool = False, cand_cap: int = 5120, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -146,7 +147,7 @@ def _compile( cute.runtime.make_fake_compact_tensor( cutlass.Float32, (n_rows, 3), stride_order=(1, 0), assumed_align=4 ) - if use_ext_counts + if (use_ext_counts or ext_rungs) else None ) seed_counts_fake = ( @@ -206,6 +207,7 @@ def _compile( use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, use_ext_cand=use_ext_cand, + ext_rungs=ext_rungs, cand_cap=cand_cap, accept_cap=accept_cap, kc_override=kc_override, @@ -214,7 +216,7 @@ def _compile( # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The # qfrac VALUES are irrelevant on this path (P1b is skipped) — only # the slot count matters. - r0_qfracs=(0.85, 0.35) if use_ext_counts else None, + r0_qfracs=(0.85, 0.35) if (use_ext_counts or ext_rungs) else None, ) return cute.compile( kernel, @@ -764,6 +766,9 @@ def gvr_topk_decode( and cand_idx.shape == (num_rows, _segtot) ), f"self_scan position column must be int32 [num_rows, {_segtot}]" use_ext_counts = seed_thr is not None and seed_counts is not None + # variant B (two-pass): thresholds without counts -> the kernel counts + # the rungs itself (stock R0 multi-count) and admits in-kernel + ext_rungs = seed_thr is not None and seed_counts is None if use_ext_counts: assert ( seed_thr.dtype == torch.float32 @@ -898,6 +903,7 @@ def gvr_topk_decode( use_ext_counts, emit_xstate, use_ext_cand, + ext_rungs, cand_cap, int(os.environ["GVR_BSTAR"]) if "GVR_BSTAR" in os.environ else None, int(os.environ["GVR_KC"]) if "GVR_KC" in os.environ else None, @@ -918,7 +924,7 @@ def gvr_topk_decode( out_indices, order_row if seqlen_sorted else None, block_max if enable_block_skip else None, - seed_thr if use_ext_counts else None, + seed_thr if (use_ext_counts or ext_rungs) else None, seed_counts if use_ext_counts else None, xstate if emit_xstate else None, cand_vals if use_ext_cand else None, From 26608ef9cf1c874dfa847693b445c90df737dc09 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:21:21 -0700 Subject: [PATCH 031/117] [None][perf] GVR ext counts: pack lines and counts into one seed row The admission preview paid two serial cold gmem reads (12B seed_thr then 12B seed_counts) on the critical path - 1-1.6us on 8-17us kernels. Pack both into a single [rows, 8] fp32 row (lines at [0..2], counts as floats at [3..5], exact to 2^24): one 32B sector serves everything, and threshold indices stay unchanged. The wrapper accepts a pre-packed row (width >= 6) natively via pack_seed(); passing separate tensors remains as a compat path that builds the pack per call. Cold probe: flash-8k variant-A gap to variant-B shrinks 1.6 -> 0.8us; flash-64k reaches parity. Smoke: ea 4/4 + vb 15/15 bit-exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 13 ++--- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 54 +++++++++++++------ 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 5e1cd25a61d6..afa4e46d3704 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4855,11 +4855,12 @@ def run_one_row( block_max_row = block_max[row_idx, None] else: block_max_row = None - if cutlass.const_expr( - self.use_ext_counts and seed_thr is not None and seed_counts is not None - ): + if cutlass.const_expr(self.use_ext_counts and seed_thr is not None): + # packed seed row [>=6] fp32: [0..2] lines, [3..5] counts as + # floats (exact to 2^24) - ONE 32B sector serves both, halving + # the serial cold loads of the admission preview seed_thr_row = seed_thr[row_idx, None] - seed_counts_row = seed_counts[row_idx, None] + seed_counts_row = None elif cutlass.const_expr(self.ext_rungs and seed_thr is not None): seed_thr_row = seed_thr[row_idx, None] seed_counts_row = None @@ -5406,7 +5407,7 @@ def _run_phases( if cutlass.const_expr(self.use_ext_counts): if seed_thr_row[0] < cutlass.Float32(1e37): for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cm_e = cutlass.Int32(seed_counts_row[m]) + cm_e = cutlass.Int32(seed_thr_row[3 + m]) if cm_e >= cutlass.Int32(self.top_k) and cm_e <= cutlass.Int32(self.kC): ext_row = cutlass.Int32(1) # list path preview: when the SoA candidate list will be taken @@ -6062,7 +6063,7 @@ def _run_phases( bx_m = cutlass.Int32(-1) bx_c = cutlass.Int32(2147483647) for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cx = cutlass.Int32(seed_counts_row[m]) + cx = cutlass.Int32(seed_thr_row[3 + m]) if ( cx >= cutlass.Int32(self.top_k) and cx <= cutlass.Int32(self.kC) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index e540fc9814c3..d4e5e2a4b098 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -143,20 +143,19 @@ def _compile( if enable_block_skip else None ) + # ext counts ride PACKED with the lines ([rows, 8] fp32: lines at + # [0..2], counts as floats at [3..5]) - one 32B sector per row seed_thr_fake = ( cute.runtime.make_fake_compact_tensor( - cutlass.Float32, (n_rows, 3), stride_order=(1, 0), assumed_align=4 + cutlass.Float32, + (n_rows, 8 if use_ext_counts else 3), + stride_order=(1, 0), + assumed_align=4, ) if (use_ext_counts or ext_rungs) else None ) - seed_counts_fake = ( - cute.runtime.make_fake_compact_tensor( - cutlass.Int32, (n_rows, 3), stride_order=(1, 0), assumed_align=4 - ) - if use_ext_counts - else None - ) + seed_counts_fake = None cand_vals_fake = ( cute.runtime.make_fake_compact_tensor( cutlass.Float32, (n_rows, cand_cap), stride_order=(1, 0), assumed_align=4 @@ -521,6 +520,19 @@ def derive_seed_lines_v4( return out.contiguous() +def pack_seed(seed_thr: torch.Tensor, seed_counts: torch.Tensor) -> torch.Tensor: + """Pack lines + exact counts into one [rows, 8] fp32 seed row. + + Lines land at [0..2], counts as floats at [3..5] (exact to 2^24); + one 32B sector per row. Build ONCE per step, outside any timed + region. + """ + pack = torch.zeros((seed_thr.shape[0], 8), dtype=torch.float32, device=seed_thr.device) + pack[:, 0:3] = seed_thr + pack[:, 3:6] = seed_counts.float() + return pack.contiguous() + + def emu_seed_counts( logits: torch.Tensor, seq_lens: torch.Tensor, @@ -765,18 +777,23 @@ def gvr_topk_decode( and cand_idx.is_contiguous() and cand_idx.shape == (num_rows, _segtot) ), f"self_scan position column must be int32 [num_rows, {_segtot}]" - use_ext_counts = seed_thr is not None and seed_counts is not None + # packed seed row ([rows, >=6] fp32: lines + counts-as-floats) is the + # native ext-counts input; separate seed_counts is the compat path and + # pays a per-call pack build - pre-pack with pack_seed() instead. + pre_packed = seed_thr is not None and seed_thr.shape[1] >= 6 + use_ext_counts = seed_thr is not None and (seed_counts is not None or pre_packed) # variant B (two-pass): thresholds without counts -> the kernel counts # the rungs itself (stock R0 multi-count) and admits in-kernel - ext_rungs = seed_thr is not None and seed_counts is None + ext_rungs = seed_thr is not None and seed_counts is None and not pre_packed if use_ext_counts: assert ( seed_thr.dtype == torch.float32 and seed_thr.is_cuda and seed_thr.is_contiguous() - and seed_thr.shape == (num_rows, 3) - ), "seed_thr must be contiguous CUDA fp32 [num_rows, 3]" - assert ( + and seed_thr.shape[0] == num_rows + and (pre_packed or seed_thr.shape[1] == 3) + ), "seed_thr must be contiguous CUDA fp32 [num_rows, 3|8]" + assert pre_packed or ( seed_counts.dtype == torch.int32 and seed_counts.is_cuda and seed_counts.is_contiguous() @@ -882,6 +899,13 @@ def gvr_topk_decode( else: min_blocks_per_mp = 1 + seed_pack = None + if use_ext_counts: + if pre_packed: + seed_pack = seed_thr + else: + seed_pack = pack_seed(seed_thr, seed_counts) + compiled = _compile( cute_dtype, top_k, @@ -924,8 +948,8 @@ def gvr_topk_decode( out_indices, order_row if seqlen_sorted else None, block_max if enable_block_skip else None, - seed_thr if (use_ext_counts or ext_rungs) else None, - seed_counts if use_ext_counts else None, + seed_pack if use_ext_counts else (seed_thr if ext_rungs else None), + None, xstate if emit_xstate else None, cand_vals if use_ext_cand else None, cand_idx if (use_ext_cand or self_scan) else None, From 09699c9d27ca9f2a37ed7184f11eb3f916351b09 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:30:26 -0700 Subject: [PATCH 032/117] [None][perf] GVR: open the K>512 skip gate for parked ext counts; publish rung counts Two changes: 1. The stock-path small-batch gate (drop block_max at K > 512, tiny batch) was tuned in the loose-rung era, when the sample-quantile list kept 60%+ of the blocks. With ext counts the admitted line is parked in every rung slot BEFORE the count pass, so the active list forms at the accepted threshold and skips by the real band density. Bypass the gate for parked ext counts: pro-1M list goes 0 -> 1039 blocks (12.7% pass, exactly the host-side prediction) and the cell runs 36.7 -> 16.6us warm (2.1x); v32 64k-256k all flip to the skip walk (15.6-16.1 vs 17.2-18.2us row-split). All probes bit-exact. 2. ext_rungs publishes its three measured rung counts in xstate[4..6] (production builds only; debug stamp layouts win the slots otherwise) - the closed loop derives next-step lines from real device counts instead of a host-side recount emulation. Also adds GVR_SKIP_DBG forensics (list length / current flag / chosen rung / drop mask via xstate) used to find the gate. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 16 ++++++++++++++++ .../cute_dsl_kernels/top_k/run_gvr_topk.py | 18 +++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index afa4e46d3704..afff4efd3906 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -49,6 +49,7 @@ # P4 sub-phase clock64 breakdown -> xstate[1,2,4,5,6,7] (debug: clobbers # the closed-loop thr/anch publish; single-shot cells only, not chains) _P4_SUB_DBG = bool(int(os.environ.get("GVR_P4_SUB_DBG", "0"))) +_SKIP_DBG = bool(int(os.environ.get("GVR_SKIP_DBG", "0"))) # --------------------------------------------------------------------------- @@ -6564,6 +6565,21 @@ def _run_phases( # cand_count_p4 = pre-P4 snapshot (P4 repurposes # the s_iscalars slots). xstate_row[3] = cutlass.Float32(cand_count_p4) + if cutlass.const_expr( + self.ext_rungs and not _P4_SUB_DBG and not _P4_TAIL_DBG + ): + # closed-loop food: the three rung counts this + # step measured (exact, straight from the R0 + # multi-count) - the host derives next-step + # lines from these instead of re-counting. + xstate_row[4] = cutlass.Float32(s_mt_cnt[0]) + xstate_row[5] = cutlass.Float32(s_mt_cnt[1]) + xstate_row[6] = cutlass.Float32(s_mt_cnt[2]) + if cutlass.const_expr(_SKIP_DBG and self.enable_block_skip): + xstate_row[4] = cutlass.Float32(s_active_cnt[0]) + xstate_row[5] = cutlass.Float32(s_active_cnt[1]) + xstate_row[6] = cutlass.Float32(s_r0col[0]) + xstate_row[7] = cutlass.Float32(s_active_cnt[2]) else: # cs>1: only the leader (CTA 0 in cluster) runs Phase 4. if is_leader: diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index d4e5e2a4b098..eb4209a59dbc 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -747,9 +747,21 @@ def gvr_topk_decode( # (kC/K = 6 vs 10), the bounds prune less, and the row-split configs # win outright (cold protocol, pro 262k BS1: skip 21.3-21.6us at # cs1/cs8 vs stock cs8 19.7us) -> keep the stock path. - if block_max is not None and num_rows < 8 and top_k > 512 and not self_scan: - # self_scan stage 2 owns its own skip economics (phase-0 block - # loop) — the stock-path small-batch gate does not apply + if ( + block_max is not None + and num_rows < 8 + and top_k > 512 + and not self_scan + and not (seed_thr is not None and seed_counts is not None) + and not (seed_thr is not None and seed_thr.shape[1] >= 6) + ): + # Stock-path small-batch gate, tuned in the loose-rung era (the + # sample-quantile list kept 60%+ of the blocks at K>512). It does + # NOT apply when the admitted line is known up front: ext counts + # park the tight line in every rung slot, so the list forms at + # the accepted threshold and skips by the real band density + # (pro-1M: 12.7% pass -> the list is live again). self_scan owns + # its own skip economics likewise. block_max = None if self_scan: # fused self-contained mode: kernel scans/buckets the row itself. From 993082c85acd9e4b5000e3885e68aee966fa224f Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:39:33 -0700 Subject: [PATCH 033/117] [None][fix] GVR ext_rungs: cluster-side count publish + runtime rung validity Two chain-integration fixes for the two-pass variant B: 1. The rung-count xstate publish only existed on the cs=1 exit; at cs>1 the closed loop read zeros, derived garbage lines, and - worse - inverted rungs broke bit-exactness downstream. Publish the cluster-merged counts from the leader exit too. 2. Rung validity is now a RUNTIME check (finite and strictly ascending); invalid lines fall back to the stock P1 seed + P1b quantile-rung path in-kernel, so exactness never rides on the host loop's line quality (cold start, dropped rows, NaN counts). p640 capture chain: published counts match the host emulation bit-for-bit (cntdiff=0 every step), all arms exact, admission 0.75-1.0. Smokes: ea 4/4 + vb 15/15. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 94 +++++++++++++++---- 1 file changed, 77 insertions(+), 17 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index afff4efd3906..ce372a829359 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -5468,19 +5468,53 @@ def _run_phases( # valid), so the preIdx gather is skipped wholesale. A miss whose # target lies outside [t_0, t_2] recovers via the refine loop's # 8x bracket expansion (same fail-soft as the stock path). + rungs_ok = cutlass.Int32(0) if cutlass.const_expr(self.ext_rungs): - # variant B: the rungs carry the bracket, so P1's gather buys - # nothing - same seed-line init as the ext-counts hit path. - if tidx == cutlass.Int32(0): - s_thr[0] = seed_thr_row[1] - s_thr[1] = seed_thr_row[0] - s_thr[2] = seed_thr_row[2] - s_iscalars[0] = cutlass.Int32(0) # cand_count - s_iscalars[1] = cutlass.Int32(0) # done - s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb seeding owns) - s_iscalars[3] = cutlass.Int32(-1) # cnt_hi - s_iscalars[4] = cutlass.Int32(0) # out_count - cute.arch.barrier() + # runtime validity: finite AND strictly ascending; anything + # else (cold start, dropped row, NaN from the host loop) + # falls back to the stock seed path below - exactness never + # rides on the host's line quality + if ( + seed_thr_row[0] < cutlass.Float32(1e37) + and seed_thr_row[0] > cutlass.Float32(-1e37) + and seed_thr_row[1] > seed_thr_row[0] + and seed_thr_row[2] > seed_thr_row[1] + ): + rungs_ok = cutlass.Int32(1) + if cutlass.const_expr(self.ext_rungs): + if rungs_ok == cutlass.Int32(1): + # variant B: the rungs carry the bracket, so P1's gather + # buys nothing - same seed-line init as the ext-counts + # hit path. + if tidx == cutlass.Int32(0): + s_thr[0] = seed_thr_row[1] + s_thr[1] = seed_thr_row[0] + s_thr[2] = seed_thr_row[2] + s_iscalars[0] = cutlass.Int32(0) # cand_count + s_iscalars[1] = cutlass.Int32(0) # done + s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb owns) + s_iscalars[3] = cutlass.Int32(-1) # cnt_hi + s_iscalars[4] = cutlass.Int32(0) # out_count + cute.arch.barrier() + if rungs_ok == cutlass.Int32(0): + self.phase1_preidx_stats( + input_row, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_wmin, + smem_wmax, + smem_wsum, + smem_wcnt_p1, + s_thr, + s_iscalars, + tidx, + warp_id, + lane, + smem_gath=smem_gath, + s_mt_thr=s_mt_thr, + ) if cutlass.const_expr(self.use_ext_counts): if ext_row == cutlass.Int32(1): if tidx == cutlass.Int32(0): @@ -6110,11 +6144,28 @@ def _run_phases( # variant B: rung thresholds = the closed-loop seed # lines verbatim; the stock multi-count measures # them and the argmin admission below picks the - # tightest one in [K, kC]. - if tidx == cutlass.Int32(0): - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - s_mt_thr[m] = seed_thr_row[m] - cute.arch.barrier() + # tightest one in [K, kC]. Invalid lines fall back + # to the stock P1b quantile rungs (P1 stats ran on + # this row in that case). + if rungs_ok == cutlass.Int32(1): + if tidx == cutlass.Int32(0): + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + s_mt_thr[m] = seed_thr_row[m] + cute.arch.barrier() + if rungs_ok == cutlass.Int32(0): + self.phase1b_hspace_rungs( + input_row, + N, + pre_idx_row, + pre_idx_count, + pre_idx_offset, + smem_hist, + s_thr, + s_mt_thr, + tidx, + warp_id, + lane, + ) if cutlass.const_expr(not (self.use_ext_counts or self.ext_rungs)): if cutlass.const_expr(self.p1b_cache): # rungs from the SMEM gather-cache P1 stashed (no 2nd @@ -6665,6 +6716,15 @@ def _run_phases( xstate_row[1] = s_thr[0] xstate_row[2] = s_thr[0] xstate_row[3] = cutlass.Float32(cand_count_p4) + if cutlass.const_expr( + self.ext_rungs and not _P4_SUB_DBG and not _P4_TAIL_DBG + ): + # cluster-merged rung counts (identical on + # every CTA after the multi-count DSMEM + # aggregation) + xstate_row[4] = cutlass.Float32(s_mt_cnt[0]) + xstate_row[5] = cutlass.Float32(s_mt_cnt[1]) + xstate_row[6] = cutlass.Float32(s_mt_cnt[2]) # Final cluster barrier: keep peer CTAs (and their SMEM) alive # until the leader's gather + Phase 4 finish. Skipped at From ed64e6ca7b6e8a5b2f69dce0df95484239ce9bb6 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 04:27:40 -0700 Subject: [PATCH 034/117] [None][fix] GVR ext counts: full line validity on both admission previews The counts/list admission previews guarded only t0 finiteness; a NaN in t1/t2 (or inverted lines) from the host closed loop could get parked into the refine brackets - the same failure mode that broke ext_rungs bit-exactness on-chain before its runtime validity check. Both previews now require all three lines finite and strictly ascending; invalid rows fall to the stock seed path. Unified smoke 30/30 bit-exact including adversarial cases (NaN mid line, inverted lines) across v5-list, va and vb modes; v5 callers now pass the pre-packed [rows, 8] seed row (the per-call compat build is off the hot path). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index ce372a829359..3a4a70d22b12 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -5406,7 +5406,18 @@ def _run_phases( ck1 = cutlass.Int64(0) ext_row = cutlass.Int32(0) if cutlass.const_expr(self.use_ext_counts): - if seed_thr_row[0] < cutlass.Float32(1e37): + # line validity mirrors ext_rungs: ALL THREE lines finite and + # strictly ascending. The old t0-only guard let a NaN in + # t1/t2 get parked into the refine brackets (the same failure + # mode that broke ext_rungs exactness on-chain); invalid rows + # fall to the stock path, exactness never rides on the host + # loop's line quality. + if ( + seed_thr_row[0] < cutlass.Float32(1e37) + and seed_thr_row[0] > cutlass.Float32(-1e37) + and seed_thr_row[1] > seed_thr_row[0] + and seed_thr_row[2] > seed_thr_row[1] + ): for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): cm_e = cutlass.Int32(seed_thr_row[3 + m]) if cm_e >= cutlass.Int32(self.top_k) and cm_e <= cutlass.Int32(self.kC): @@ -5428,6 +5439,9 @@ def _run_phases( and claimed_p >= cutlass.Int32(self.top_k + 64) and claimed_p <= cutlass.Int32(self.list_cap) and seed_thr_row[0] < cutlass.Float32(1e37) + and seed_thr_row[0] > cutlass.Float32(-1e37) + and seed_thr_row[1] > seed_thr_row[0] + and seed_thr_row[2] > seed_thr_row[1] ): ext_row = cutlass.Int32(1) # ---- self_scan phase 0: fused scan-bucket ---- From 9b611a961697d953b845d0f0cf725793c849d8e6 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 05:53:14 -0700 Subject: [PATCH 035/117] [None][perf] GVR ext counts: single-column parked count + merged admission prologue The parked M-ary pass counted the SAME admitted threshold in all three rung columns - 3x the compares and 3 per-thread count columns for identical values. Non-skip builds now count it ONCE through the refine primitive (same per-thread ptcnt cache and cluster merge that Phase 3 consumes) and accept in place; the M-ary machinery, argmin, handoff and miss refine stand down via the pre-marked rung column. A measured count outside the band (stale host state) falls back to the full M-ary rerun on the three distinct seed lines. Rung parking also folds into the P1-init thread0 block: one barrier covers the whole admission prologue instead of two. Cold probe (variant A vs B, same node): mid rows flip in A's favor - pro-64k/128k and flash-128k 0.95-0.96x of B (from ~1.05x), cs8 at parity; skip builds keep the M-ary pass (list build lives there). Unified smoke 30/30 bit-exact incl. adversarial lines. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 140 +++++++++++++----- 1 file changed, 103 insertions(+), 37 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 3a4a70d22b12..1c65e5e80ea2 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -5540,6 +5540,34 @@ def _run_phases( s_iscalars[2] = cutlass.Int32(-1) # cnt_lo (fb seeding owns) s_iscalars[3] = cutlass.Int32(-1) # cnt_hi s_iscalars[4] = cutlass.Int32(0) # out_count + # rung parking folded into the SAME thread0 block + # (was a second thread0 block + barrier in the R0 + # region): tightest in-band count picks the admitted + # line; single-column builds stage it in s_thr[0] and + # pre-mark the rung column (M_qf = accepted, -2 = + # defensive M-ary rerun). + bx_m = cutlass.Int32(-1) + bx_c = cutlass.Int32(2147483647) + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + cx = cutlass.Int32(seed_thr_row[3 + m]) + if ( + cx >= cutlass.Int32(self.top_k) + and cx <= cutlass.Int32(self.kC) + and cx < bx_c + ): + bx_m = cutlass.Int32(m) + bx_c = cx + for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): + if bx_m >= cutlass.Int32(0): + s_mt_thr[m] = seed_thr_row[bx_m] + else: + s_mt_thr[m] = seed_thr_row[m] + if cutlass.const_expr(not self.enable_block_skip): + if bx_m >= cutlass.Int32(0): + s_thr[0] = seed_thr_row[bx_m] + s_r0col[0] = cutlass.Int32(self.M_qf) + else: + s_r0col[0] = cutlass.Int32(-2) cute.arch.barrier() if ext_row == cutlass.Int32(0): self.phase1_preidx_stats( @@ -6108,24 +6136,51 @@ def _run_phases( # count (+ list build at that threshold) and classify # admits it; a full miss keeps the 3 distinct rungs as # measured brackets for the seeded refine. - if tidx == cutlass.Int32(0): - bx_m = cutlass.Int32(-1) - bx_c = cutlass.Int32(2147483647) - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - cx = cutlass.Int32(seed_thr_row[3 + m]) - if ( - cx >= cutlass.Int32(self.top_k) - and cx <= cutlass.Int32(self.kC) - and cx < bx_c - ): - bx_m = cutlass.Int32(m) - bx_c = cx - for m in cutlass.range_constexpr(cutlass.const_expr(self.M_thr)): - if bx_m >= cutlass.Int32(0): - s_mt_thr[m] = seed_thr_row[bx_m] - else: - s_mt_thr[m] = seed_thr_row[m] - cute.arch.barrier() + # rung parking + single-path staging happen in + # the P1-init thread0 block (one barrier for + # the whole admission prologue). + if cutlass.const_expr(not self.enable_block_skip): + # v3: the parked M-ary pass counted the SAME + # threshold in all three columns (3x compare + # + 3 ptcnt columns for identical values). + # Count it ONCE with the refine primitive - + # same per-thread ptcnt cache and cluster + # merge P3 consumes - and accept in place. + if s_r0col[0] == cutlass.Int32(self.M_qf): + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[0], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + do_cluster_sync=do_cluster_sync, + smem_input=smem_input, + ) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + cpar = s_iscalars[0] + if cpar >= cutlass.Int32( + self.top_k + ) and cpar <= cutlass.Int32(self.kC): + s_iscalars[1] = cutlass.Int32(1) + else: + # emission counts disagree with + # the measured count (stale + # host state): rerun the full + # M-ary machinery on the three + # distinct seed lines. + s_r0col[0] = cutlass.Int32(-2) + for m in cutlass.range_constexpr( + cutlass.const_expr(self.M_thr) + ): + s_mt_thr[m] = seed_thr_row[m] + cute.arch.barrier() if ext_row == cutlass.Int32(0): if cutlass.const_expr(self.p1b_cache): # rungs from the SMEM gather-cache P1 stashed (no 2nd @@ -6208,26 +6263,37 @@ def _run_phases( warp_id, lane, ) - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) + r0_par = cutlass.Int32(0) + if cutlass.const_expr(self.use_ext_counts and not self.enable_block_skip): + # single-column fast path accepted: the parked count + # is done and admitted; the M-ary pass, argmin, + # handoff and miss machinery all stand down + # (s_r0col == M_qf skips the copy and the refine). + if s_r0col[0] == cutlass.Int32(self.M_qf) and s_iscalars[ + 1 + ] == cutlass.Int32(1): + r0_par = cutlass.Int32(1) + if r0_par == cutlass.Int32(0): + self.block_count_ge_multi( + input_row, + slice_start, + slice_end, + s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, + tidx, + warp_id, + lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, + ) cute.arch.barrier() - if tidx == 0: + if tidx == 0 and r0_par == cutlass.Int32(0): # tightest admissible rung = SMALLEST count in [K, kC]. # (Explicit argmin: with r0_vseed the pmean column is not # sorted into the rung order; for sorted rungs this is From fc0137aa54ba7db81b4a294b5650d167e4677321 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:16:29 -0700 Subject: [PATCH 036/117] [None][perf] GVR wrapper: 512 threads for small-K list-hit rows List-hit rows do O(list) work (~2-4K entries at K<=512), not O(N); the N-keyed thread heuristic handed them 1024 threads and paid the barrier-heavy refine phases for no parallel gain. Key the pick on the mode: ext-cand with K<=512 takes 512 threads (flash-512k v5 hit path 8.4 -> 7.1us cold, -15%); K=1024 lists are large enough to keep the N-keyed pick (pro cells within +/-4%). Unified smoke 30/30 bit-exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index eb4209a59dbc..268264b9b956 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -871,7 +871,14 @@ def gvr_topk_decode( N_cols = logits.shape[1] N_dec = max_seq_len if max_seq_len is not None else N_cols if num_threads_per_block is None: - if self_scan: + if use_ext_cand and top_k <= 512: + # list-hit rows do O(list) work (~2-4K entries at K<=512), + # not O(N): the N-keyed 1024-thread pick pays barrier-heavy + # phases for no parallel gain (flash-512k v5: 8.4 -> 7.1us + # at 512 threads; K=1024 lists are big enough to keep the + # N-keyed pick). + num_threads_per_block = 512 + elif self_scan: # self_scan owns the whole row scan in one CTA: the phase-0 # cp.async pipeline scales with warp count at every N (the # 512-thread short-row heuristic below is tuned for the From a6728fa9f266a565038c24d8e3166843c8525635 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 26 Jul 2026 07:39:50 -0700 Subject: [PATCH 037/117] [None][perf] GVR ext counts: fused count-free take for short rows With the admitted line's exact count already known from emission, the count pass exists only to build Phase 3's placement prefix. Short rows (N < 16384) now claim-collect in ONE pass: 4 chunks in flight per iteration (the count primitive's ILP shape) and per-element direct smem atomics for passers (no warp sync, claims hide under the read stream); the claim total re-verifies the count and take_cand bypasses P2+P3 wholesale, landing straight in P4 (candidate-order agnostic, as the v5 list path already proves). Out-of-band claims (stale host state) reset and fall back to the stock single-count path. Two falsified shapes are documented by the arc: per-element scalar loads (list-walk habit, +9us at 128k) and per-position ballots (the self_scan-falsified warp-lock pattern, +3us). The N-gate keeps the claim-dense 16k+ cells (pro band at 9.4%) on count-then-place, which measures at parity or better there. Cold probe vs variant B on the same node: every gated cell wins - flash-8k/32k 0.95/0.89, pro-16k/32k 0.89/0.90; out-of-gate cells stay at their two-pass optima (0.93-0.97). Unified smoke 30/30 bit-exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 1c65e5e80ea2..7dad3d33a8f1 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -6026,6 +6026,126 @@ def _run_phases( if cutlass.const_expr(_P4_TAIL_DBG): ck1 = cute.arch.clock64() + # ---- parked count-free take (ext counts, no list): the + # emission already measured the admitted line's exact count, + # so the count pass exists ONLY to build P3's placement + # prefix. Claim-collect instead (v5 edge-cut walk shape: + # 4-way strided reads, ballot-merged claims): ONE cold pass + # replaces count(cold) + collect(L2-hot). P4 is candidate- + # order agnostic (the v5 list path feeds it emission-claim + # order already). The claim total re-measures the count; a + # mismatch with the parked band (stale host state) leaves + # take_cand=0 and the stock single-count path below recovers. + if cutlass.const_expr( + self.use_ext_counts + and not self.use_ext_cand + and not self.self_scan + and not self.enable_block_skip + and cluster_size == 1 + and self.dtype == cutlass.Float32 + ): + if ext_row == cutlass.Int32(1) and N < cutlass.Int32(16384): + # short rows only: the fused walk wins ~1us below + # ~16k (pass fixed costs dominate there); at 16k+ + # the claim-dense cells (pro 9.4% band) lose to + # count-then-place and the rest sit at parity, so + # the two-pass path keeps them (measured both ways, + # loncheng cells; captures all sit above the gate) + cut_p = s_thr[0] # parked line (staged at P1-init) + rbase = input_row.iterator.toint() + vw_p = cutlass.const_expr(self.vec_bits // self.dtype.width) + va_p = cutlass.const_expr(self.vec_align_bytes) + eb_p = cutlass.const_expr(self.dtype.width // 8) + cp_atom = self._make_load_copy_atom() + frag0 = cute.make_fragment((vw_p,), self.dtype) + frag1 = cute.make_fragment((vw_p,), self.dtype) + frag2 = cute.make_fragment((vw_p,), self.dtype) + frag3 = cute.make_fragment((vw_p,), self.dtype) + step4_p = cutlass.const_expr(4 * num_threads * vw_p) + nfull_p = (N // cutlass.Int32(step4_p)) * cutlass.Int32(step4_p) + it_p = tidx * cutlass.Int32(vw_p) + # 4 chunks in flight per iter (the count primitive's + # ILP shape); per-element DIRECT smem atomics for + # passers - no warp sync, claims hide under the reads + while it_p < nfull_p: + for _jf in cutlass.range_constexpr(4): + gp_p = cute.make_ptr( + self.dtype, + rbase + + cutlass.Int64(it_p + cutlass.Int32(_jf * num_threads * vw_p)) + * cutlass.Int64(eb_p), + cute.AddressSpace.gmem, + assumed_align=va_p, + ) + cute.copy( + cp_atom, + cute.make_tensor(gp_p, cute.make_layout((vw_p,))), + frag0 + if _jf == 0 + else frag1 + if _jf == 1 + else frag2 + if _jf == 2 + else frag3, + ) + for _jf in cutlass.range_constexpr(4): + for _jv in cutlass.range_constexpr(vw_p): + v_p = cutlass.Float32( + ( + frag0 + if _jf == 0 + else frag1 + if _jf == 1 + else frag2 + if _jf == 2 + else frag3 + )[_jv] + ) + if v_p >= cut_p: + sl_p = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if sl_p < cutlass.Int32(self.kC): + smem_keys[sl_p] = v_p + smem_vals[sl_p] = ( + it_p + + cutlass.Int32(_jf * num_threads * vw_p) + + cutlass.Int32(_jv) + ) + it_p = it_p + cutlass.Int32(step4_p) + i_p = nfull_p + tidx + while i_p < N: + vp_p = cute.make_ptr( + cutlass.Float32, + rbase + cutlass.Int64(i_p) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + pval = cute.make_tensor(vp_p, cute.make_layout((1,)))[0] + if pval >= cut_p: + sl_p = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if sl_p < cutlass.Int32(self.kC): + smem_keys[sl_p] = pval + smem_vals[sl_p] = i_p + i_p = i_p + cutlass.Int32(num_threads) + cute.arch.barrier() + cnt_p = s_iscalars[0] + if cnt_p >= cutlass.Int32(self.top_k) and cnt_p <= cutlass.Int32(self.kC): + take_cand = cutlass.Int32(1) + if tidx == cutlass.Int32(0): + s_iscalars[1] = cutlass.Int32(1) # done + cute.arch.barrier() + if take_cand == cutlass.Int32(0): + # stale host counts: reset the claim counter so + # the stock single-count path re-measures cleanly + if tidx == cutlass.Int32(0): + s_iscalars[0] = cutlass.Int32(0) + cute.arch.barrier() + # ---- self_scan take: cut straight from the phase-0 cursors ---- # Same admission state machine as the v5 list (tightest line # whose count fits [K, B*] wins; anchor = loosest in-band From c1c5ada29b99cf1adf4f51d363c45a34f7250a6b Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:37:19 -0700 Subject: [PATCH 038/117] [None][perf] FP4 indexer: port fused block-metadata emission (P0) Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 95 +++- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 466 +++++++++++++++++- .../test_cute_dsl_fp4_paged_mqa_logits.py | 212 ++++++++ 3 files changed, 761 insertions(+), 12 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 0db351140735..d387d9f10ba2 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -8250,11 +8250,13 @@ def _compile(cls, num_epi_subtiles, epi_dtype, output_dtype, - remove_online_sf_transpose=False): + remove_online_sf_transpose=False, + emit_block_meta=False, + emit_hit_stats=True): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, - remove_online_sf_transpose) + remove_online_sf_transpose, emit_block_meta, emit_hit_stats) if key in cls.kernel_cache: return @@ -8317,6 +8319,26 @@ def _compile(cls, (num_ctas, 2), stride_order=(1, 0)) + # Block-meta tensors (fused-GVR support): nb_pad*4 records. + block_max_fake = None + hit_stats_fake = None + hit_bitmap_fake = None + if emit_block_meta: + nb_sym = cute.sym_int() + block_max_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), nb_sym), + stride_order=(1, 0), + assumed_align=16) + if emit_hit_stats: + hit_stats_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), 4), + stride_order=(1, 0), + assumed_align=16) + hit_bitmap_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (sym_B, cute.sym_int()), + stride_order=(1, 0), + assumed_align=16) + fake_stream = cute.runtime.make_fake_stream( use_tvm_ffi_env_stream=True) @@ -8331,6 +8353,8 @@ def _compile(cls, epi_dtype=to_cutlass[epi_dtype], output_dtype=to_cutlass[output_dtype], remove_online_sf_transpose=remove_online_sf_transpose, + emit_block_meta=emit_block_meta, + emit_hit_stats=emit_hit_stats, ) compiled = cute.compile( @@ -8345,6 +8369,9 @@ def _compile(cls, sm_fake, cutlass.Int32(1), cutlass.Int32(1), + block_max_fake, + hit_stats_fake, + hit_bitmap_fake, fake_stream, options="--enable-tvm-ffi", ) @@ -8366,6 +8393,11 @@ def forward( epi_dtype: torch.dtype = torch.float32, output_dtype: torch.dtype = torch.float32, remove_online_sf_transpose: bool = False, + emit_block_meta: bool = False, + emit_hit_stats: bool = True, + hit_bitmap: Optional[torch.Tensor] = None, + block_max_out: Optional[torch.Tensor] = None, + hit_stats_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Execute FP4 paged MQA logits kernel. @@ -8381,8 +8413,15 @@ def forward( num_epi_subtiles: epilogue sub-tile count (1, 2, or 4) epi_dtype: epilogue compute dtype output_dtype: output logits dtype + emit_block_meta: also emit per-128-block metadata for the + fused GVR top-k. Requires ``hit_bitmap`` + [B, nb_pad*4] int32 (1 bit per compressed kv position, + request-level). ``block_max_out``/``hit_stats_out`` + ([B*next_n, nb_pad] fp32 / [B*next_n, nb_pad, 4] fp32) + are allocated when not supplied. Returns: - logits: [B*next_n, max_context_len] output_dtype + logits [B*next_n, max_context_len]; with emit_block_meta, + the tuple (logits, block_max, hit_stats). """ B, next_n, H, half_D = q.shape N = next_n * H @@ -8435,10 +8474,46 @@ def forward( ) logits = logits[:, :max_context_len] + # Block-meta buffers (fused-GVR support). nb_pad mirrors the + # logits padding so WG1's odd-num_kv OOB tile lands in padding. + if emit_block_meta: + nb_pad = aligned_max_ctx // compute_block_kv + # 4 warp-partial records per block (see FP4MQALogitsKernel). + nrec = nb_pad * 4 + if emit_hit_stats: + assert ( + hit_bitmap is not None + and hit_bitmap.dtype == torch.int32 + and hit_bitmap.is_cuda and hit_bitmap.is_contiguous() + and hit_bitmap.dim() == 2 and hit_bitmap.shape[0] == B + and hit_bitmap.shape[1] >= nb_pad * 4 + ), (f"emit_hit_stats requires hit_bitmap int32 " + f"[{B}, >= {nb_pad * 4}]; got " + f"{None if hit_bitmap is None else (hit_bitmap.dtype, tuple(hit_bitmap.shape))}" + ) + # Per-row aggregate {enc_min, enc_max, sum, cnt}. The + # kernel only ATOMICALLY MERGES into this buffer — the + # caller must pre-initialize it to the identities + # {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} each step. + assert hit_stats_out is not None, ( + "emit_hit_stats requires a caller-initialized " + "hit_stats_out [B*next_n, 4] fp32 (identity-filled)") + assert (hit_stats_out.shape == (B * next_n, 4) + and hit_stats_out.is_contiguous()) + else: + hit_bitmap = None + hit_stats_out = None + if block_max_out is None: + block_max_out = torch.empty((B * next_n, nrec), + device=q.device, + dtype=torch.float32) + assert (block_max_out.shape == (B * next_n, nrec) + and block_max_out.is_contiguous()) + # Compile if needed (fake tensors, no real data required) key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, - remove_online_sf_transpose) + remove_online_sf_transpose, emit_block_meta, emit_hit_stats) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8450,12 +8525,20 @@ def forward( num_epi_subtiles, epi_dtype, output_dtype, - remove_online_sf_transpose=remove_online_sf_transpose) + remove_online_sf_transpose=remove_online_sf_transpose, + emit_block_meta=emit_block_meta, + emit_hit_stats=emit_hit_stats) compiled = cls.kernel_cache[key] # TVM FFI: pass raw tensors, no dlpack/stream needed + if emit_block_meta: + compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, + context_lens, schedule_meta, num_phys_blocks, B, + block_max_out, hit_stats_out, hit_bitmap) + return logits, block_max_out, hit_stats_out compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, - context_lens, schedule_meta, num_phys_blocks, B) + context_lens, schedule_meta, num_phys_blocks, B, None, + None, None) return logits @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index 94db5d143085..dfbdc00dbed5 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -64,6 +64,79 @@ # form is also accepted by older wrappers, so keep it version-independent. _RND_RN = "rn" +# Reduction identities for the emit_block_meta epilogue path; match the GVR +# kernel's FLT_MAX/NEG_FLT_MAX sentinels (gvr_topk_decode.py) so the fused +# Phase 1's degenerate checks behave identically to the gather path. +_META_FLT_MAX = 3.4028235e38 +_META_NEG_FLT_MAX = -3.4028235e38 + + +# --------------------------------------------------------------------------- +# Global-memory reductions for the per-row hit aggregate (emit_hit_stats). +# fp32 has no native atomic min/max; we use the standard order-preserving +# int encoding enc(f) = bits(f) >= 0 ? bits(f) : bits(f) ^ 0x7FFFFFFF +# (an involution; signed-int order == float order, -0.0 quirk harmless for +# min/max seeding) and red.global.{min,max}.s32. Sum uses red.global.add.f32 +# (order-nondeterministic — perturbs only the heuristic mean seed). +# --------------------------------------------------------------------------- +@dsl_user_op +def _red_global_fmin_ordered(addr_i64, fval, *, loc=None, ip=None): + llvm.inline_asm( + None, + [addr_i64.ir_value(loc=loc, ip=ip), fval.ir_value(loc=loc, ip=ip)], + "{\n\t" + ".reg .b32 k;\n\t" + ".reg .pred p;\n\t" + "mov.b32 k, $1;\n\t" + "setp.lt.s32 p, k, 0;\n\t" + "@p xor.b32 k, k, 0x7FFFFFFF;\n\t" + "red.global.min.s32 [$0], k;\n\t" + "}", + "l,f", + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def _red_global_fmax_ordered(addr_i64, fval, *, loc=None, ip=None): + llvm.inline_asm( + None, + [addr_i64.ir_value(loc=loc, ip=ip), fval.ir_value(loc=loc, ip=ip)], + "{\n\t" + ".reg .b32 k;\n\t" + ".reg .pred p;\n\t" + "mov.b32 k, $1;\n\t" + "setp.lt.s32 p, k, 0;\n\t" + "@p xor.b32 k, k, 0x7FFFFFFF;\n\t" + "red.global.max.s32 [$0], k;\n\t" + "}", + "l,f", + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def _red_global_add_f32(addr_i64, fval, *, loc=None, ip=None): + llvm.inline_asm( + None, + [addr_i64.ir_value(loc=loc, ip=ip), fval.ir_value(loc=loc, ip=ip)], + "red.global.add.f32 [$0], $1;", + "l,f", + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + loc=loc, + ip=ip, + ) + @dsl_user_op def pack_f16x2( @@ -363,6 +436,8 @@ def __init__( output_dtype=cutlass.Float32, remove_online_sf_transpose: bool = False, use_batched_store: bool = True, + emit_block_meta: bool = False, + emit_hit_stats: bool = True, ): # Static FP4 invariants — see plan Sanity checklist. assert num_heads == 64, "FP4 kernel hardcodes num_heads=64 for TMEM/SMEM budget" @@ -407,6 +482,38 @@ def __init__( # When True, defer per-t STG to register array and emit all STGs in # one contiguous LSU phase after the for-t loop (epilogue micro-opt). self.use_batched_store = use_batched_store + # When True, the epilogue additionally emits per-128-token-block + # metadata consumed by the fused GVR top-k (gvr_topk_decode.py — + # fused_preidx_stats / enable_block_skip). Emission is fully + # WARP-AUTONOMOUS: each of the WG's 4 warps writes one partial + # record per tile per t (record index = tile*4 + warp); the GVR + # consumer folds the 4 partials per block. No cross-warp barrier — + # a per-tile named-barrier fold costs +53% indexer wall-clock. + # block_max [num_rows, nb_pad*4] fp32 — warp-partial max of + # f32(stored logit) over valid positions (kv_pos < ctx); + # fold(4) is the lossless block-skip upper bound. Computed on + # the POST-conversion value so it bounds what GVR reads back, + # bit-exactly. + # hit_agg [num_rows, 4] fp32 — PER-ROW aggregate + # {enc_min, enc_max, sum, cnt} of stored logits at positions + # flagged in hit_bitmap; min/max slots hold the + # order-preserving int encoding (see _red_global_fmin_ordered). + # Buffer must be pre-initialized to + # {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} per step. Replaces GVR + # Phase 1's random gather. + # hit_bitmap [batch, nb_pad*4] int32 — 1 bit per kv + # position (request-level; from the previous step's top-k). + # Cost per tile per t: 1 bitmap LDG/thread + 1 warp redux + 1 + # lane-0 STG (block_max) + <= 4 LANE-LOCAL accumulator ops (hit + # stats); the hit accumulators flush ONCE per q-transition via + # atomics (_flush_hit_agg). A per-tile warp-redux emission of the + # hit stats was tried first and cost +72% indexer wall-clock. + # + # emit_hit_stats sub-knob (only meaningful with emit_block_meta): + # False emits block_max ONLY (no bitmap read, no hit aggregate) — + # sufficient for GVR block-skip without the fused Phase 1. + self.emit_block_meta = emit_block_meta + self.emit_hit_stats = emit_hit_stats # epi_bytes covers fp16 and bf16 (FP8 only handled fp16). self.epi_bytes = 2 if epi_dtype in (cutlass.Float16, cutlass.BFloat16) else 4 # sW stage stride padded to 128-byte SMEM alignment for TMA bulk copy. @@ -627,6 +734,9 @@ def __call__( schedule_meta: cute.Tensor, # [num_sms+1, 2] int32 num_phys_blocks: cutlass.Int32, batch_size: cutlass.Int32, + block_max: cute.Tensor, # [num_rows, nb_pad*4] fp32 warp-partials (or None) + hit_stats: cute.Tensor, # [num_rows, 4] fp32 hit aggregate (or None unless emit_hit_stats) + hit_bitmap: cute.Tensor, # [batch, nb_pad*4] int32 (or None unless emit_block_meta) stream: cuda.CUstream, ): # Derive KV data and SF views from the fused uint8 buffer. @@ -826,6 +936,9 @@ class SharedStorage: context_lens, schedule_meta, batch_size, + block_max, + hit_stats, + hit_bitmap, self.cluster_layout_vmnk, self.a_smem_layout_staged, self.b_smem_layout_staged, @@ -848,6 +961,44 @@ class SharedStorage: stream=stream, ) + @cute.jit + def _flush_hit_agg( + self, + mHitAgg, # [num_rows, 4] fp32 {enc_min, enc_max, sum, cnt} + q_flush, # request whose accumulators are being flushed + hacc_min, + hacc_max, + hacc_sum, + hacc_cnt, + meta_lane, + ): + """Warp-reduce the per-lane hit accumulators and merge them into the + per-row global aggregate via one set of atomics (encoded-int + min/max + fp32 adds), then reset to identities. Called once per + q-transition per warp — atomic traffic is negligible. The + aggregate buffer must be pre-initialized to + {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} per step (prototype: test + harness; production: folded into the bitmap prepare kernel).""" + next_n = cutlass.const_expr(self.next_n) + base_addr = mHitAgg.iterator.toint() + for t in cutlass.range_constexpr(next_n): + w_min = cute.arch.warp_redux_sync(hacc_min[t], "fmin") + w_max = cute.arch.warp_redux_sync(hacc_max[t], "fmax") + w_sum = cute.arch.warp_reduction_sum(hacc_sum[t]) + w_cnt = cute.arch.warp_redux_sync(hacc_cnt[t], "add") + if meta_lane == cutlass.Int32(0): + if w_cnt > cutlass.Int32(0): + row = q_flush * cutlass.Int32(next_n) + cutlass.Int32(t) + row_addr = base_addr + cutlass.Int64(row) * cutlass.Int64(16) + _red_global_fmin_ordered(row_addr, w_min) + _red_global_fmax_ordered(row_addr + cutlass.Int64(4), w_max) + _red_global_add_f32(row_addr + cutlass.Int64(8), w_sum) + _red_global_add_f32(row_addr + cutlass.Int64(12), cutlass.Float32(w_cnt)) + hacc_min[t] = cutlass.Float32(_META_FLT_MAX) + hacc_max[t] = cutlass.Float32(_META_NEG_FLT_MAX) + hacc_sum[t] = cutlass.Float32(0.0) + hacc_cnt[t] = cutlass.Int32(0) + @cute.kernel def kernel( self, @@ -867,6 +1018,9 @@ def kernel( mContextLens: cute.Tensor, # [batch_size] mScheduleMeta: cute.Tensor, # [num_sms+1, 2] int32 batch_size: cutlass.Int32, + mBlockMax: cute.Tensor, # [num_rows, nb_pad*4] fp32 warp-partials (or None) + mHitAgg: cute.Tensor, # [num_rows, 4] fp32 {enc_min, enc_max, sum, cnt} (or None) + mHitBitmap: cute.Tensor, # [batch, nb_pad*4] int32 (or None) cluster_layout_vmnk: cute.Layout, a_smem_layout_staged: cute.ComposedLayout, b_smem_layout_staged: cute.ComposedLayout, @@ -1115,6 +1269,12 @@ def kernel( layout=sf_kv_smem_layout_staged, byte_alignment=128, ) + # Block-meta emission is fully warp-autonomous (each warp's lane 0 + # writes its own warp-partial record straight to GMEM; the GVR + # side folds 4 partials per block) — no SMEM scratch, no named + # barrier. A cross-warp SMEM fold + per-tile named barrier was + # tried first and cost +53% indexer wall-clock by serializing the + # epilogue's warp pipelining. a_mcast_mask = cpasync.create_tma_multicast_mask( cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2 @@ -1661,7 +1821,6 @@ def kernel( ) // block_kv_val # Update while-loop condition has_work = (next_q_idx != end_q_idx) | (next_kv_idx != end_kv_idx) - elif is_umma_warp_1: # UMMA warp for group 1 # Explicitly waits on Q pipeline — critical because TMA warp 1 @@ -1794,7 +1953,6 @@ def kernel( ) // block_kv_val # Update while-loop condition has_work = (next_q_idx != end_q_idx) | (next_kv_idx != end_kv_idx) - elif is_math_warp: cute.arch.warpgroup_reg_alloc(240) @@ -1838,6 +1996,15 @@ def kernel( MAX_NUM_W_IN_REG = 64 else: # fp32, 4-byte weights MAX_NUM_W_IN_REG = 56 if next_n == 3 else 64 + if cutlass.const_expr(self.emit_block_meta): + # Free ~8 registers for the meta accumulators/fragments + # — the epilogue's weight cache is tuned to the spill + # edge (see MAX_NUM_W_IN_REG SASS notes above). + MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 + if cutlass.const_expr(self.emit_hit_stats): + # Hit accumulators + bitmap word add ~6 more live + # registers across the tile loop. + MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -1848,6 +2015,33 @@ def kernel( else: result_arr = None q_stage_local = cutlass.Int32(0) + if cutlass.const_expr(self.emit_block_meta): + ctx_cur = cutlass.Int32(0) + meta_warp = local_tidx // 32 + meta_lane = local_tidx % 32 + if cutlass.const_expr(self.emit_hit_stats): + # Per-lane hit accumulators, carried across ALL + # tiles of the same q and flushed once per + # q-transition — zero warp-wide ops per tile (the + # per-tile redux version cost +72% wall-clock). + hacc_min = cute.make_fragment(next_n, cutlass.Float32) + hacc_max = cute.make_fragment(next_n, cutlass.Float32) + hacc_sum = cute.make_fragment(next_n, cutlass.Float32) + hacc_cnt = cute.make_fragment(next_n, cutlass.Int32) + for _t in cutlass.range_constexpr(next_n): + hacc_min[_t] = cutlass.Float32(_META_FLT_MAX) + hacc_max[_t] = cutlass.Float32(_META_NEG_FLT_MAX) + hacc_sum[_t] = cutlass.Float32(0.0) + hacc_cnt[_t] = cutlass.Int32(0) + # Batched bitmap read state: a per-tile LDG's + # ~300cy L2 trip lands on the critical path every + # tile (isolated at +42% kernel time; one-ahead + # prefetch did not help). All 32 lanes of a warp + # need the SAME word per tile, so instead lane l + # loads the word for tile j+l once per 32 tiles + # and each tile takes its word via one shuffle. + meta_j = cutlass.Int32(0) + hitw_batch = cutlass.Int32(0) while has_work: # fetch_next_task: commit next → current @@ -1869,11 +2063,51 @@ def kernel( w_cache[t_i * NUM_W_IN_REG + w_j] = sW[ (t_i * num_heads + w_j, q_stage_local) ] + if cutlass.const_expr(self.emit_block_meta): + # Flush the PREVIOUS request's hit accumulators + # before switching context. + if cutlass.const_expr(self.emit_hit_stats): + if q_idx_old < batch_size: + self._flush_hit_agg( + mHitAgg, + q_idx_old, + hacc_min, + hacc_max, + hacc_sum, + hacc_cnt, + meta_lane, + ) + # New bitmap row: invalidate the batched + # word cache (forces a reload). + meta_j = cutlass.Int32(0) + # Compressed-space context len; the meta valid + # mask (kv_pos < ctx_cur) keeps GEMM garbage in + # the aligned padding region out of block_max. + ctx_cur = mContextLens[q_idx] # Process KV block for group 0 (kv_idx + 0) # Unconditional Math: OOB results # written to aligned padding region in logits buffer. kv_pos = kv_idx * block_kv_val + m_coord + if cutlass.const_expr(self.emit_block_meta): + meta_kv_tile = kv_idx + meta_valid = kv_pos < ctx_cur + if cutlass.const_expr(self.emit_hit_stats): + # Warp-uniform reload once per 32 tiles: this + # WG's tile at counter j+l is kv_tile + 2*l, + # whose warp word index is (kv_tile+2*l)*4 + + # warp. Clamp keeps end-of-row lanes in + # bounds (their tiles are never consumed). + if (meta_j & cutlass.Int32(31)) == cutlass.Int32(0): + w_idx = ( + meta_kv_tile + cutlass.Int32(2) * meta_lane + ) * cutlass.Int32(4) + meta_warp + w_idx = min(w_idx, mHitBitmap.shape[1] - cutlass.Int32(1)) + hitw_batch = mHitBitmap[(q_idx, w_idx)] + hit_word = cute.arch.shuffle_sync( + hitw_batch, meta_j & cutlass.Int32(31) + ) + meta_j = meta_j + cutlass.Int32(1) # Step 5.7: drop kv_pipeline.consumer_wait/release and # scale_val LDS — UMMA owns KV+SF pipe; SF is baked into @@ -2054,11 +2288,74 @@ def kernel( else: result_t = s0x + s0y + s1x + s1y # Step 5.7: drop * scale_val (FP4 SF baked into acc). + stored_t = self.output_dtype(result_t) if cutlass.const_expr(self.use_batched_store): - result_arr[t] = self.output_dtype(result_t) + result_arr[t] = stored_t else: out_row = q_idx * next_n + t - mLogits[(out_row, kv_pos)] = self.output_dtype(result_t) + mLogits[(out_row, kv_pos)] = stored_t + if cutlass.const_expr(self.emit_block_meta): + # Meta reduction on the POST-conversion value so + # block_max bounds what GVR reads back bit-exactly + # (pre-conversion fp32 max could round up past a + # stored logit's converted value's block max). + f32_t = cutlass.Float32(stored_t) + bmax_v = cutlass.Float32(_META_NEG_FLT_MAX) + if meta_valid: + bmax_v = f32_t + r_bmax = cute.arch.warp_redux_sync(bmax_v, "fmax") + # Warp-autonomous store: record index = + # tile*4 + warp; the GVR consumer folds the 4 + # warp-partials per block (fold of partials == + # block stat — associative + identity-padded). + if meta_lane == cutlass.Int32(0): + out_row_m = q_idx * next_n + t + rec_m = meta_kv_tile * cutlass.Int32(4) + meta_warp + mBlockMax[(out_row_m, rec_m)] = r_bmax + if cutlass.const_expr(self.emit_hit_stats): + # Lane-local accumulation, fully BRANCHLESS + # (data-dependent `if meta_hit` compiled to + # real divergent branches whose condition + # waits on the bitmap LDG — 43% of all warp + # stall samples). Bit-mask select is also + # NaN-safe for OOB-tile garbage logits. No + # valid-mask needed: the bitmap contract + # only sets bits inside [0, ctx). + meta_hit = ( + hit_word >> (kv_pos & cutlass.Int32(31)) + ) & cutlass.Int32(1) + msk = cutlass.Int32(0) - meta_hit # 0 / ~0 + inv = cutlass.Int32(-1) - msk + fbits = cutlass.Int32( + llvm.bitcast(cutlass.Int32.mlir_type, f32_t.ir_value()) + ) + # bits(+FLT_MAX)=0x7F7FFFFF, + # bits(-FLT_MAX)=0xFF7FFFFF (as i32: neg). + selmin = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + ( + (fbits & msk) | (cutlass.Int32(0x7F7FFFFF) & inv) + ).ir_value(), + ) + ) + selmax = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + ( + (fbits & msk) | (cutlass.Int32(-8388609) & inv) + ).ir_value(), + ) + ) + seladd = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, (fbits & msk).ir_value() + ) + ) + hacc_min[t] = cutlass.min(hacc_min[t], selmin) + hacc_max[t] = cutlass.max(hacc_max[t], selmax) + hacc_sum[t] = hacc_sum[t] + seladd + hacc_cnt[t] = hacc_cnt[t] + meta_hit if cutlass.const_expr(self.use_batched_store): # Batched STG: all result_arr[t] → mLogits in one pass. @@ -2078,6 +2375,13 @@ def kernel( # Update while-loop condition has_work = (next_q_idx != end_q_idx) | (next_kv_idx != end_kv_idx) + # Flush the final request's hit accumulators (WG 0). + if cutlass.const_expr(self.emit_block_meta and self.emit_hit_stats): + if q_idx < batch_size: + self._flush_hit_agg( + mHitAgg, q_idx, hacc_min, hacc_max, hacc_sum, hacc_cnt, meta_lane + ) + # Release last Q stage (WG 0) if q_idx < batch_size: q_pipeline.consumer_release(q_cons_state) @@ -2101,6 +2405,15 @@ def kernel( MAX_NUM_W_IN_REG = 64 else: MAX_NUM_W_IN_REG = 56 if next_n == 3 else 64 + if cutlass.const_expr(self.emit_block_meta): + # Free ~8 registers for the meta accumulators/fragments + # — the epilogue's weight cache is tuned to the spill + # edge (see MAX_NUM_W_IN_REG SASS notes above). + MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 + if cutlass.const_expr(self.emit_hit_stats): + # Hit accumulators + bitmap word add ~6 more live + # registers across the tile loop. + MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -2111,6 +2424,33 @@ def kernel( else: result_arr = None q_stage_local = cutlass.Int32(0) + if cutlass.const_expr(self.emit_block_meta): + ctx_cur = cutlass.Int32(0) + meta_warp = local_tidx // 32 + meta_lane = local_tidx % 32 + if cutlass.const_expr(self.emit_hit_stats): + # Per-lane hit accumulators, carried across ALL + # tiles of the same q and flushed once per + # q-transition — zero warp-wide ops per tile (the + # per-tile redux version cost +72% wall-clock). + hacc_min = cute.make_fragment(next_n, cutlass.Float32) + hacc_max = cute.make_fragment(next_n, cutlass.Float32) + hacc_sum = cute.make_fragment(next_n, cutlass.Float32) + hacc_cnt = cute.make_fragment(next_n, cutlass.Int32) + for _t in cutlass.range_constexpr(next_n): + hacc_min[_t] = cutlass.Float32(_META_FLT_MAX) + hacc_max[_t] = cutlass.Float32(_META_NEG_FLT_MAX) + hacc_sum[_t] = cutlass.Float32(0.0) + hacc_cnt[_t] = cutlass.Int32(0) + # Batched bitmap read state: a per-tile LDG's + # ~300cy L2 trip lands on the critical path every + # tile (isolated at +42% kernel time; one-ahead + # prefetch did not help). All 32 lanes of a warp + # need the SAME word per tile, so instead lane l + # loads the word for tile j+l once per 32 tiles + # and each tile takes its word via one shuffle. + meta_j = cutlass.Int32(0) + hitw_batch = cutlass.Int32(0) while has_work: # fetch_next_task: commit next → current @@ -2132,12 +2472,56 @@ def kernel( w_cache[t_i * NUM_W_IN_REG + w_j] = sW[ (t_i * num_heads + w_j, q_stage_local) ] + if cutlass.const_expr(self.emit_block_meta): + # Flush the PREVIOUS request's hit accumulators + # before switching context. + if cutlass.const_expr(self.emit_hit_stats): + if q_idx_old < batch_size: + self._flush_hit_agg( + mHitAgg, + q_idx_old, + hacc_min, + hacc_max, + hacc_sum, + hacc_cnt, + meta_lane, + ) + # New bitmap row: invalidate the batched + # word cache (forces a reload). + meta_j = cutlass.Int32(0) + # Compressed-space context len; the meta valid + # mask (kv_pos < ctx_cur) keeps GEMM garbage in + # the aligned padding region out of block_max. + ctx_cur = mContextLens[q_idx] # Process KV block for group 1 (kv_idx + 1) # Unconditional Math kv_idx_1 = kv_idx + 1 kv_pos = kv_idx_1 * block_kv_val + m_coord + if cutlass.const_expr(self.emit_block_meta): + meta_kv_tile = kv_idx_1 + # See WG 0. For the odd-num_kv OOB tile + # (kv_idx_1 == num_kv) every lane has + # kv_pos >= ctx_cur, so identities land in the + # nb_pad padding slot — never read by GVR. + meta_valid = kv_pos < ctx_cur + if cutlass.const_expr(self.emit_hit_stats): + # Warp-uniform reload once per 32 tiles: this + # WG's tile at counter j+l is kv_tile + 2*l, + # whose warp word index is (kv_tile+2*l)*4 + + # warp. Clamp keeps end-of-row lanes in + # bounds (their tiles are never consumed). + if (meta_j & cutlass.Int32(31)) == cutlass.Int32(0): + w_idx = ( + meta_kv_tile + cutlass.Int32(2) * meta_lane + ) * cutlass.Int32(4) + meta_warp + w_idx = min(w_idx, mHitBitmap.shape[1] - cutlass.Int32(1)) + hitw_batch = mHitBitmap[(q_idx, w_idx)] + hit_word = cute.arch.shuffle_sync( + hitw_batch, meta_j & cutlass.Int32(31) + ) + meta_j = meta_j + cutlass.Int32(1) # Step 5.7: drop kv_pipeline.consumer_wait/release and # scale_val LDS — UMMA owns KV+SF pipe. @@ -2307,11 +2691,74 @@ def kernel( else: result_t = s0x + s0y + s1x + s1y # Step 5.7: drop * scale_val (FP4 SF baked into acc). + stored_t = self.output_dtype(result_t) if cutlass.const_expr(self.use_batched_store): - result_arr[t] = self.output_dtype(result_t) + result_arr[t] = stored_t else: out_row = q_idx * next_n + t - mLogits[(out_row, kv_pos)] = self.output_dtype(result_t) + mLogits[(out_row, kv_pos)] = stored_t + if cutlass.const_expr(self.emit_block_meta): + # Meta reduction on the POST-conversion value so + # block_max bounds what GVR reads back bit-exactly + # (pre-conversion fp32 max could round up past a + # stored logit's converted value's block max). + f32_t = cutlass.Float32(stored_t) + bmax_v = cutlass.Float32(_META_NEG_FLT_MAX) + if meta_valid: + bmax_v = f32_t + r_bmax = cute.arch.warp_redux_sync(bmax_v, "fmax") + # Warp-autonomous store: record index = + # tile*4 + warp; the GVR consumer folds the 4 + # warp-partials per block (fold of partials == + # block stat — associative + identity-padded). + if meta_lane == cutlass.Int32(0): + out_row_m = q_idx * next_n + t + rec_m = meta_kv_tile * cutlass.Int32(4) + meta_warp + mBlockMax[(out_row_m, rec_m)] = r_bmax + if cutlass.const_expr(self.emit_hit_stats): + # Lane-local accumulation, fully BRANCHLESS + # (data-dependent `if meta_hit` compiled to + # real divergent branches whose condition + # waits on the bitmap LDG — 43% of all warp + # stall samples). Bit-mask select is also + # NaN-safe for OOB-tile garbage logits. No + # valid-mask needed: the bitmap contract + # only sets bits inside [0, ctx). + meta_hit = ( + hit_word >> (kv_pos & cutlass.Int32(31)) + ) & cutlass.Int32(1) + msk = cutlass.Int32(0) - meta_hit # 0 / ~0 + inv = cutlass.Int32(-1) - msk + fbits = cutlass.Int32( + llvm.bitcast(cutlass.Int32.mlir_type, f32_t.ir_value()) + ) + # bits(+FLT_MAX)=0x7F7FFFFF, + # bits(-FLT_MAX)=0xFF7FFFFF (as i32: neg). + selmin = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + ( + (fbits & msk) | (cutlass.Int32(0x7F7FFFFF) & inv) + ).ir_value(), + ) + ) + selmax = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + ( + (fbits & msk) | (cutlass.Int32(-8388609) & inv) + ).ir_value(), + ) + ) + seladd = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, (fbits & msk).ir_value() + ) + ) + hacc_min[t] = cutlass.min(hacc_min[t], selmin) + hacc_max[t] = cutlass.max(hacc_max[t], selmax) + hacc_sum[t] = hacc_sum[t] + seladd + hacc_cnt[t] = hacc_cnt[t] + meta_hit if cutlass.const_expr(self.use_batched_store): # Batched STG: all result_arr[t] → mLogits in one pass. @@ -2331,6 +2778,13 @@ def kernel( # Update while-loop condition has_work = (next_q_idx != end_q_idx) | (next_kv_idx != end_kv_idx) + # Flush the final request's hit accumulators (WG 1). + if cutlass.const_expr(self.emit_block_meta and self.emit_hit_stats): + if q_idx < batch_size: + self._flush_hit_agg( + mHitAgg, q_idx, hacc_min, hacc_max, hacc_sum, hacc_cnt, meta_lane + ) + # Release last Q stage (WG 1) if q_idx < batch_size: q_pipeline.consumer_release(q_cons_state) diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py index 8dae8201ba89..40df2d060159 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py @@ -547,6 +547,218 @@ def test_cute_dsl_fp4_paged_mqa_logits( ) +# --------------------------------------------------------------------------- +# Block-meta emission (emit_block_meta — fused-GVR support). +# --------------------------------------------------------------------------- + +_FLT_MAX_F32 = torch.finfo(torch.float32).max + + +def _enc_ordered_f32(t: torch.Tensor) -> torch.Tensor: + """Order-preserving int encoding of fp32 (involution; also decodes).""" + bits = t.float().contiguous().view(torch.int32) + enc = torch.where(bits >= 0, bits, bits ^ 0x7FFFFFFF) + return enc.view(torch.float32) + + +def _hit_agg_identities(num_rows: int, device) -> torch.Tensor: + ident = torch.tensor([_FLT_MAX_F32, -_FLT_MAX_F32], dtype=torch.float32, device=device) + enc = _enc_ordered_f32(ident) + out = torch.zeros((num_rows, 4), dtype=torch.float32, device=device) + out[:, 0] = enc[0] + out[:, 1] = enc[1] + return out.contiguous() + + +def _pack_hit_bitmap( + pre_idx: torch.Tensor, batch_size: int, num_words: int, device +) -> torch.Tensor: + """[B, num_words] int32; bit (pos % 32) of word (pos // 32) set per + valid pre_idx entry — the kernel's hit test layout.""" + bitmap = torch.zeros((batch_size, num_words), dtype=torch.int64, device=device) + for b in range(batch_size): + idx = pre_idx[b].to(torch.int64).unique() + idx = idx[(idx >= 0) & (idx < num_words * 32)] + bitmap[b].scatter_add_(0, idx >> 5, torch.ones_like(idx) << (idx & 31)) + # int64 -> int32 with bit-31 wraparound (torch refuses the overflow). + wrapped = bitmap & 0xFFFFFFFF + wrapped = torch.where(wrapped >= 2**31, wrapped - 2**32, wrapped) + return wrapped.to(torch.int32) + + +@skip_not_sm100 +@pytest.mark.parametrize("batch_size", [1, 4]) +@pytest.mark.parametrize("next_n", [1, 2, 3]) +# 4224 = 33 blocks of 128 -> odd num_kv exercises WG1's OOB padding tile. +@pytest.mark.parametrize("avg_ctx", [4096, 4224]) +@pytest.mark.parametrize("phys_block_kv", [64, 128]) +@pytest.mark.parametrize("fix_length", [True, False]) +@pytest.mark.parametrize("emit_hit_stats", [True, False]) +def test_cute_dsl_fp4_paged_mqa_logits_block_meta( + batch_size, + next_n, + avg_ctx, + phys_block_kv, + fix_length, + emit_hit_stats, +): + """emit_block_meta correctness: block_max / hit_stats recomputed from + the KERNEL'S OWN logits output (fp4 numerics differ from the torch + reference logits, but the meta contract is defined on what the kernel + stores). NaN-prefilled buffers prove no writes land outside + [0, num_kv (+1 when odd)) per row.""" + from tensorrt_llm._torch.custom_ops.cute_dsl_custom_ops import CuteDSLFP4PagedMQALogitsRunner + + torch.manual_seed(7) + torch.cuda.manual_seed(7) + num_heads, head_dim, top_k = 64, 128, 512 + max_model_len = max(avg_ctx * 2, 2048) + device = "cuda" + + if fix_length: + context_lens = torch.full((batch_size,), avg_ctx, dtype=torch.int32, device=device) + else: + lo = max(phys_block_kv, int(0.7 * avg_ctx)) + context_lens = torch.randint( + lo, int(1.3 * avg_ctx) + 1, (batch_size,), dtype=torch.int32, device=device + ).clamp(max=max_model_len) + + num_blocks_per_seq = ceil_div_tensor(context_lens, phys_block_kv) + num_total_blocks = int(num_blocks_per_seq.sum().item()) + batch_size * 2 + max_blocks_per_seq = int(num_blocks_per_seq.max().item()) + block_table = torch.zeros((batch_size, max_blocks_per_seq), dtype=torch.int32, device=device) + pool = torch.randperm(num_total_blocks, device=device, dtype=torch.int32) + off = 0 + for i, n_blks in enumerate(num_blocks_per_seq.tolist()): + block_table[i, :n_blks] = pool[off : off + n_blks] + off += n_blks + + q = torch.randn((batch_size, next_n, num_heads, head_dim), device=device, dtype=torch.bfloat16) + kv_cache = torch.randn( + (num_total_blocks, phys_block_kv, 1, head_dim), device=device, dtype=torch.bfloat16 + ) + weights = torch.randn((batch_size * next_n, num_heads), device=device, dtype=torch.float32) + + q_packed, sf_q_packed = per_token_cast_to_fp4( + q.view(-1, head_dim), use_ue8m0=True, gran_k=32, use_packed_ue8m0=True + ) + q_fp4 = q_packed.view(torch.uint8).view(batch_size, next_n, num_heads, head_dim // 2) + sf_q = sf_q_packed.view(torch.int32).view(batch_size, next_n, num_heads) + remove_online_sf_transpose = phys_block_kv == 128 + kv_fused, _ = kv_cache_cast_to_fp4( + kv_cache, remove_online_sf_transpose=remove_online_sf_transpose + ) + + DG_METADATA_BLOCK_KV = 64 + num_sms = deep_gemm.get_num_sms() + schedule_meta = deep_gemm.get_paged_mqa_logits_metadata( + context_lens.unsqueeze(-1), DG_METADATA_BLOCK_KV, num_sms + ) + + # pre_idx per request within [0, ctx) -> packed bitmap. + aligned_max_ctx = align(max_model_len, 256) + nb_pad = aligned_max_ctx // 128 + pre_idx = torch.zeros((batch_size, top_k), dtype=torch.int32, device=device) + for b in range(batch_size): + pre_idx[b] = torch.randint( + 0, int(context_lens[b].item()), (top_k,), dtype=torch.int32, device=device + ) + bitmap = _pack_hit_bitmap(pre_idx, batch_size, nb_pad * 4, device) + + # block_max: 4 warp-partial records per block; consumers fold. NaN + # prefill proves write coverage is exactly [0, written_hi*4) per row. + # hit_stats: per-row aggregate the kernel atomically merges into — + # MUST be identity-initialized by the caller. + nan = float("nan") + block_max = torch.full( + (batch_size * next_n, nb_pad * 4), nan, dtype=torch.float32, device=device + ) + hit_stats = _hit_agg_identities(batch_size * next_n, device) + + meta_kwargs = dict( + emit_block_meta=True, + emit_hit_stats=emit_hit_stats, + block_max_out=block_max, + ) + if emit_hit_stats: + meta_kwargs.update(hit_bitmap=bitmap, hit_stats_out=hit_stats) + logits, bm, hs = CuteDSLFP4PagedMQALogitsRunner.forward( + q_fp4, + sf_q, + kv_fused, + weights, + context_lens, + block_table, + schedule_meta, + max_model_len, + num_epi_subtiles=1, + epi_dtype=torch.float32, + output_dtype=torch.bfloat16, + remove_online_sf_transpose=remove_online_sf_transpose, + **meta_kwargs, + ) + torch.cuda.synchronize() + + lf = logits.float() + for row in range(batch_size * next_n): + req = row // next_n + ctx = int(context_lens[req].item()) + num_kv = ceil_div(ctx, 128) + tag = f"row={row} req={req} ctx={ctx} next_n={next_n} pbk={phys_block_kv}" + + # Fold the kernel's 4 warp-partials per block (the consumer-side + # contract); per-warp partials themselves depend on the TMEM + # lane->row mapping and are not checked individually. + bm_fold = bm[row].view(nb_pad, 4).amax(-1) + + # block_max reference from the kernel's own stored logits. + padded = torch.full((nb_pad * 128,), -_FLT_MAX_F32, device=device) + padded[:ctx] = lf[row, :ctx] + ref_bmax = padded.view(nb_pad, 128).amax(-1) + torch.testing.assert_close( + bm_fold[:num_kv], + ref_bmax[:num_kv], + atol=0.0, + rtol=0.0, + msg=lambda m, tag=tag: f"block_max mismatch: {tag}\n{m}", + ) + + if emit_hit_stats: + # Per-row hit aggregate reference (bitmap semantics: dedup + + # pos < ctx). min/max slots are encoded (involution decodes). + idx = pre_idx[req].to(torch.int64).unique() + idx = idx[(idx >= 0) & (idx < ctx)] + got_min = _enc_ordered_f32(hs[row, 0:1])[0] + got_max = _enc_ordered_f32(hs[row, 1:2])[0] + got_sum = hs[row, 2] + got_cnt = hs[row, 3] + if idx.numel() > 0: + vals = lf[row, idx] + assert got_min.item() == vals.min().item(), f"hit_min: {tag}" + assert got_max.item() == vals.max().item(), f"hit_max: {tag}" + # Atomic-add merge order vs torch sum order: fp slack. + torch.testing.assert_close( + got_sum, + vals.sum(), + atol=1e-2, + rtol=1e-4, + msg=lambda m, tag=tag: f"hit_sum mismatch: {tag}\n{m}", + ) + assert got_cnt.item() == float(idx.numel()), f"hit_cnt: {tag}" + else: + assert got_min.item() == _FLT_MAX_F32, f"identity min: {tag}" + assert got_max.item() == -_FLT_MAX_F32, f"identity max: {tag}" + assert got_cnt.item() == 0.0, f"identity cnt: {tag}" + + # Odd num_kv: WG1's OOB tile writes pure identities into block + # slot num_kv (every lane invalid). + written_hi = num_kv + (num_kv % 2) + if written_hi > num_kv: + assert bm_fold[num_kv].item() == -_FLT_MAX_F32, tag + # No stray writes past the padding tile: NaN prefill intact. + assert bm[row, written_hi * 4 :].isnan().all(), f"stray block_max write: {tag}" + + # --------------------------------------------------------------------------- # Benchmarking entry point (run module directly). # --------------------------------------------------------------------------- From b5421cc8bf8c3c7b573a97814748536a7dff851d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:19:17 -0700 Subject: [PATCH 039/117] [None][perf] FP4 indexer: emit per-row seed counts in the GEMM epilogue (L1) Lane-local count(logit >= thr_j) accumulation (3 thresholds/row, fp32 post-conversion domain, block_max valid mask), flushed at q-transition and tile-loop tail via warp-redux + lane0 red.global.add.s32. Caller zero-initializes the counts buffer each step. Plumbed through the host runner (emit_seed_counts compile key, fake tensors, contract asserts) and covered by an exactness unit test against the kernel's own logits. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 60 +++++++- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 103 +++++++++++++ .../test_cute_dsl_fp4_paged_mqa_logits.py | 141 ++++++++++++++++++ 3 files changed, 298 insertions(+), 6 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index d387d9f10ba2..a419613fbf76 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -8252,11 +8252,13 @@ def _compile(cls, output_dtype, remove_online_sf_transpose=False, emit_block_meta=False, - emit_hit_stats=True): + emit_hit_stats=True, + emit_seed_counts=False): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, - remove_online_sf_transpose, emit_block_meta, emit_hit_stats) + remove_online_sf_transpose, emit_block_meta, emit_hit_stats, + emit_seed_counts) if key in cls.kernel_cache: return @@ -8338,6 +8340,17 @@ def _compile(cls, cutlass.Int32, (sym_B, cute.sym_int()), stride_order=(1, 0), assumed_align=16) + seed_thr_fake = None + seed_counts_fake = None + if emit_seed_counts: + seed_thr_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), 3), + stride_order=(1, 0), + assumed_align=4) + seed_counts_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), 3), + stride_order=(1, 0), + assumed_align=4) fake_stream = cute.runtime.make_fake_stream( use_tvm_ffi_env_stream=True) @@ -8355,6 +8368,7 @@ def _compile(cls, remove_online_sf_transpose=remove_online_sf_transpose, emit_block_meta=emit_block_meta, emit_hit_stats=emit_hit_stats, + emit_seed_counts=emit_seed_counts, ) compiled = cute.compile( @@ -8373,6 +8387,8 @@ def _compile(cls, hit_stats_fake, hit_bitmap_fake, fake_stream, + seed_thr=seed_thr_fake, + seed_counts=seed_counts_fake, options="--enable-tvm-ffi", ) cls.kernel_cache[key] = compiled @@ -8398,6 +8414,9 @@ def forward( hit_bitmap: Optional[torch.Tensor] = None, block_max_out: Optional[torch.Tensor] = None, hit_stats_out: Optional[torch.Tensor] = None, + emit_seed_counts: bool = False, + seed_thr: Optional[torch.Tensor] = None, + seed_counts_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Execute FP4 paged MQA logits kernel. @@ -8510,10 +8529,37 @@ def forward( assert (block_max_out.shape == (B * next_n, nrec) and block_max_out.is_contiguous()) + if emit_seed_counts: + assert emit_block_meta, ( + "emit_seed_counts requires emit_block_meta") + # Per-row seed-count emission: 3 thresholds per row (fp32, + # post-conversion value domain), counts accumulated with + # red.global.add.s32 — the caller must zero seed_counts_out + # each step. + assert ( + seed_thr is not None and seed_thr.dtype == torch.float32 + and seed_thr.is_cuda and seed_thr.is_contiguous() + and seed_thr.shape == (B * next_n, 3) + ), (f"emit_seed_counts requires seed_thr fp32 " + f"[{B * next_n}, 3]; got " + f"{None if seed_thr is None else (seed_thr.dtype, tuple(seed_thr.shape))}" + ) + assert (seed_counts_out is not None + and seed_counts_out.dtype == torch.int32 + and seed_counts_out.is_cuda + and seed_counts_out.is_contiguous() + and seed_counts_out.shape == (B * next_n, 3)), ( + "emit_seed_counts requires a caller-zeroed " + "seed_counts_out int32 [B*next_n, 3]") + else: + seed_thr = None + seed_counts_out = None + # Compile if needed (fake tensors, no real data required) key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, - remove_online_sf_transpose, emit_block_meta, emit_hit_stats) + remove_online_sf_transpose, emit_block_meta, emit_hit_stats, + emit_seed_counts) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8527,18 +8573,20 @@ def forward( output_dtype, remove_online_sf_transpose=remove_online_sf_transpose, emit_block_meta=emit_block_meta, - emit_hit_stats=emit_hit_stats) + emit_hit_stats=emit_hit_stats, + emit_seed_counts=emit_seed_counts) compiled = cls.kernel_cache[key] # TVM FFI: pass raw tensors, no dlpack/stream needed if emit_block_meta: compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, - block_max_out, hit_stats_out, hit_bitmap) + block_max_out, hit_stats_out, hit_bitmap, seed_thr, + seed_counts_out) return logits, block_max_out, hit_stats_out compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, None, - None, None) + None, None, None, None) return logits @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index dfbdc00dbed5..a9486671e478 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -124,6 +124,18 @@ def _red_global_fmax_ordered(addr_i64, fval, *, loc=None, ip=None): @dsl_user_op +def _red_global_add_s32(addr_i64, ival, *, loc=None, ip=None): + llvm.inline_asm( + None, + [addr_i64.ir_value(loc=loc, ip=ip), ival.ir_value(loc=loc, ip=ip)], + "red.global.add.s32 [$0], $1;", + "l,r", + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + ) + + def _red_global_add_f32(addr_i64, fval, *, loc=None, ip=None): llvm.inline_asm( None, @@ -438,6 +450,7 @@ def __init__( use_batched_store: bool = True, emit_block_meta: bool = False, emit_hit_stats: bool = True, + emit_seed_counts: bool = False, ): # Static FP4 invariants — see plan Sanity checklist. assert num_heads == 64, "FP4 kernel hardcodes num_heads=64 for TMEM/SMEM budget" @@ -514,6 +527,18 @@ def __init__( # sufficient for GVR block-skip without the fused Phase 1. self.emit_block_meta = emit_block_meta self.emit_hit_stats = emit_hit_stats + # emit_seed_counts (L1 of the epilogue suite; requires + # emit_block_meta): per row, count stored logits >= each of the + # T=3 caller-provided thresholds (cross-step seed thresholds from + # the GVR xstate). Lane-local accumulation across tiles + one + # warp-redux + lane-0 red.global.add.s32 per threshold per row + # transition — the hit-stats cost profile. Counts are computed on + # the POST-conversION value over valid positions only, matching + # the top-k consumer's verification semantics + # (workspace/epilogue_topk_interface.md). + if emit_seed_counts and not emit_block_meta: + raise ValueError("emit_seed_counts requires emit_block_meta") + self.emit_seed_counts = emit_seed_counts # epi_bytes covers fp16 and bf16 (FP8 only handled fp16). self.epi_bytes = 2 if epi_dtype in (cutlass.Float16, cutlass.BFloat16) else 4 # sW stage stride padded to 128-byte SMEM alignment for TMA bulk copy. @@ -738,6 +763,8 @@ def __call__( hit_stats: cute.Tensor, # [num_rows, 4] fp32 hit aggregate (or None unless emit_hit_stats) hit_bitmap: cute.Tensor, # [batch, nb_pad*4] int32 (or None unless emit_block_meta) stream: cuda.CUstream, + seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (emit_seed_counts) + seed_counts: cute.Tensor = None, # [num_rows, 3] int32 out, caller-zeroed ): # Derive KV data and SF views from the fused uint8 buffer. # Fused layout per phys block: [data half_head_dim*phys_block_kv bytes] @@ -939,6 +966,8 @@ class SharedStorage: block_max, hit_stats, hit_bitmap, + seed_thr, + seed_counts, self.cluster_layout_vmnk, self.a_smem_layout_staged, self.b_smem_layout_staged, @@ -999,6 +1028,24 @@ def _flush_hit_agg( hacc_sum[t] = cutlass.Float32(0.0) hacc_cnt[t] = cutlass.Int32(0) + @cute.jit + def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane): + """Warp-redux the lane-local seed counters and fire one lane-0 + red.global.add.s32 per (t, threshold). Caller zero-initializes + mSeedCounts each step; cross-CTA totals accumulate atomically.""" + next_n = cutlass.const_expr(self.next_n) + base_addr = mSeedCounts.iterator.toint() + for t in cutlass.range_constexpr(next_n): + for j in cutlass.range_constexpr(3): + w_cnt = cute.arch.warp_redux_sync(scnt[t * 3 + j], "add") + if meta_lane == cutlass.Int32(0): + row = q_idx * cutlass.Int32(next_n) + cutlass.Int32(t) + addr = base_addr + ( + cutlass.Int64(row) * cutlass.Int64(3) + cutlass.Int64(j) + ) * cutlass.Int64(4) + _red_global_add_s32(addr, w_cnt) + scnt[t * 3 + j] = cutlass.Int32(0) + @cute.kernel def kernel( self, @@ -1021,6 +1068,8 @@ def kernel( mBlockMax: cute.Tensor, # [num_rows, nb_pad*4] fp32 warp-partials (or None) mHitAgg: cute.Tensor, # [num_rows, 4] fp32 {enc_min, enc_max, sum, cnt} (or None) mHitBitmap: cute.Tensor, # [batch, nb_pad*4] int32 (or None) + mSeedThr: cute.Tensor, # [num_rows, 3] fp32 seed thresholds (or None) + mSeedCounts: cute.Tensor, # [num_rows, 3] int32 counts out (or None) cluster_layout_vmnk: cute.Layout, a_smem_layout_staged: cute.ComposedLayout, b_smem_layout_staged: cute.ComposedLayout, @@ -2005,6 +2054,9 @@ def kernel( # Hit accumulators + bitmap word add ~6 more live # registers across the tile loop. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 + if cutlass.const_expr(self.emit_seed_counts): + # 3 thresholds + 3 counters per t. + MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -2019,6 +2071,12 @@ def kernel( ctx_cur = cutlass.Int32(0) meta_warp = local_tidx // 32 meta_lane = local_tidx % 32 + if cutlass.const_expr(self.emit_seed_counts): + sthr = cute.make_fragment(next_n * 3, cutlass.Float32) + scnt = cute.make_fragment(next_n * 3, cutlass.Int32) + for _i in cutlass.range_constexpr(next_n * 3): + sthr[_i] = cutlass.Float32(_META_FLT_MAX) + scnt[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_hit_stats): # Per-lane hit accumulators, carried across ALL # tiles of the same q and flushed once per @@ -2083,6 +2141,12 @@ def kernel( # Compressed-space context len; the meta valid # mask (kv_pos < ctx_cur) keeps GEMM garbage in # the aligned padding region out of block_max. + if cutlass.const_expr(self.emit_seed_counts): + if q_idx_old < batch_size: + self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + for _t in cutlass.range_constexpr(next_n): + for _j in cutlass.range_constexpr(3): + sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] ctx_cur = mContextLens[q_idx] # Process KV block for group 0 (kv_idx + 0) @@ -2312,6 +2376,15 @@ def kernel( out_row_m = q_idx * next_n + t rec_m = meta_kv_tile * cutlass.Int32(4) + meta_warp mBlockMax[(out_row_m, rec_m)] = r_bmax + if cutlass.const_expr(self.emit_seed_counts): + # Seed-count accumulation: branchless 0/1 + # adds on the post-conversion value; the + # valid mask keeps aligned-padding garbage + # out (same contract as block_max). + valid_i1 = cutlass.Int32(meta_valid) + for _j in cutlass.range_constexpr(3): + ge_j = cutlass.Int32(f32_t >= sthr[t * 3 + _j]) + scnt[t * 3 + _j] = scnt[t * 3 + _j] + (ge_j & valid_i1) if cutlass.const_expr(self.emit_hit_stats): # Lane-local accumulation, fully BRANCHLESS # (data-dependent `if meta_hit` compiled to @@ -2381,6 +2454,9 @@ def kernel( self._flush_hit_agg( mHitAgg, q_idx, hacc_min, hacc_max, hacc_sum, hacc_cnt, meta_lane ) + if cutlass.const_expr(self.emit_seed_counts): + if q_idx < batch_size: + self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane) # Release last Q stage (WG 0) if q_idx < batch_size: @@ -2414,6 +2490,9 @@ def kernel( # Hit accumulators + bitmap word add ~6 more live # registers across the tile loop. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 + if cutlass.const_expr(self.emit_seed_counts): + # 3 thresholds + 3 counters per t. + MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -2428,6 +2507,12 @@ def kernel( ctx_cur = cutlass.Int32(0) meta_warp = local_tidx // 32 meta_lane = local_tidx % 32 + if cutlass.const_expr(self.emit_seed_counts): + sthr = cute.make_fragment(next_n * 3, cutlass.Float32) + scnt = cute.make_fragment(next_n * 3, cutlass.Int32) + for _i in cutlass.range_constexpr(next_n * 3): + sthr[_i] = cutlass.Float32(_META_FLT_MAX) + scnt[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_hit_stats): # Per-lane hit accumulators, carried across ALL # tiles of the same q and flushed once per @@ -2492,6 +2577,12 @@ def kernel( # Compressed-space context len; the meta valid # mask (kv_pos < ctx_cur) keeps GEMM garbage in # the aligned padding region out of block_max. + if cutlass.const_expr(self.emit_seed_counts): + if q_idx_old < batch_size: + self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + for _t in cutlass.range_constexpr(next_n): + for _j in cutlass.range_constexpr(3): + sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] ctx_cur = mContextLens[q_idx] # Process KV block for group 1 (kv_idx + 1) @@ -2715,6 +2806,15 @@ def kernel( out_row_m = q_idx * next_n + t rec_m = meta_kv_tile * cutlass.Int32(4) + meta_warp mBlockMax[(out_row_m, rec_m)] = r_bmax + if cutlass.const_expr(self.emit_seed_counts): + # Seed-count accumulation: branchless 0/1 + # adds on the post-conversion value; the + # valid mask keeps aligned-padding garbage + # out (same contract as block_max). + valid_i1 = cutlass.Int32(meta_valid) + for _j in cutlass.range_constexpr(3): + ge_j = cutlass.Int32(f32_t >= sthr[t * 3 + _j]) + scnt[t * 3 + _j] = scnt[t * 3 + _j] + (ge_j & valid_i1) if cutlass.const_expr(self.emit_hit_stats): # Lane-local accumulation, fully BRANCHLESS # (data-dependent `if meta_hit` compiled to @@ -2784,6 +2884,9 @@ def kernel( self._flush_hit_agg( mHitAgg, q_idx, hacc_min, hacc_max, hacc_sum, hacc_cnt, meta_lane ) + if cutlass.const_expr(self.emit_seed_counts): + if q_idx < batch_size: + self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane) # Release last Q stage (WG 1) if q_idx < batch_size: diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py index 40df2d060159..49a19e8d38b4 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py @@ -759,6 +759,147 @@ def test_cute_dsl_fp4_paged_mqa_logits_block_meta( assert bm[row, written_hi * 4 :].isnan().all(), f"stray block_max write: {tag}" +@skip_not_sm100 +@pytest.mark.parametrize("batch_size", [1, 4]) +@pytest.mark.parametrize("next_n", [1, 2, 3]) +# 4224 = 33 blocks of 128 -> odd num_kv exercises WG1's OOB padding tile. +@pytest.mark.parametrize("avg_ctx", [4096, 4224]) +@pytest.mark.parametrize("phys_block_kv", [64, 128]) +@pytest.mark.parametrize("fix_length", [True, False]) +def test_cute_dsl_fp4_paged_mqa_logits_seed_counts( + batch_size, + next_n, + avg_ctx, + phys_block_kv, + fix_length, +): + """emit_seed_counts exactness: per-row counts of logits >= threshold + recomputed from the KERNEL'S OWN logits output (the count contract is + defined on post-conversion values, same as block_max). Thresholds are + per-row quantiles of the row's own logits so each of the 3 counters + lands in a different regime (loose/mid/tight).""" + from tensorrt_llm._torch.custom_ops.cute_dsl_custom_ops import CuteDSLFP4PagedMQALogitsRunner + + torch.manual_seed(11) + torch.cuda.manual_seed(11) + num_heads, head_dim = 64, 128 + max_model_len = max(avg_ctx * 2, 2048) + device = "cuda" + + if fix_length: + context_lens = torch.full((batch_size,), avg_ctx, dtype=torch.int32, device=device) + else: + lo = max(phys_block_kv, int(0.7 * avg_ctx)) + context_lens = torch.randint( + lo, int(1.3 * avg_ctx) + 1, (batch_size,), dtype=torch.int32, device=device + ).clamp(max=max_model_len) + + num_blocks_per_seq = ceil_div_tensor(context_lens, phys_block_kv) + num_total_blocks = int(num_blocks_per_seq.sum().item()) + batch_size * 2 + max_blocks_per_seq = int(num_blocks_per_seq.max().item()) + block_table = torch.zeros((batch_size, max_blocks_per_seq), dtype=torch.int32, device=device) + pool = torch.randperm(num_total_blocks, device=device, dtype=torch.int32) + off = 0 + for i, n_blks in enumerate(num_blocks_per_seq.tolist()): + block_table[i, :n_blks] = pool[off : off + n_blks] + off += n_blks + + q = torch.randn((batch_size, next_n, num_heads, head_dim), device=device, dtype=torch.bfloat16) + kv_cache = torch.randn( + (num_total_blocks, phys_block_kv, 1, head_dim), device=device, dtype=torch.bfloat16 + ) + weights = torch.randn((batch_size * next_n, num_heads), device=device, dtype=torch.float32) + + q_packed, sf_q_packed = per_token_cast_to_fp4( + q.view(-1, head_dim), use_ue8m0=True, gran_k=32, use_packed_ue8m0=True + ) + q_fp4 = q_packed.view(torch.uint8).view(batch_size, next_n, num_heads, head_dim // 2) + sf_q = sf_q_packed.view(torch.int32).view(batch_size, next_n, num_heads) + remove_online_sf_transpose = phys_block_kv == 128 + kv_fused, _ = kv_cache_cast_to_fp4( + kv_cache, remove_online_sf_transpose=remove_online_sf_transpose + ) + + DG_METADATA_BLOCK_KV = 64 + num_sms = deep_gemm.get_num_sms() + schedule_meta = deep_gemm.get_paged_mqa_logits_metadata( + context_lens.unsqueeze(-1), DG_METADATA_BLOCK_KV, num_sms + ) + + aligned_max_ctx = align(max_model_len, 256) + nb_pad = aligned_max_ctx // 128 + num_rows = batch_size * next_n + + # First pass without seed counts to harvest per-row logits for + # threshold picking (post-conversion value domain). + nan = float("nan") + block_max = torch.full((num_rows, nb_pad * 4), nan, dtype=torch.float32, device=device) + common = dict( + num_epi_subtiles=1, + epi_dtype=torch.float32, + output_dtype=torch.bfloat16, + remove_online_sf_transpose=remove_online_sf_transpose, + ) + logits0, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( + q_fp4, + sf_q, + kv_fused, + weights, + context_lens, + block_table, + schedule_meta, + max_model_len, + emit_block_meta=True, + emit_hit_stats=False, + block_max_out=block_max, + **common, + ) + torch.cuda.synchronize() + lf0 = logits0.float() + + seed_thr = torch.empty((num_rows, 3), dtype=torch.float32, device=device) + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + vals = lf0[row, :ctx] + # Loose / mid / tight rungs; ties on exact stored values are the + # point (>= must count them all). + seed_thr[row, 0] = torch.quantile(vals, 0.10) + seed_thr[row, 1] = torch.quantile(vals, 0.90) + seed_thr[row, 2] = torch.quantile(vals, 0.998) + + seed_counts = torch.zeros((num_rows, 3), dtype=torch.int32, device=device) + block_max.fill_(nan) + logits, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( + q_fp4, + sf_q, + kv_fused, + weights, + context_lens, + block_table, + schedule_meta, + max_model_len, + emit_block_meta=True, + emit_hit_stats=False, + block_max_out=block_max, + emit_seed_counts=True, + seed_thr=seed_thr, + seed_counts_out=seed_counts, + **common, + ) + torch.cuda.synchronize() + + lf = logits.float() + torch.testing.assert_close(lf, lf0, atol=0.0, rtol=0.0) + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + tag = f"row={row} ctx={ctx} next_n={next_n} pbk={phys_block_kv}" + ref = (lf[row, :ctx].unsqueeze(0) >= seed_thr[row].unsqueeze(1)).sum(-1) + got = seed_counts[row].to(torch.int64) + assert torch.equal(got.cpu(), ref.cpu().to(torch.int64)), ( + f"seed_counts mismatch: {tag} got={got.tolist()} ref={ref.tolist()}" + ) + + # --------------------------------------------------------------------------- # Benchmarking entry point (run module directly). # --------------------------------------------------------------------------- From 57f154062bb2dca63b8a387ee6bf67ed5445af28 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:58:01 -0700 Subject: [PATCH 040/117] [None][perf] FP4 indexer: drop seed-count register budget cut ncu (B=128 ctx=4096, SpeedOfLight/Occupancy/SchedulerStats) shows the seed-count cost is epilogue ALU/issue (Compute 20->22.7%), not register pressure: block limits and achieved occupancy are identical with or without a -8 weight-cache cut, and the cut measured neutral-to-slower. Kernel-level marginals: block_max +3.7%, +seed counts +3.2%, +hit stats +10%. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index a9486671e478..0f4880a756f8 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -2054,9 +2054,10 @@ def kernel( # Hit accumulators + bitmap word add ~6 more live # registers across the tile loop. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 - if cutlass.const_expr(self.emit_seed_counts): - # 3 thresholds + 3 counters per t. - MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 + # emit_seed_counts: no budget cut — ncu shows the cost + # is epilogue ALU/issue, not registers (occupancy and + # block limits identical with or without a -8 cut; the + # cut itself measured neutral-to-slower). NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -2490,9 +2491,10 @@ def kernel( # Hit accumulators + bitmap word add ~6 more live # registers across the tile loop. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 - if cutlass.const_expr(self.emit_seed_counts): - # 3 thresholds + 3 counters per t. - MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 + # emit_seed_counts: no budget cut — ncu shows the cost + # is epilogue ALU/issue, not registers (occupancy and + # block limits identical with or without a -8 cut; the + # cut itself measured neutral-to-slower). NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the From b7f4de364d4ec8144ff223f6087af29645f54a21 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 20 Jul 2026 09:40:36 -0700 Subject: [PATCH 041/117] [None][perf] FP4 indexer: L2 candidate pre-collect in the GEMM epilogue Unordered (value, index) pair collection of all positions >= the t_0 seed threshold, off by default (emit_cand). Per-(warp, t) claim windows: a refill claims (hits + 8) slots with one returning global atomic; window consumption is latency-free (a per-hit atomic round-trip measured 2x kernel time at 131k ctx). Window tails are sentinel-filled (index = -1) at q-transition/loop end, so claimed over-approximates the true count; counts[r][0] stays exact and claimed >= K certifies candidate coverage of the true top-K. No-hit iterations exit on one compare against the already-computed r_bmax 32-position bound. Exactness: 32/32 unit combos (exact live set + sentinel accounting when it fits; overflow -> void with valid unique entries). Cost (ncu, kernel-level, realistic 0.8% density at 131k ctx): +27% marginal over P0+L1 -- consumers enable it only where the top-k saving exceeds the GEMM cost (decision waterfall). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 59 +++- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 281 +++++++++++++++++- .../test_cute_dsl_fp4_paged_mqa_logits.py | 165 ++++++++++ 3 files changed, 498 insertions(+), 7 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index a419613fbf76..0a01f7b71e93 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -8253,12 +8253,14 @@ def _compile(cls, remove_online_sf_transpose=False, emit_block_meta=False, emit_hit_stats=True, - emit_seed_counts=False): + emit_seed_counts=False, + emit_cand=False, + cand_cap=5120): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, - emit_seed_counts) + emit_seed_counts, emit_cand, cand_cap) if key in cls.kernel_cache: return @@ -8340,6 +8342,17 @@ def _compile(cls, cutlass.Int32, (sym_B, cute.sym_int()), stride_order=(1, 0), assumed_align=16) + cand_fake = None + cand_ctl_fake = None + if emit_cand: + cand_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), cand_cap * 2), + stride_order=(1, 0), + assumed_align=8) + cand_ctl_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), 2), + stride_order=(1, 0), + assumed_align=8) seed_thr_fake = None seed_counts_fake = None if emit_seed_counts: @@ -8369,6 +8382,8 @@ def _compile(cls, emit_block_meta=emit_block_meta, emit_hit_stats=emit_hit_stats, emit_seed_counts=emit_seed_counts, + emit_cand=emit_cand, + cand_cap=cand_cap, ) compiled = cute.compile( @@ -8389,6 +8404,8 @@ def _compile(cls, fake_stream, seed_thr=seed_thr_fake, seed_counts=seed_counts_fake, + cand=cand_fake, + cand_ctl=cand_ctl_fake, options="--enable-tvm-ffi", ) cls.kernel_cache[key] = compiled @@ -8417,6 +8434,9 @@ def forward( emit_seed_counts: bool = False, seed_thr: Optional[torch.Tensor] = None, seed_counts_out: Optional[torch.Tensor] = None, + emit_cand: bool = False, + cand_out: Optional[torch.Tensor] = None, + cand_ctl_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Execute FP4 paged MQA logits kernel. @@ -8555,11 +8575,36 @@ def forward( seed_thr = None seed_counts_out = None + cand_cap = 0 + if emit_cand: + assert emit_seed_counts, ( + "emit_cand requires emit_seed_counts (t_0 threshold)") + # Unordered (value, index) pair scatter; the caller zeroes + # cand_ctl_out {claimed, void} each step. cand_out is + # [B*next_n, CAP*2] int32 (fp32 bits in even words). + assert ( + cand_out is not None and cand_out.dtype == torch.int32 + and cand_out.is_cuda and cand_out.is_contiguous() + and cand_out.dim() == 2 and cand_out.shape[0] == B * next_n + and cand_out.shape[1] % 2 == 0 and cand_out.shape[1] > 0), ( + "emit_cand requires cand_out int32 [B*next_n, CAP*2]") + assert (cand_ctl_out is not None + and cand_ctl_out.dtype == torch.int32 + and cand_ctl_out.is_cuda + and cand_ctl_out.is_contiguous() + and cand_ctl_out.shape == (B * next_n, 2)), ( + "emit_cand requires a caller-zeroed cand_ctl_out " + "int32 [B*next_n, 2]") + cand_cap = cand_out.shape[1] // 2 + else: + cand_out = None + cand_ctl_out = None + # Compile if needed (fake tensors, no real data required) key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, - emit_seed_counts) + emit_seed_counts, emit_cand, cand_cap) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8574,7 +8619,9 @@ def forward( remove_online_sf_transpose=remove_online_sf_transpose, emit_block_meta=emit_block_meta, emit_hit_stats=emit_hit_stats, - emit_seed_counts=emit_seed_counts) + emit_seed_counts=emit_seed_counts, + emit_cand=emit_cand, + cand_cap=cand_cap) compiled = cls.kernel_cache[key] # TVM FFI: pass raw tensors, no dlpack/stream needed @@ -8582,11 +8629,11 @@ def forward( compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, block_max_out, hit_stats_out, hit_bitmap, seed_thr, - seed_counts_out) + seed_counts_out, cand_out, cand_ctl_out) return logits, block_max_out, hit_stats_out compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, None, - None, None, None, None) + None, None, None, None, None, None) return logits @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index 0f4880a756f8..acb9b50e4672 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -57,7 +57,7 @@ from cutlass._mlir import ir from cutlass._mlir.dialects import llvm, vector from cutlass.cute.nvgpu import cpasync, tcgen05 -from cutlass.cutlass_dsl import dsl_user_op +from cutlass.cutlass_dsl import T, dsl_user_op from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait # CuTe DSL CUDA 13 validates rounding modes as string literals. The string @@ -123,6 +123,24 @@ def _red_global_fmax_ordered(addr_i64, fval, *, loc=None, ip=None): ) +@dsl_user_op +def _atom_global_add_s32(addr_i64, ival, *, loc=None, ip=None): + """atom.global.add.s32 returning the OLD value (warp batch-claim).""" + return cutlass.Int32( + llvm.inline_asm( + T.i32(), + [addr_i64.ir_value(loc=loc, ip=ip), ival.ir_value(loc=loc, ip=ip)], + "atom.global.add.s32 $0, [$1], $2;", + "=r,l,r", + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + loc=loc, + ip=ip, + ) + ) + + @dsl_user_op def _red_global_add_s32(addr_i64, ival, *, loc=None, ip=None): llvm.inline_asm( @@ -451,6 +469,8 @@ def __init__( emit_block_meta: bool = False, emit_hit_stats: bool = True, emit_seed_counts: bool = False, + emit_cand: bool = False, + cand_cap: int = 5120, ): # Static FP4 invariants — see plan Sanity checklist. assert num_heads == 64, "FP4 kernel hardcodes num_heads=64 for TMEM/SMEM budget" @@ -539,6 +559,19 @@ def __init__( if emit_seed_counts and not emit_block_meta: raise ValueError("emit_seed_counts requires emit_block_meta") self.emit_seed_counts = emit_seed_counts + # emit_cand (L2 of the epilogue suite): unordered pre-collect of all + # (value, index) pairs >= the t_0 seed threshold via warp ballot + + # lane0 batch atomic claim. claimed >= K certifies the candidate + # set covers the true top-K (contract in epilogue_topk_interface.md). + if emit_cand and not emit_seed_counts: + raise ValueError("emit_cand requires emit_seed_counts (t_0 source)") + self.emit_cand = emit_cand + self.cand_cap = cand_cap + # Per-warp claim window: one atomic claims (hits + CAND_WIN) slots; + # subsequent hits consume the window latency-free. The unconsumed + # tail is sentinel-filled (idx = -1) at q-transition/loop end, so + # `claimed` over-approximates the true count (counts[r][0] exact). + self.CAND_WIN = 8 # epi_bytes covers fp16 and bf16 (FP8 only handled fp16). self.epi_bytes = 2 if epi_dtype in (cutlass.Float16, cutlass.BFloat16) else 4 # sW stage stride padded to 128-byte SMEM alignment for TMA bulk copy. @@ -765,6 +798,8 @@ def __call__( stream: cuda.CUstream, seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (emit_seed_counts) seed_counts: cute.Tensor = None, # [num_rows, 3] int32 out, caller-zeroed + cand: cute.Tensor = None, # [num_rows, CAP*2] int32 {val bits, idx} pairs + cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 {claimed, void}, zeroed ): # Derive KV data and SF views from the fused uint8 buffer. # Fused layout per phys block: [data half_head_dim*phys_block_kv bytes] @@ -968,6 +1003,8 @@ class SharedStorage: hit_bitmap, seed_thr, seed_counts, + cand, + cand_ctl, self.cluster_layout_vmnk, self.a_smem_layout_staged, self.b_smem_layout_staged, @@ -1046,6 +1083,32 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane): _red_global_add_s32(addr, w_cnt) scnt[t * 3 + j] = cutlass.Int32(0) + @cute.jit + def _flush_cand_window(self, mCand, q_idx, cwbase, cwleft, meta_lane): + """Sentinel-fill the unconsumed tail of each per-(warp, t) claim + window (idx word = -1; consumers skip sentinels) and invalidate the + window. wleft <= CAND_WIN + 31 always fits one lane round.""" + next_n = cutlass.const_expr(self.next_n) + CAP_C = cutlass.const_expr(self.cand_cap) + cand_base = mCand.iterator.toint() + for t in cutlass.range_constexpr(next_n): + if cwleft[t] > cutlass.Int32(0): + row_c = q_idx * cutlass.Int32(next_n) + cutlass.Int32(t) + sl_f = cwbase[t] + meta_lane + if meta_lane < cwleft[t] and sl_f < cutlass.Int32(CAP_C): + pair_f = cand_base + ( + cutlass.Int64(row_c) * cutlass.Int64(CAP_C) + cutlass.Int64(sl_f) + ) * cutlass.Int64(8) + iptr_f = cute.make_ptr( + cutlass.Int32, + pair_f + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(iptr_f, cute.make_layout((1,)))[0] = cutlass.Int32(-1) + cwbase[t] = cutlass.Int32(0) + cwleft[t] = cutlass.Int32(0) + @cute.kernel def kernel( self, @@ -1070,6 +1133,8 @@ def kernel( mHitBitmap: cute.Tensor, # [batch, nb_pad*4] int32 (or None) mSeedThr: cute.Tensor, # [num_rows, 3] fp32 seed thresholds (or None) mSeedCounts: cute.Tensor, # [num_rows, 3] int32 counts out (or None) + mCand: cute.Tensor, # [num_rows, CAP*2] int32 pair scatter (or None) + mCandCtl: cute.Tensor, # [num_rows, 2] int32 {claimed, void} (or None) cluster_layout_vmnk: cute.Layout, a_smem_layout_staged: cute.ComposedLayout, b_smem_layout_staged: cute.ComposedLayout, @@ -2078,6 +2143,12 @@ def kernel( for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) + if cutlass.const_expr(self.emit_cand): + cwbase = cute.make_fragment(next_n, cutlass.Int32) + cwleft = cute.make_fragment(next_n, cutlass.Int32) + for _i in cutlass.range_constexpr(next_n): + cwbase[_i] = cutlass.Int32(0) + cwleft[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_hit_stats): # Per-lane hit accumulators, carried across ALL # tiles of the same q and flushed once per @@ -2145,6 +2216,11 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + if cutlass.const_expr(self.emit_cand): + if q_idx_old < batch_size: + self._flush_cand_window( + mCand, q_idx_old, cwbase, cwleft, meta_lane + ) for _t in cutlass.range_constexpr(next_n): for _j in cutlass.range_constexpr(3): sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] @@ -2386,6 +2462,99 @@ def kernel( for _j in cutlass.range_constexpr(3): ge_j = cutlass.Int32(f32_t >= sthr[t * 3 + _j]) scnt[t * 3 + _j] = scnt[t * 3 + _j] + (ge_j & valid_i1) + if cutlass.const_expr(self.emit_cand): + # L2 pre-collect at t_0 with per-warp claim + # WINDOWS: refills claim (hits + CAND_WIN) + # slots in ONE atomic; between refills the + # warp consumes its window latency-free (a + # per-hit atomic round-trip stalled the + # epilogue 2x at 131k ctx). Unconsumed tail + # is sentinel-filled on flush; counts[r][0] + # stays the exact count. Gated on the + # already-computed warp-uniform 32-position + # bound: r_bmax < t_0 proves zero hits, so + # the common no-hit iteration costs ONE fp + # compare (no ballot, no select). bound >= + # t_0 guarantees a nonzero ballot (exact + # per-lane max, invalid -> -FLT_MAX). + if r_bmax >= sthr[t * 3 + 0]: + pred_c = cutlass.Int32(0) + if meta_valid: + if f32_t >= sthr[t * 3 + 0]: + pred_c = cutlass.Int32(1) + mask_c = cute.arch.vote_ballot_sync(pred_c != cutlass.Int32(0)) + row_c = q_idx * next_n + t + cnt_c = cutlass.Int32(cute.arch.popc(mask_c)) + lm_c = ( + cutlass.Uint32(1) << cutlass.Uint32(meta_lane) + ) - cutlass.Uint32(1) + off_c = cutlass.Int32(cute.arch.popc(mask_c & lm_c)) + CAP_C = cutlass.const_expr(self.cand_cap) + cand_b = mCand.iterator.toint() + if cnt_c > cwleft[t]: + # sentinel-fill the old tail, then + # refill: one atomic per window. + sl_o = cwbase[t] + meta_lane + if meta_lane < cwleft[t] and sl_o < cutlass.Int32(CAP_C): + pair_o = cand_b + ( + cutlass.Int64(row_c) * cutlass.Int64(CAP_C) + + cutlass.Int64(sl_o) + ) * cutlass.Int64(8) + iptr_o = cute.make_ptr( + cutlass.Int32, + pair_o + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(iptr_o, cute.make_layout((1,)))[0] = ( + cutlass.Int32(-1) + ) + m_c = cnt_c + cutlass.Int32(self.CAND_WIN) + ctl_addr = mCandCtl.iterator.toint() + ( + cutlass.Int64(row_c) * cutlass.Int64(8) + ) + nb_c = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0): + nb_c = _atom_global_add_s32(ctl_addr, m_c) + nb_c = cute.arch.shuffle_sync(nb_c, cutlass.Int32(0)) + cwbase[t] = nb_c + cwleft[t] = m_c + # one-shot void mark on crossing CAP + if meta_lane == cutlass.Int32(0): + if nb_c + m_c > cutlass.Int32( + CAP_C + ) and nb_c <= cutlass.Int32(CAP_C): + vdptr = cute.make_ptr( + cutlass.Int32, + ctl_addr + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vdptr, cute.make_layout((1,)))[ + 0 + ] = cutlass.Int32(1) + slot_c = cwbase[t] + off_c + if pred_c != cutlass.Int32(0) and slot_c < cutlass.Int32(CAP_C): + pair_addr = cand_b + ( + cutlass.Int64(row_c) * cutlass.Int64(CAP_C) + + cutlass.Int64(slot_c) + ) * cutlass.Int64(8) + vptr_c = cute.make_ptr( + cutlass.Float32, + pair_addr, + cute.AddressSpace.gmem, + assumed_align=8, + ) + cute.make_tensor(vptr_c, cute.make_layout((1,)))[0] = f32_t + iptr_c = cute.make_ptr( + cutlass.Int32, + pair_addr + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(iptr_c, cute.make_layout((1,)))[0] = kv_pos + cwbase[t] = cwbase[t] + cnt_c + cwleft[t] = cwleft[t] - cnt_c if cutlass.const_expr(self.emit_hit_stats): # Lane-local accumulation, fully BRANCHLESS # (data-dependent `if meta_hit` compiled to @@ -2458,6 +2627,9 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx < batch_size: self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane) + if cutlass.const_expr(self.emit_cand): + if q_idx < batch_size: + self._flush_cand_window(mCand, q_idx, cwbase, cwleft, meta_lane) # Release last Q stage (WG 0) if q_idx < batch_size: @@ -2515,6 +2687,12 @@ def kernel( for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) + if cutlass.const_expr(self.emit_cand): + cwbase = cute.make_fragment(next_n, cutlass.Int32) + cwleft = cute.make_fragment(next_n, cutlass.Int32) + for _i in cutlass.range_constexpr(next_n): + cwbase[_i] = cutlass.Int32(0) + cwleft[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_hit_stats): # Per-lane hit accumulators, carried across ALL # tiles of the same q and flushed once per @@ -2582,6 +2760,11 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + if cutlass.const_expr(self.emit_cand): + if q_idx_old < batch_size: + self._flush_cand_window( + mCand, q_idx_old, cwbase, cwleft, meta_lane + ) for _t in cutlass.range_constexpr(next_n): for _j in cutlass.range_constexpr(3): sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] @@ -2817,6 +3000,99 @@ def kernel( for _j in cutlass.range_constexpr(3): ge_j = cutlass.Int32(f32_t >= sthr[t * 3 + _j]) scnt[t * 3 + _j] = scnt[t * 3 + _j] + (ge_j & valid_i1) + if cutlass.const_expr(self.emit_cand): + # L2 pre-collect at t_0 with per-warp claim + # WINDOWS: refills claim (hits + CAND_WIN) + # slots in ONE atomic; between refills the + # warp consumes its window latency-free (a + # per-hit atomic round-trip stalled the + # epilogue 2x at 131k ctx). Unconsumed tail + # is sentinel-filled on flush; counts[r][0] + # stays the exact count. Gated on the + # already-computed warp-uniform 32-position + # bound: r_bmax < t_0 proves zero hits, so + # the common no-hit iteration costs ONE fp + # compare (no ballot, no select). bound >= + # t_0 guarantees a nonzero ballot (exact + # per-lane max, invalid -> -FLT_MAX). + if r_bmax >= sthr[t * 3 + 0]: + pred_c = cutlass.Int32(0) + if meta_valid: + if f32_t >= sthr[t * 3 + 0]: + pred_c = cutlass.Int32(1) + mask_c = cute.arch.vote_ballot_sync(pred_c != cutlass.Int32(0)) + row_c = q_idx * next_n + t + cnt_c = cutlass.Int32(cute.arch.popc(mask_c)) + lm_c = ( + cutlass.Uint32(1) << cutlass.Uint32(meta_lane) + ) - cutlass.Uint32(1) + off_c = cutlass.Int32(cute.arch.popc(mask_c & lm_c)) + CAP_C = cutlass.const_expr(self.cand_cap) + cand_b = mCand.iterator.toint() + if cnt_c > cwleft[t]: + # sentinel-fill the old tail, then + # refill: one atomic per window. + sl_o = cwbase[t] + meta_lane + if meta_lane < cwleft[t] and sl_o < cutlass.Int32(CAP_C): + pair_o = cand_b + ( + cutlass.Int64(row_c) * cutlass.Int64(CAP_C) + + cutlass.Int64(sl_o) + ) * cutlass.Int64(8) + iptr_o = cute.make_ptr( + cutlass.Int32, + pair_o + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(iptr_o, cute.make_layout((1,)))[0] = ( + cutlass.Int32(-1) + ) + m_c = cnt_c + cutlass.Int32(self.CAND_WIN) + ctl_addr = mCandCtl.iterator.toint() + ( + cutlass.Int64(row_c) * cutlass.Int64(8) + ) + nb_c = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0): + nb_c = _atom_global_add_s32(ctl_addr, m_c) + nb_c = cute.arch.shuffle_sync(nb_c, cutlass.Int32(0)) + cwbase[t] = nb_c + cwleft[t] = m_c + # one-shot void mark on crossing CAP + if meta_lane == cutlass.Int32(0): + if nb_c + m_c > cutlass.Int32( + CAP_C + ) and nb_c <= cutlass.Int32(CAP_C): + vdptr = cute.make_ptr( + cutlass.Int32, + ctl_addr + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vdptr, cute.make_layout((1,)))[ + 0 + ] = cutlass.Int32(1) + slot_c = cwbase[t] + off_c + if pred_c != cutlass.Int32(0) and slot_c < cutlass.Int32(CAP_C): + pair_addr = cand_b + ( + cutlass.Int64(row_c) * cutlass.Int64(CAP_C) + + cutlass.Int64(slot_c) + ) * cutlass.Int64(8) + vptr_c = cute.make_ptr( + cutlass.Float32, + pair_addr, + cute.AddressSpace.gmem, + assumed_align=8, + ) + cute.make_tensor(vptr_c, cute.make_layout((1,)))[0] = f32_t + iptr_c = cute.make_ptr( + cutlass.Int32, + pair_addr + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(iptr_c, cute.make_layout((1,)))[0] = kv_pos + cwbase[t] = cwbase[t] + cnt_c + cwleft[t] = cwleft[t] - cnt_c if cutlass.const_expr(self.emit_hit_stats): # Lane-local accumulation, fully BRANCHLESS # (data-dependent `if meta_hit` compiled to @@ -2889,6 +3165,9 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx < batch_size: self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane) + if cutlass.const_expr(self.emit_cand): + if q_idx < batch_size: + self._flush_cand_window(mCand, q_idx, cwbase, cwleft, meta_lane) # Release last Q stage (WG 1) if q_idx < batch_size: diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py index 49a19e8d38b4..5d4f745cf407 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py @@ -900,6 +900,171 @@ def test_cute_dsl_fp4_paged_mqa_logits_seed_counts( ) +@skip_not_sm100 +@pytest.mark.parametrize("batch_size", [1, 4]) +@pytest.mark.parametrize("next_n", [1, 3]) +@pytest.mark.parametrize("avg_ctx", [4096, 4224]) +@pytest.mark.parametrize("phys_block_kv", [64, 128]) +@pytest.mark.parametrize("cap_mode", ["roomy", "tight"]) +def test_cute_dsl_fp4_paged_mqa_logits_cand( + batch_size, + next_n, + avg_ctx, + phys_block_kv, + cap_mode, +): + """emit_cand correctness: the unordered (value, index) pre-collect at + t_0 must contain EXACTLY the set {i < ctx : logits[r, i] >= t_0} when + it fits (void == 0), and degrade safely on overflow (void == 1, all + written slots valid + unique, claimed and counts[0] still exact).""" + from tensorrt_llm._torch.custom_ops.cute_dsl_custom_ops import CuteDSLFP4PagedMQALogitsRunner + + torch.manual_seed(13) + torch.cuda.manual_seed(13) + num_heads, head_dim = 64, 128 + max_model_len = max(avg_ctx * 2, 2048) + device = "cuda" + + context_lens = torch.full((batch_size,), avg_ctx, dtype=torch.int32, device=device) + num_blocks_per_seq = ceil_div_tensor(context_lens, phys_block_kv) + num_total_blocks = int(num_blocks_per_seq.sum().item()) + batch_size * 2 + max_blocks_per_seq = int(num_blocks_per_seq.max().item()) + block_table = torch.zeros((batch_size, max_blocks_per_seq), dtype=torch.int32, device=device) + pool = torch.randperm(num_total_blocks, device=device, dtype=torch.int32) + off = 0 + for i, n_blks in enumerate(num_blocks_per_seq.tolist()): + block_table[i, :n_blks] = pool[off : off + n_blks] + off += n_blks + + q = torch.randn((batch_size, next_n, num_heads, head_dim), device=device, dtype=torch.bfloat16) + kv_cache = torch.randn( + (num_total_blocks, phys_block_kv, 1, head_dim), device=device, dtype=torch.bfloat16 + ) + weights = torch.randn((batch_size * next_n, num_heads), device=device, dtype=torch.float32) + q_packed, sf_q_packed = per_token_cast_to_fp4( + q.view(-1, head_dim), use_ue8m0=True, gran_k=32, use_packed_ue8m0=True + ) + q_fp4 = q_packed.view(torch.uint8).view(batch_size, next_n, num_heads, head_dim // 2) + sf_q = sf_q_packed.view(torch.int32).view(batch_size, next_n, num_heads) + remove_online_sf_transpose = phys_block_kv == 128 + kv_fused, _ = kv_cache_cast_to_fp4( + kv_cache, remove_online_sf_transpose=remove_online_sf_transpose + ) + DG_METADATA_BLOCK_KV = 64 + num_sms = deep_gemm.get_num_sms() + schedule_meta = deep_gemm.get_paged_mqa_logits_metadata( + context_lens.unsqueeze(-1), DG_METADATA_BLOCK_KV, num_sms + ) + aligned_max_ctx = align(max_model_len, 256) + nb_pad = aligned_max_ctx // 128 + num_rows = batch_size * next_n + nan = float("nan") + block_max = torch.full((num_rows, nb_pad * 4), nan, dtype=torch.float32, device=device) + common = dict( + num_epi_subtiles=1, + epi_dtype=torch.float32, + output_dtype=torch.bfloat16, + remove_online_sf_transpose=remove_online_sf_transpose, + ) + base_args = ( + q_fp4, + sf_q, + kv_fused, + weights, + context_lens, + block_table, + schedule_meta, + max_model_len, + ) + + # Pass 1: harvest logits for threshold picking. + logits0, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( + *base_args, + emit_block_meta=True, + emit_hit_stats=False, + block_max_out=block_max, + **common, + ) + torch.cuda.synchronize() + lf0 = logits0.float() + + seed_thr = torch.empty((num_rows, 3), dtype=torch.float32, device=device) + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + vals = lf0[row, :ctx] + seed_thr[row, 0] = torch.quantile(vals, 0.90) # t_0: ~10% of ctx + seed_thr[row, 1] = torch.quantile(vals, 0.97) + seed_thr[row, 2] = torch.quantile(vals, 0.998) + + # Window claiming over-claims by up to ~CAND_WIN per epilogue warp + # touching the row (sentinel-filled tails); B=1 rows spread over many + # CTAs, so roomy needs slack well beyond the ~410 true hits. + cap = 4096 if cap_mode == "roomy" else 128 + seed_counts = torch.zeros((num_rows, 3), dtype=torch.int32, device=device) + cand = torch.full((num_rows, cap * 2), -1, dtype=torch.int32, device=device) + ctl = torch.zeros((num_rows, 2), dtype=torch.int32, device=device) + block_max.fill_(nan) + logits, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( + *base_args, + emit_block_meta=True, + emit_hit_stats=False, + block_max_out=block_max, + emit_seed_counts=True, + seed_thr=seed_thr, + seed_counts_out=seed_counts, + emit_cand=True, + cand_out=cand, + cand_ctl_out=ctl, + **common, + ) + torch.cuda.synchronize() + lf = logits.float() + torch.testing.assert_close(lf, lf0, atol=0.0, rtol=0.0) + + pairs = cand.view(num_rows, cap, 2) + vals_bits = pairs[..., 0] + idxs = pairs[..., 1] + vals = vals_bits.view(torch.float32) + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + t0 = seed_thr[row, 0] + ref_mask = lf[row, :ctx] >= t0 + ref_count = int(ref_mask.sum()) + ref_idx = set(torch.nonzero(ref_mask, as_tuple=False).flatten().tolist()) + tag = f"row={row} ctx={ctx} cap={cap} ref={ref_count} mode={cap_mode}" + claimed = int(ctl[row, 0]) + void = int(ctl[row, 1]) + # counts[0] is the exact count regardless of windows/overflow; + # claimed >= true count (sentinel-padded window tails). + assert int(seed_counts[row, 0]) == ref_count, f"counts0: {tag}" + assert claimed >= ref_count, f"claimed < true count: {tag} got={claimed}" + n_written = min(claimed, cap) + got_idx = idxs[row, :n_written].long() + live = got_idx >= 0 + got_list = got_idx[live].tolist() + assert len(set(got_list)) == len(got_list), f"duplicate idx: {tag}" + assert set(got_list).issubset(ref_idx), f"non-member idx: {tag}" + # pair integrity on live entries: value word == stored logit bits. + live_idx = got_idx[live] + torch.testing.assert_close( + vals[row, :n_written][live], + lf[row, live_idx], + atol=0.0, + rtol=0.0, + msg=lambda m, tag=tag: f"pair value mismatch: {tag}\n{m}", + ) + if cap_mode == "roomy": + assert void == 0, f"void set without overflow: {tag} claimed={claimed}" + assert claimed <= cap, f"claimed past cap without void: {tag}" + assert set(got_list) == ref_idx, f"set mismatch: {tag}" + # every claimed slot is live or sentinel; unclaimed tail untouched + assert bool((idxs[row, claimed:] == -1).all()), f"stray write: {tag}" + assert int(live.sum()) == ref_count, f"live count: {tag}" + else: + assert ref_count > cap, f"test setup wants overflow: {tag}" + assert void == 1, f"void not set on overflow: {tag}" + + # --------------------------------------------------------------------------- # Benchmarking entry point (run module directly). # --------------------------------------------------------------------------- From 68ef232ec3c1b524b7b8adc32685bd1e911d78f6 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:59:52 -0700 Subject: [PATCH 042/117] [None][fix] FP4 indexer: packed seed-row emission + counts-only threshold load fix Counts-only emission (emit_seed_counts without emit_cand) never loaded the per-q thresholds: the sthr (re)load sat inside the emit_cand block at both WG q-transition sites, so counters compared against the FLT_MAX init and flushed zeros. Move the load under emit_seed_counts where it belongs (L2 pre-collect commit had nested it wrong; its own tests all run emit_cand=True which masked it). Add seed_packed mode: one [rows, 8] fp32 seed row per the top-k pre-packed contract - lines at cols 0..2 (kernel reads (row, j<=2) unchanged), counts accumulate via red.global.add.f32 at cols 3..5 (exact to 2^24). Selected by passing an [rows, 8] seed_thr with seed_counts_out=None; the same buffer then feeds the top-k launch with no host repack. Unit test parametrized over both contracts; logits equality now compares valid prefixes only (past ctx the output buffer is unwritten allocator garbage and differs run-to-run). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 91 +++++++++++++------ .../paged_mqa_logits/fp4_paged_mqa_logits.py | 49 +++++++--- .../test_cute_dsl_fp4_paged_mqa_logits.py | 26 +++++- 3 files changed, 121 insertions(+), 45 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 0a01f7b71e93..0f0b12cae380 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -8254,13 +8254,14 @@ def _compile(cls, emit_block_meta=False, emit_hit_stats=True, emit_seed_counts=False, + seed_packed=False, emit_cand=False, cand_cap=5120): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, - emit_seed_counts, emit_cand, cand_cap) + emit_seed_counts, seed_packed, emit_cand, cand_cap) if key in cls.kernel_cache: return @@ -8356,14 +8357,28 @@ def _compile(cls, seed_thr_fake = None seed_counts_fake = None if emit_seed_counts: - seed_thr_fake = cute.runtime.make_fake_compact_tensor( - cutlass.Float32, (cute.sym_int(), 3), - stride_order=(1, 0), - assumed_align=4) - seed_counts_fake = cute.runtime.make_fake_compact_tensor( - cutlass.Int32, (cute.sym_int(), 3), - stride_order=(1, 0), - assumed_align=4) + if seed_packed: + # single [rows, 8] fp32 packed seed row: lines at + # cols 0..2 (kernel reads (row, j<=2) unchanged), + # counts accumulate as fp32 at cols 3..5; the same + # tensor is bound to both params. + seed_thr_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), 8), + stride_order=(1, 0), + assumed_align=4) + seed_counts_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), 8), + stride_order=(1, 0), + assumed_align=4) + else: + seed_thr_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), 3), + stride_order=(1, 0), + assumed_align=4) + seed_counts_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), 3), + stride_order=(1, 0), + assumed_align=4) fake_stream = cute.runtime.make_fake_stream( use_tvm_ffi_env_stream=True) @@ -8382,6 +8397,7 @@ def _compile(cls, emit_block_meta=emit_block_meta, emit_hit_stats=emit_hit_stats, emit_seed_counts=emit_seed_counts, + seed_packed=seed_packed, emit_cand=emit_cand, cand_cap=cand_cap, ) @@ -8549,28 +8565,44 @@ def forward( assert (block_max_out.shape == (B * next_n, nrec) and block_max_out.is_contiguous()) + seed_packed = False if emit_seed_counts: assert emit_block_meta, ( "emit_seed_counts requires emit_block_meta") - # Per-row seed-count emission: 3 thresholds per row (fp32, - # post-conversion value domain), counts accumulated with - # red.global.add.s32 — the caller must zero seed_counts_out - # each step. - assert ( - seed_thr is not None and seed_thr.dtype == torch.float32 - and seed_thr.is_cuda and seed_thr.is_contiguous() - and seed_thr.shape == (B * next_n, 3) - ), (f"emit_seed_counts requires seed_thr fp32 " - f"[{B * next_n}, 3]; got " - f"{None if seed_thr is None else (seed_thr.dtype, tuple(seed_thr.shape))}" - ) - assert (seed_counts_out is not None - and seed_counts_out.dtype == torch.int32 - and seed_counts_out.is_cuda - and seed_counts_out.is_contiguous() - and seed_counts_out.shape == (B * next_n, 3)), ( - "emit_seed_counts requires a caller-zeroed " - "seed_counts_out int32 [B*next_n, 3]") + if seed_counts_out is None: + # Packed contract: seed_thr IS the [rows, 8] fp32 seed + # row (top-k pre-packed layout). Lines at cols 0..2; + # counts accumulate as fp32 at cols 3..5 - the caller + # zeroes cols 3..7 and writes lines each step. The + # same buffer then feeds the top-k launch directly. + seed_packed = True + assert ( + seed_thr is not None and seed_thr.dtype == torch.float32 + and seed_thr.is_cuda and seed_thr.is_contiguous() + and seed_thr.shape == (B * next_n, 8) + ), (f"packed emit_seed_counts requires seed_thr fp32 " + f"[{B * next_n}, 8]; got " + f"{None if seed_thr is None else (seed_thr.dtype, tuple(seed_thr.shape))}" + ) + seed_counts_out = seed_thr + else: + # Legacy split contract: 3 thresholds per row (fp32), + # counts accumulated with red.global.add.s32 into a + # caller-zeroed int32 [rows, 3]. + assert ( + seed_thr is not None and seed_thr.dtype == torch.float32 + and seed_thr.is_cuda and seed_thr.is_contiguous() + and seed_thr.shape == (B * next_n, 3) + ), (f"emit_seed_counts requires seed_thr fp32 " + f"[{B * next_n}, 3]; got " + f"{None if seed_thr is None else (seed_thr.dtype, tuple(seed_thr.shape))}" + ) + assert (seed_counts_out.dtype == torch.int32 + and seed_counts_out.is_cuda + and seed_counts_out.is_contiguous() + and seed_counts_out.shape == (B * next_n, 3)), ( + "emit_seed_counts requires a caller-zeroed " + "seed_counts_out int32 [B*next_n, 3]") else: seed_thr = None seed_counts_out = None @@ -8604,7 +8636,7 @@ def forward( key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, - emit_seed_counts, emit_cand, cand_cap) + emit_seed_counts, seed_packed, emit_cand, cand_cap) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8620,6 +8652,7 @@ def forward( emit_block_meta=emit_block_meta, emit_hit_stats=emit_hit_stats, emit_seed_counts=emit_seed_counts, + seed_packed=seed_packed, emit_cand=emit_cand, cand_cap=cand_cap) compiled = cls.kernel_cache[key] diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index acb9b50e4672..cd5fbbaa07bc 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -469,6 +469,7 @@ def __init__( emit_block_meta: bool = False, emit_hit_stats: bool = True, emit_seed_counts: bool = False, + seed_packed: bool = False, emit_cand: bool = False, cand_cap: int = 5120, ): @@ -559,6 +560,14 @@ def __init__( if emit_seed_counts and not emit_block_meta: raise ValueError("emit_seed_counts requires emit_block_meta") self.emit_seed_counts = emit_seed_counts + # seed_packed: single [num_rows, 8] fp32 seed row per the top-k + # pre-packed contract - lines at cols 0..2, counts ACCUMULATED AS + # FLOATS at cols 3..5 (exact to 2^24; red.global.add.f32). The + # caller zeroes cols 3..7 and writes the lines each step; the + # same buffer feeds the top-k launch with no host repack. + if seed_packed and not emit_seed_counts: + raise ValueError("seed_packed requires emit_seed_counts") + self.seed_packed = seed_packed # emit_cand (L2 of the epilogue suite): unordered pre-collect of all # (value, index) pairs >= the t_0 seed threshold via warp ballot + # lane0 batch atomic claim. claimed >= K certifies the candidate @@ -1068,8 +1077,11 @@ def _flush_hit_agg( @cute.jit def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane): """Warp-redux the lane-local seed counters and fire one lane-0 - red.global.add.s32 per (t, threshold). Caller zero-initializes - mSeedCounts each step; cross-CTA totals accumulate atomically.""" + red.global.add per (t, threshold). Caller zero-initializes the + count slots each step; cross-CTA totals accumulate atomically. + + seed_packed: mSeedCounts IS the [num_rows, 8] packed seed row - + counts land as fp32 at cols 3..5 (exact to 2^24).""" next_n = cutlass.const_expr(self.next_n) base_addr = mSeedCounts.iterator.toint() for t in cutlass.range_constexpr(next_n): @@ -1077,10 +1089,16 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane): w_cnt = cute.arch.warp_redux_sync(scnt[t * 3 + j], "add") if meta_lane == cutlass.Int32(0): row = q_idx * cutlass.Int32(next_n) + cutlass.Int32(t) - addr = base_addr + ( - cutlass.Int64(row) * cutlass.Int64(3) + cutlass.Int64(j) - ) * cutlass.Int64(4) - _red_global_add_s32(addr, w_cnt) + if cutlass.const_expr(self.seed_packed): + addr = base_addr + ( + cutlass.Int64(row) * cutlass.Int64(8) + cutlass.Int64(3 + j) + ) * cutlass.Int64(4) + _red_global_add_f32(addr, cutlass.Float32(w_cnt)) + else: + addr = base_addr + ( + cutlass.Int64(row) * cutlass.Int64(3) + cutlass.Int64(j) + ) * cutlass.Int64(4) + _red_global_add_s32(addr, w_cnt) scnt[t * 3 + j] = cutlass.Int32(0) @cute.jit @@ -2216,14 +2234,18 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + # (re)load this q's thresholds - gated on + # emit_seed_counts, NOT emit_cand: counts- + # only mode needs them too (a stale + # FLT_MAX default zeroes every counter) + for _t in cutlass.range_constexpr(next_n): + for _j in cutlass.range_constexpr(3): + sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] if cutlass.const_expr(self.emit_cand): if q_idx_old < batch_size: self._flush_cand_window( mCand, q_idx_old, cwbase, cwleft, meta_lane ) - for _t in cutlass.range_constexpr(next_n): - for _j in cutlass.range_constexpr(3): - sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] ctx_cur = mContextLens[q_idx] # Process KV block for group 0 (kv_idx + 0) @@ -2760,14 +2782,17 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + # (re)load this q's thresholds - gated on + # emit_seed_counts, NOT emit_cand (see the + # WG0 twin above) + for _t in cutlass.range_constexpr(next_n): + for _j in cutlass.range_constexpr(3): + sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] if cutlass.const_expr(self.emit_cand): if q_idx_old < batch_size: self._flush_cand_window( mCand, q_idx_old, cwbase, cwleft, meta_lane ) - for _t in cutlass.range_constexpr(next_n): - for _j in cutlass.range_constexpr(3): - sthr[_t * 3 + _j] = mSeedThr[(q_idx * next_n + _t, _j)] ctx_cur = mContextLens[q_idx] # Process KV block for group 1 (kv_idx + 1) diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py index 5d4f745cf407..9ac8c880846a 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py @@ -766,12 +766,14 @@ def test_cute_dsl_fp4_paged_mqa_logits_block_meta( @pytest.mark.parametrize("avg_ctx", [4096, 4224]) @pytest.mark.parametrize("phys_block_kv", [64, 128]) @pytest.mark.parametrize("fix_length", [True, False]) +@pytest.mark.parametrize("packed", [False, True]) def test_cute_dsl_fp4_paged_mqa_logits_seed_counts( batch_size, next_n, avg_ctx, phys_block_kv, fix_length, + packed, ): """emit_seed_counts exactness: per-row counts of logits >= threshold recomputed from the KERNEL'S OWN logits output (the count contract is @@ -867,7 +869,15 @@ def test_cute_dsl_fp4_paged_mqa_logits_seed_counts( seed_thr[row, 1] = torch.quantile(vals, 0.90) seed_thr[row, 2] = torch.quantile(vals, 0.998) - seed_counts = torch.zeros((num_rows, 3), dtype=torch.int32, device=device) + if packed: + # Packed contract: one [rows, 8] fp32 seed row, lines at cols + # 0..2, counts accumulate as fp32 at cols 3..5 (caller zeroes). + seed_row = torch.zeros((num_rows, 8), dtype=torch.float32, device=device) + seed_row[:, 0:3] = seed_thr + thr_arg, counts_arg = seed_row, None + else: + seed_counts = torch.zeros((num_rows, 3), dtype=torch.int32, device=device) + thr_arg, counts_arg = seed_thr, seed_counts block_max.fill_(nan) logits, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( q_fp4, @@ -882,14 +892,22 @@ def test_cute_dsl_fp4_paged_mqa_logits_seed_counts( emit_hit_stats=False, block_max_out=block_max, emit_seed_counts=True, - seed_thr=seed_thr, - seed_counts_out=seed_counts, + seed_thr=thr_arg, + seed_counts_out=counts_arg, **common, ) torch.cuda.synchronize() lf = logits.float() - torch.testing.assert_close(lf, lf0, atol=0.0, rtol=0.0) + # compare valid prefixes only: past ctx the buffer is unwritten + # allocator garbage and differs run-to-run + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + torch.testing.assert_close(lf[row, :ctx], lf0[row, :ctx], atol=0.0, rtol=0.0) + if packed: + assert torch.equal(seed_row[:, 0:3], seed_thr), "lines clobbered" + assert (seed_row[:, 6:8] == 0).all(), "stray write past counts" + seed_counts = seed_row[:, 3:6].to(torch.int32) for row in range(num_rows): ctx = int(context_lens[row // next_n].item()) tag = f"row={row} ctx={ctx} next_n={next_n} pbk={phys_block_kv}" From 5d1812d63f702c0ea2ae9863a1752632a5872c5a Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:05:37 -0700 Subject: [PATCH 043/117] [None][chore] GVR: device-residency stamps in P4_TAIL_DBG Add a row-phase entry clock under GVR_P4_TAIL_DBG and publish device total (entry->publish) and true in-kernel prologue to xstate[1]/[2]. Wall minus device total isolates host launch/marshalling from kernel work - warm back-to-back walls previously mis-attributed ~9us of host gap as kernel prologue on mid-range cells. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 7dad3d33a8f1..9829d4215802 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -5404,6 +5404,9 @@ def _run_phases( # CTA-uniform and the dynamic branches below stay convergent. ck0 = cutlass.Int64(0) ck1 = cutlass.Int64(0) + ckE = cutlass.Int64(0) + if cutlass.const_expr(_P4_TAIL_DBG): + ckE = cute.arch.clock64() # row-phase entry (device-residency ref) ext_row = cutlass.Int32(0) if cutlass.const_expr(self.use_ext_counts): # line validity mirrors ext_rungs: ALL THREE lines finite and @@ -6794,6 +6797,11 @@ def _run_phases( xstate_row[2] = anch_pub if cutlass.const_expr(_P4_TAIL_DBG): ck3 = cute.arch.clock64() + # [1] device total (entry->publish), [2] true + # in-kernel prologue (entry->walk start): wall + # minus [1] = host/launch, NOT kernel work + xstate_row[1] = cutlass.Float32(cutlass.Int32(ck3 - ckE)) + xstate_row[2] = cutlass.Float32(cutlass.Int32(ck0 - ckE)) xstate_row[4] = cutlass.Float32(cutlass.Int32(ck1 - ck0)) # walk+flags xstate_row[5] = cutlass.Float32(cutlass.Int32(ck2 - ck1)) # P2/P3 gap xstate_row[6] = cutlass.Float32(cutlass.Int32(ck3 - ck2)) # Phase 4 From aec6c665fe6d1c331be0447be54c328db1cb9c28 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:27:52 -0700 Subject: [PATCH 044/117] [None][perf] GVR P4: eager position fetch + warp0 bin search Two independent cuts on the rank-scatter critical path (P4 was 80% of device time on latency-bound mid-range cells): 1. Eager position fetch (ext-cand list rows): the take walk already computes each candidate's segmented slot - fetch the idx column in the SAME 4-fragment ILP batch as the value read and store TRUE positions in smem_vals. The post-P4 slot swap (a 3-round gmem read-gather-write over K outputs, serialized cold late in the kernel) disappears; self_scan keeps the old swap (its positions live in its own column). Extra idx bytes are streamed alongside the value column the walk reads anyway. 2. warp0 bin search (coarse + fine): the 3-step block searches spent ~0.2us per block barrier staging warp partials. One warp now owns the whole search: per-lane descending chunk sums, an inclusive shuffle scan, ballot for the straddling lane (prefix monotonicity: target = 32 - popc), then a lane-serial resolve. Three barriers collapse into one on each search; the count resets fold into the resolving lane. Cold-protocol kernel-only (pro mid cells, B200): wf 8k/16k/32k/64k -14..-17.5% (8.56->7.35us at 8k B1; speedup vs PR tip 1.19->1.38, 1.44->1.75 at 64k B1); P4_SUB fine 1.2->0.78us; va shares the bin search win. unified_smoke 3 cells x 10 modes exact incl. flash-512k long rows and list+block-skip. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 233 +++++++++--------- 1 file changed, 117 insertions(+), 116 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 9829d4215802..420df20c4088 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3479,56 +3479,49 @@ def phase4_rank_scatter( cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc2 = cute.arch.clock64() - # ---- 3-step high→low bin search → straddling bin b* + rank_above ---- - warp_bin_sum = cutlass.Int32(0) - for jb in cutlass.range_constexpr(bins_per_warp): - bidx_s = ( - cutlass.Int32(kBins - 1) - - warp_id * cutlass.Int32(bins_per_warp) - - cutlass.Int32(jb) - ) - warp_bin_sum = warp_bin_sum + smem_hist[bidx_s] - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = warp_bin_sum - cute.arch.barrier() - if tidx == cutlass.Int32(0): - cum = cutlass.Int32(0) - tw = cutlass.Int32(num_warps - 1) - found = cutlass.Int32(0) - for w2 in cutlass.range_constexpr(self.num_warps): - cum = cum + smem_wcnt[w2] - if cum >= cutlass.Int32(kK) and found == cutlass.Int32(0): - tw = cutlass.Int32(w2) - found = cutlass.Int32(1) - cum2 = cutlass.Int32(0) - for w3 in cutlass.range_constexpr(self.num_warps): - if cutlass.Int32(w3) < tw: - cum2 = cum2 + smem_wcnt[w3] - s_iscalars[2] = cum2 # prefix-count before target warp - s_iscalars[3] = tw - cute.arch.barrier() - target_warp = s_iscalars[3] - if warp_id == target_warp and lane == cutlass.Int32(0): - base_cum = s_iscalars[2] - b_star = cutlass.Int32(kBins - 1) - rank_above = base_cum - set_d = cutlass.Int32(0) - for jb2 in cutlass.range_constexpr(bins_per_warp): - bidx2 = ( + # ---- warp0 high→low bin search → straddling bin b* + rank_above ---- + # Single-warp shuffle scan replaces the 3-step block search: + # lane L owns the L-th DESCENDING chunk of bins, an inclusive + # warp scan finds the straddling lane (prefixes are monotone, + # so hit lanes form a suffix: target = 32 - popc(ballot)), + # and that lane resolves b* serially in its own chunk. Two of + # the three block barriers (~0.2us each) disappear. + if warp_id == cutlass.Int32(0): + lsum = cutlass.Int32(0) + for jb in cutlass.range_constexpr(bins_per_warp): + bidx_s = ( cutlass.Int32(kBins - 1) - - target_warp * cutlass.Int32(bins_per_warp) - - cutlass.Int32(jb2) + - lane * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb) ) - ra_before = base_cum - base_cum = base_cum + smem_hist[bidx2] - if base_cum >= cutlass.Int32(kK) and set_d == cutlass.Int32(0): - b_star = bidx2 - rank_above = ra_before # count in bins strictly above b* - set_d = cutlass.Int32(1) - s_iscalars[2] = rank_above - s_iscalars[3] = b_star - s_iscalars[4] = cutlass.Int32(0) # cnt_above - s_iscalars[1] = cutlass.Int32(0) # cnt_straddle + lsum = lsum + smem_hist[bidx_s] + pref = warp_scan(lsum, tidx, lane, 32) + hit0 = cutlass.Int32(0) + if pref >= cutlass.Int32(kK): + hit0 = cutlass.Int32(1) + mhit = cute.arch.vote_ballot_sync(hit0 != cutlass.Int32(0)) + tl0 = cutlass.Int32(32) - cutlass.Int32(cute.arch.popc(mhit)) + if lane == tl0: + base_cum = pref - lsum # exclusive prefix before chunk + b_star = cutlass.Int32(kBins - 1) + rank_above = base_cum + set_d = cutlass.Int32(0) + for jb2 in cutlass.range_constexpr(bins_per_warp): + bidx2 = ( + cutlass.Int32(kBins - 1) + - lane * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb2) + ) + ra_before = base_cum + base_cum = base_cum + smem_hist[bidx2] + if base_cum >= cutlass.Int32(kK) and set_d == cutlass.Int32(0): + b_star = bidx2 + rank_above = ra_before # count strictly above b* + set_d = cutlass.Int32(1) + s_iscalars[2] = rank_above + s_iscalars[3] = b_star + s_iscalars[4] = cutlass.Int32(0) # cnt_above + s_iscalars[1] = cutlass.Int32(0) # cnt_straddle cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc3 = cute.arch.clock64() @@ -3569,66 +3562,50 @@ def phase4_rank_scatter( atomicAdd(smem_hist.iterator + sb, cutlass.Int32(1)) ifb = ifb + cutlass.Int32(num_threads) cute.arch.barrier() - # fine 3-step search seeded at rank_above (over fbins bins) - fws = cutlass.Int32(0) - for jbf in cutlass.range_constexpr(fbpw): - bif = ( - cutlass.Int32(fbins - 1) - - warp_id * cutlass.Int32(fbpw) - - cutlass.Int32(jbf) - ) - fws = fws + smem_hist[bif] - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = fws - cute.arch.barrier() - if tidx == cutlass.Int32(0): - cumf = rank_above - twf = cutlass.Int32(num_warps - 1) - fnd = cutlass.Int32(0) - for w2 in cutlass.range_constexpr(self.num_warps): - cumf = cumf + smem_wcnt[w2] - if cumf >= cutlass.Int32(kK) and fnd == cutlass.Int32(0): - twf = cutlass.Int32(w2) - fnd = cutlass.Int32(1) - pre = rank_above - for w3 in cutlass.range_constexpr(self.num_warps): - if cutlass.Int32(w3) < twf: - pre = pre + smem_wcnt[w3] - # Stage prefix/target-warp metadata in spare s_iscalars - # slots, NOT smem_hist[0]/[1]: the last fine warp's reverse - # scan below walks fine bins down to 0/1, so reusing those - # histogram bins as scratch would corrupt sb_star/ra_fine - # when twf2 == num_warps-1. Slots [4]/[1] are dead here - # (re-zeroed at the cnt_above/cnt_strad reset below). - s_iscalars[4] = pre # prefix into target fine warp - s_iscalars[1] = twf # target fine warp - cute.arch.barrier() - pre_f = s_iscalars[4] - twf2 = s_iscalars[1] - if warp_id == twf2 and lane == cutlass.Int32(0): - base_f = pre_f - sb_star = cutlass.Int32(fbins - 1) - ra_fine = base_f - sd = cutlass.Int32(0) - for jb3 in cutlass.range_constexpr(fbpw): - sbi = ( + # fine warp0 search seeded at rank_above (over fbins bins); + # same single-warp shuffle-scan shape as the coarse search + # above - three block barriers collapse into one. sb*/ra + # go via smem_hist[2]/[3] (fine bins walked never reach + # slots 2/3 only when the target chunk excludes them, so + # publish AFTER the serial walk like before - the walk + # reads bins, the publish overwrites scratch slots). + if warp_id == cutlass.Int32(0): + fls = cutlass.Int32(0) + for jbf in cutlass.range_constexpr(fbpw): + bif = ( cutlass.Int32(fbins - 1) - - twf2 * cutlass.Int32(fbpw) - - cutlass.Int32(jb3) + - lane * cutlass.Int32(fbpw) + - cutlass.Int32(jbf) ) - ra_b = base_f - base_f = base_f + smem_hist[sbi] - if base_f >= cutlass.Int32(kK) and sd == cutlass.Int32(0): - sb_star = sbi - ra_fine = ra_b - sd = cutlass.Int32(1) - smem_hist[2] = sb_star - smem_hist[3] = ra_fine - cute.arch.barrier() - if tidx == cutlass.Int32(0): - s_iscalars[4] = cutlass.Int32(0) # cnt_above - s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) - s_iscalars[1] = cutlass.Int32(0) # cnt_strad (b*, sub==sb*) + fls = fls + smem_hist[bif] + fpref = warp_scan(fls, tidx, lane, 32) + rank_above + fhit = cutlass.Int32(0) + if fpref >= cutlass.Int32(kK): + fhit = cutlass.Int32(1) + fm = cute.arch.vote_ballot_sync(fhit != cutlass.Int32(0)) + ftl = cutlass.Int32(32) - cutlass.Int32(cute.arch.popc(fm)) + if lane == ftl: + base_f = fpref - fls + sb_star = cutlass.Int32(fbins - 1) + ra_fine = base_f + sd = cutlass.Int32(0) + for jb3 in cutlass.range_constexpr(fbpw): + sbi = ( + cutlass.Int32(fbins - 1) + - lane * cutlass.Int32(fbpw) + - cutlass.Int32(jb3) + ) + ra_b = base_f + base_f = base_f + smem_hist[sbi] + if base_f >= cutlass.Int32(kK) and sd == cutlass.Int32(0): + sb_star = sbi + ra_fine = ra_b + sd = cutlass.Int32(1) + smem_hist[2] = sb_star + smem_hist[3] = ra_fine + s_iscalars[4] = cutlass.Int32(0) # cnt_above + s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) + s_iscalars[1] = cutlass.Int32(0) # cnt_strad (b*, sub==sb*) cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc4 = cute.arch.clock64() @@ -5720,8 +5697,10 @@ def _run_phases( line_cut = cutlass.Int32(0) anch_t = cutlass.Float32(0.0) vbase = cutlass.Int64(0) + ibase = cutlass.Int64(0) if cutlass.const_expr(True): vbase = cand_vals_row.iterator.toint() + ibase = cand_idx_row.iterator.toint() if usable == cutlass.Int32(1): # cut = tightest line in [K, B*]; anchor = loosest. if n2_c >= kK_l and n2_c <= bs_l: @@ -5947,7 +5926,19 @@ def _run_phases( smem_keys[j_c] = cute.make_tensor(vp_c, cute.make_layout((1,)))[ 0 ] - smem_vals[j_c] = src_c + # eager position fetch: the idx column + # rides the same ILP batch as the value + # read, so vals hold TRUE positions and + # the post-P4 slot swap disappears + ip_c = cute.make_ptr( + cutlass.Int32, + ibase + cutlass.Int64(src_c) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + smem_vals[j_c] = cute.make_tensor(ip_c, cute.make_layout((1,)))[ + 0 + ] i_c = i_c + cutlass.Int32(4 * num_threads) cute.arch.barrier() if line_cut == cutlass.Int32(0): @@ -5962,7 +5953,7 @@ def _run_phases( for _ju in cutlass.range_constexpr(4): j_c = i_c + cutlass.Int32(_ju * num_threads) pval = cutlass.Float32(self.NEG_FLT_MAX) - src_c = cutlass.Int32(0) + pidx = cutlass.Int32(-1) keep = cutlass.Int32(0) if j_c < total_l: src_c = j_c @@ -5977,10 +5968,20 @@ def _run_phases( assumed_align=4, ) pval = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] + # eager position fetch (unconditional: + # keeps the load independent of the + # value compare, same ILP batch) + ip_c = cute.make_ptr( + cutlass.Int32, + ibase + cutlass.Int64(src_c) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + pidx = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] if pval >= cut_t: keep = cutlass.Int32(1) pvals.append(pval) - pidxs.append(src_c) + pidxs.append(pidx) keeps.append(keep) m0 = cute.arch.vote_ballot_sync(keeps[0] != cutlass.Int32(0)) m1 = cute.arch.vote_ballot_sync(keeps[1] != cutlass.Int32(0)) @@ -6745,15 +6746,15 @@ def _run_phases( if cutlass.const_expr(_P4_SUB_DBG): ck_sw0 = cute.arch.clock64() if cutlass.const_expr( - (self.use_ext_cand or self.self_scan) - and self.use_ext_counts - and self.dtype == cutlass.Float32 + self.self_scan and self.use_ext_counts and self.dtype == cutlass.Float32 ): - # List rows: the compact stored LIST INDICES in the - # vals slots (saving a second cold gmem pass over the - # position column). Swap them for true positions with - # K fully-parallel gathers. Must precede the xstate + # self_scan rows: the compact stored SEGMENT COORDS in + # the vals slots. Swap them for true positions with K + # fully-parallel gathers. Must precede the xstate # publish, which reads output slot K-1 as a position. + # (ext_cand list rows translate EAGERLY in the take + # walk - the idx column rides the value ILP batch - + # so they never reach this loop.) if list_used == cutlass.Int32(1): io_r = tidx while io_r < cutlass.Int32(self.top_k): From 71f3d709119e9066ebb90240b55cf3fe1e375bda Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:42:53 -0700 Subject: [PATCH 045/117] [None][perf] GVR P4: walk-fused range + hist zero for list rows List rows know their candidate minimum by construction (the cut line), and the take walk already touches every candidate value - accumulate the per-fragment max there (free ILP), warp-reduce it into smem_wcnt under the walk's existing end barrier, and pre-zero the coarse histogram in the walk prologue. P4's block min/max scan, its zero pass and their three barriers all vanish for list rows; fallback rows keep the stock sequence (block-uniform runtime branch, same pattern as the ext-row paths). Also strictly safer than the old min: sentinel pads (-inf) used to drag bmin to -inf on t0 cuts and collapse the coarse binning; cut-line min bins them to slot 0 via the existing clamp. Warm device residency on pro mid cells: 5.3 -> 4.6us (P4+pub 4.2 -> 3.3us, walk +0.2 absorbing the staging); 32 cells exact, unified_smoke 3 cells x 10 modes exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 165 ++++++++++++------ 1 file changed, 113 insertions(+), 52 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 420df20c4088..c237f8aeda79 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3403,6 +3403,8 @@ def phase4_rank_scatter( tidx, warp_id, lane, + ext_range_flag=None, # list rows: walk pre-staged range + hist zero + ext_min=None, # list rows: cut line == exact candidate minimum ): kK = cutlass.const_expr(self.top_k) kBins = cutlass.const_expr(self.kNumBins) @@ -3427,43 +3429,61 @@ def phase4_rank_scatter( sc6 = cutlass.Int64(0) if cutlass.const_expr(_P4_SUB_DBG): sc0 = cute.arch.clock64() - # ---- block min/max over candidates ---- - local_cmin = cutlass.Float32(self.FLT_MAX) - local_cmax = cutlass.Float32(self.NEG_FLT_MAX) - i5 = tidx - while i5 < cand_count: - v = smem_keys[i5] - local_cmin = _fmin_f32_inline(local_cmin, v) - local_cmax = cute.arch.fmax(local_cmax, v) - i5 = i5 + cutlass.Int32(num_threads) - cmin = self.warp_reduce_min_f32(local_cmin) - cmax = self.warp_reduce_max_f32(local_cmax) - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = float_as_uint32(cmin) - smem_hist[warp_id] = float_as_uint32(cmax) - cute.arch.barrier() bmin_r = cutlass.Float32(self.FLT_MAX) bmax_r = cutlass.Float32(self.NEG_FLT_MAX) - for w in cutlass.range_constexpr(self.num_warps): - vmin = cutlass.Float32( - llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[w].ir_value()) - ) - vmax = cutlass.Float32( - llvm.bitcast(cutlass.Float32.mlir_type, smem_hist[w].ir_value()) - ) - bmin_r = _fmin_f32_inline(bmin_r, vmin) - bmax_r = cute.arch.fmax(bmax_r, vmax) - if bmax_r <= bmin_r: - bmax_r = bmin_r + cutlass.Float32(1e-6) - cute.arch.barrier() - if cutlass.const_expr(_P4_SUB_DBG): - sc1 = cute.arch.clock64() - # ---- zero + build histogram ---- - i6 = tidx - while i6 < cutlass.Int32(kBins): - smem_hist[i6] = cutlass.Int32(0) - i6 = i6 + cutlass.Int32(num_threads) - cute.arch.barrier() + use_ext_r = cutlass.Int32(0) + if cutlass.const_expr(ext_range_flag is not None): + use_ext_r = ext_range_flag + if use_ext_r == cutlass.Int32(1): + # list rows: the take walk pre-zeroed the hist and staged + # per-warp maxima in smem_wcnt (its end barrier orders + # them); min := cut line by construction. The minmax + # scan, the zero pass and their three barriers vanish. + if cutlass.const_expr(ext_min is not None): + bmin_r = ext_min + for w in cutlass.range_constexpr(self.num_warps): + vmax = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[w].ir_value()) + ) + bmax_r = cute.arch.fmax(bmax_r, vmax) + if bmax_r <= bmin_r: + bmax_r = bmin_r + cutlass.Float32(1e-6) + if use_ext_r == cutlass.Int32(0): + # ---- block min/max over candidates ---- + local_cmin = cutlass.Float32(self.FLT_MAX) + local_cmax = cutlass.Float32(self.NEG_FLT_MAX) + i5 = tidx + while i5 < cand_count: + v = smem_keys[i5] + local_cmin = _fmin_f32_inline(local_cmin, v) + local_cmax = cute.arch.fmax(local_cmax, v) + i5 = i5 + cutlass.Int32(num_threads) + cmin = self.warp_reduce_min_f32(local_cmin) + cmax = self.warp_reduce_max_f32(local_cmax) + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = float_as_uint32(cmin) + smem_hist[warp_id] = float_as_uint32(cmax) + cute.arch.barrier() + for w in cutlass.range_constexpr(self.num_warps): + vmin = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[w].ir_value()) + ) + vmax = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_hist[w].ir_value()) + ) + bmin_r = _fmin_f32_inline(bmin_r, vmin) + bmax_r = cute.arch.fmax(bmax_r, vmax) + if bmax_r <= bmin_r: + bmax_r = bmin_r + cutlass.Float32(1e-6) + cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc1 = cute.arch.clock64() + # ---- zero + build histogram ---- + i6 = tidx + while i6 < cutlass.Int32(kBins): + smem_hist[i6] = cutlass.Int32(0) + i6 = i6 + cutlass.Int32(num_threads) + cute.arch.barrier() range1 = bmax_r - bmin_r inv1 = (cutlass.Float32(kBins - 1) + cutlass.Float32(0.99)) / range1 i7 = tidx @@ -5901,6 +5921,17 @@ def _run_phases( s_iscalars[1] = cutlass.Int32(1) # done cute.arch.barrier() lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) + # fused P4 prologue: zero the coarse hist here and + # accumulate the candidate max INSIDE the cut walk + # (per-fragment fmax is free ILP; min := cut line by + # construction). P4's minmax scan + zero pass and + # their three barriers disappear for list rows - the + # staging rides this walk's own end barrier. + izh_c = tidx + while izh_c < cutlass.Int32(self.kNumBins): + smem_hist[izh_c] = cutlass.Int32(0) + izh_c = izh_c + cutlass.Int32(num_threads) + wmax_acc = cutlass.Float32(self.NEG_FLT_MAX) if line_cut == cutlass.Int32(1): # ---- LINE cut: dense mapped-prefix COPY of # exactly cut_n entries. No filter, no ballots, @@ -5923,9 +5954,9 @@ def _run_phases( cute.AddressSpace.gmem, assumed_align=4, ) - smem_keys[j_c] = cute.make_tensor(vp_c, cute.make_layout((1,)))[ - 0 - ] + pv_c = cute.make_tensor(vp_c, cute.make_layout((1,)))[0] + smem_keys[j_c] = pv_c + wmax_acc = cute.arch.fmax(wmax_acc, pv_c) # eager position fetch: the idx column # rides the same ILP batch as the value # read, so vals hold TRUE positions and @@ -5940,6 +5971,9 @@ def _run_phases( 0 ] i_c = i_c + cutlass.Int32(4 * num_threads) + wmax_w = self.warp_reduce_max_f32(wmax_acc) + if lane_c == cutlass.Int32(0): + smem_wcnt[tidx // cutlass.Int32(32)] = float_as_uint32(wmax_w) cute.arch.barrier() if line_cut == cutlass.Int32(0): # ---- histogram-edge cut: value-filtered mapped @@ -5980,6 +6014,7 @@ def _run_phases( pidx = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] if pval >= cut_t: keep = cutlass.Int32(1) + wmax_acc = cute.arch.fmax(wmax_acc, pval) pvals.append(pval) pidxs.append(pidx) keeps.append(keep) @@ -6019,6 +6054,9 @@ def _run_phases( smem_vals[wpos] = pidxs[_ju] off = off + cutlass.Int32(cute.arch.popc(mj)) i_c = i_c + cutlass.Int32(4 * num_threads) + wmax_w2 = self.warp_reduce_max_f32(wmax_acc) + if lane_c == cutlass.Int32(0): + smem_wcnt[tidx // cutlass.Int32(32)] = float_as_uint32(wmax_w2) cute.arch.barrier() cnt_l = s_iscalars[0] if cnt_l < cutlass.Int32(self.top_k) or cnt_l > cutlass.Int32(self.kC): @@ -6712,20 +6750,43 @@ def _run_phases( # cs=1: the single CTA per row IS the leader. cand_count_p4 = min(s_iscalars[0], cutlass.Int32(self.kC)) if cutlass.const_expr(self.enable_p4_rank_scatter): - self.phase4_rank_scatter( - smem_keys, - smem_vals, - smem_hist, - smem_wcnt, - s_thr, - s_iscalars, - output_values_row, - output_indices_row, - cand_count_p4, - tidx, - warp_id, - lane, - ) + if cutlass.const_expr( + self.use_ext_cand and self.use_ext_counts and self.dtype == cutlass.Float32 + ): + # list rows carry a walk-staged range + pre-zeroed + # hist (flag = list_used; fallback rows take the + # stock minmax path inside) + self.phase4_rank_scatter( + smem_keys, + smem_vals, + smem_hist, + smem_wcnt, + s_thr, + s_iscalars, + output_values_row, + output_indices_row, + cand_count_p4, + tidx, + warp_id, + lane, + ext_range_flag=list_used, + ext_min=cut_t, + ) + else: + self.phase4_rank_scatter( + smem_keys, + smem_vals, + smem_hist, + smem_wcnt, + s_thr, + s_iscalars, + output_values_row, + output_indices_row, + cand_count_p4, + tidx, + warp_id, + lane, + ) else: self.phase4_histogram_snap( smem_keys, From badea6ca4f38ce8b70621588007109f48157dbd6 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:41:45 -0700 Subject: [PATCH 046/117] [None][fix] GVR P4: revert warp0 bin search to the 3-step block search The single-warp shuffle-scan rewrite of the coarse/fine bin searches (part of the previous P4 commit) compiles and passes single-cell exactness, unified_smoke and all three compute-sanitizer tools, but intermittently corrupts results / hits illegal addresses under a full harness context (reused buffers + warmup + cold-evict loops; failure location shifts run to run). Kernel-file bisect against a context repro pins it to the warp0 hunks: warp collectives and first-assigned locals inside a dynamic 'if warp_id == 0:' branch are not reliably compiled by the DSL. Revert both searches to the 3-step block form (~0.4us given back on mid cells); eager position fetch and the walk-fused range stay in and are context-repro/cold-verified clean. Cold kernel-only after this state (pro mid, 30-layer means): wf 8k/16k/32k/64k B1 = 1.41/1.52/1.56/1.79x vs PR tip (pre-sprint: 1.19/1.25/1.28/1.44); flash 512k B1 2.54x; 480 cold cells + 30-point smoke all exact. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 189 ++++++++++-------- 1 file changed, 106 insertions(+), 83 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index c237f8aeda79..b0ca43ac5a00 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3499,49 +3499,56 @@ def phase4_rank_scatter( cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc2 = cute.arch.clock64() - # ---- warp0 high→low bin search → straddling bin b* + rank_above ---- - # Single-warp shuffle scan replaces the 3-step block search: - # lane L owns the L-th DESCENDING chunk of bins, an inclusive - # warp scan finds the straddling lane (prefixes are monotone, - # so hit lanes form a suffix: target = 32 - popc(ballot)), - # and that lane resolves b* serially in its own chunk. Two of - # the three block barriers (~0.2us each) disappear. - if warp_id == cutlass.Int32(0): - lsum = cutlass.Int32(0) - for jb in cutlass.range_constexpr(bins_per_warp): - bidx_s = ( + # ---- 3-step high→low bin search → straddling bin b* + rank_above ---- + warp_bin_sum = cutlass.Int32(0) + for jb in cutlass.range_constexpr(bins_per_warp): + bidx_s = ( + cutlass.Int32(kBins - 1) + - warp_id * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb) + ) + warp_bin_sum = warp_bin_sum + smem_hist[bidx_s] + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = warp_bin_sum + cute.arch.barrier() + if tidx == cutlass.Int32(0): + cum = cutlass.Int32(0) + tw = cutlass.Int32(num_warps - 1) + found = cutlass.Int32(0) + for w2 in cutlass.range_constexpr(self.num_warps): + cum = cum + smem_wcnt[w2] + if cum >= cutlass.Int32(kK) and found == cutlass.Int32(0): + tw = cutlass.Int32(w2) + found = cutlass.Int32(1) + cum2 = cutlass.Int32(0) + for w3 in cutlass.range_constexpr(self.num_warps): + if cutlass.Int32(w3) < tw: + cum2 = cum2 + smem_wcnt[w3] + s_iscalars[2] = cum2 # prefix-count before target warp + s_iscalars[3] = tw + cute.arch.barrier() + target_warp = s_iscalars[3] + if warp_id == target_warp and lane == cutlass.Int32(0): + base_cum = s_iscalars[2] + b_star = cutlass.Int32(kBins - 1) + rank_above = base_cum + set_d = cutlass.Int32(0) + for jb2 in cutlass.range_constexpr(bins_per_warp): + bidx2 = ( cutlass.Int32(kBins - 1) - - lane * cutlass.Int32(bins_per_warp) - - cutlass.Int32(jb) + - target_warp * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb2) ) - lsum = lsum + smem_hist[bidx_s] - pref = warp_scan(lsum, tidx, lane, 32) - hit0 = cutlass.Int32(0) - if pref >= cutlass.Int32(kK): - hit0 = cutlass.Int32(1) - mhit = cute.arch.vote_ballot_sync(hit0 != cutlass.Int32(0)) - tl0 = cutlass.Int32(32) - cutlass.Int32(cute.arch.popc(mhit)) - if lane == tl0: - base_cum = pref - lsum # exclusive prefix before chunk - b_star = cutlass.Int32(kBins - 1) - rank_above = base_cum - set_d = cutlass.Int32(0) - for jb2 in cutlass.range_constexpr(bins_per_warp): - bidx2 = ( - cutlass.Int32(kBins - 1) - - lane * cutlass.Int32(bins_per_warp) - - cutlass.Int32(jb2) - ) - ra_before = base_cum - base_cum = base_cum + smem_hist[bidx2] - if base_cum >= cutlass.Int32(kK) and set_d == cutlass.Int32(0): - b_star = bidx2 - rank_above = ra_before # count strictly above b* - set_d = cutlass.Int32(1) - s_iscalars[2] = rank_above - s_iscalars[3] = b_star - s_iscalars[4] = cutlass.Int32(0) # cnt_above - s_iscalars[1] = cutlass.Int32(0) # cnt_straddle + ra_before = base_cum + base_cum = base_cum + smem_hist[bidx2] + if base_cum >= cutlass.Int32(kK) and set_d == cutlass.Int32(0): + b_star = bidx2 + rank_above = ra_before # count in bins strictly above b* + set_d = cutlass.Int32(1) + s_iscalars[2] = rank_above + s_iscalars[3] = b_star + s_iscalars[4] = cutlass.Int32(0) # cnt_above + s_iscalars[1] = cutlass.Int32(0) # cnt_straddle cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc3 = cute.arch.clock64() @@ -3582,50 +3589,66 @@ def phase4_rank_scatter( atomicAdd(smem_hist.iterator + sb, cutlass.Int32(1)) ifb = ifb + cutlass.Int32(num_threads) cute.arch.barrier() - # fine warp0 search seeded at rank_above (over fbins bins); - # same single-warp shuffle-scan shape as the coarse search - # above - three block barriers collapse into one. sb*/ra - # go via smem_hist[2]/[3] (fine bins walked never reach - # slots 2/3 only when the target chunk excludes them, so - # publish AFTER the serial walk like before - the walk - # reads bins, the publish overwrites scratch slots). - if warp_id == cutlass.Int32(0): - fls = cutlass.Int32(0) - for jbf in cutlass.range_constexpr(fbpw): - bif = ( + # fine 3-step search seeded at rank_above (over fbins bins) + fws = cutlass.Int32(0) + for jbf in cutlass.range_constexpr(fbpw): + bif = ( + cutlass.Int32(fbins - 1) + - warp_id * cutlass.Int32(fbpw) + - cutlass.Int32(jbf) + ) + fws = fws + smem_hist[bif] + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = fws + cute.arch.barrier() + if tidx == cutlass.Int32(0): + cumf = rank_above + twf = cutlass.Int32(num_warps - 1) + fnd = cutlass.Int32(0) + for w2 in cutlass.range_constexpr(self.num_warps): + cumf = cumf + smem_wcnt[w2] + if cumf >= cutlass.Int32(kK) and fnd == cutlass.Int32(0): + twf = cutlass.Int32(w2) + fnd = cutlass.Int32(1) + pre = rank_above + for w3 in cutlass.range_constexpr(self.num_warps): + if cutlass.Int32(w3) < twf: + pre = pre + smem_wcnt[w3] + # Stage prefix/target-warp metadata in spare s_iscalars + # slots, NOT smem_hist[0]/[1]: the last fine warp's reverse + # scan below walks fine bins down to 0/1, so reusing those + # histogram bins as scratch would corrupt sb_star/ra_fine + # when twf2 == num_warps-1. Slots [4]/[1] are dead here + # (re-zeroed at the cnt_above/cnt_strad reset below). + s_iscalars[4] = pre # prefix into target fine warp + s_iscalars[1] = twf # target fine warp + cute.arch.barrier() + pre_f = s_iscalars[4] + twf2 = s_iscalars[1] + if warp_id == twf2 and lane == cutlass.Int32(0): + base_f = pre_f + sb_star = cutlass.Int32(fbins - 1) + ra_fine = base_f + sd = cutlass.Int32(0) + for jb3 in cutlass.range_constexpr(fbpw): + sbi = ( cutlass.Int32(fbins - 1) - - lane * cutlass.Int32(fbpw) - - cutlass.Int32(jbf) + - twf2 * cutlass.Int32(fbpw) + - cutlass.Int32(jb3) ) - fls = fls + smem_hist[bif] - fpref = warp_scan(fls, tidx, lane, 32) + rank_above - fhit = cutlass.Int32(0) - if fpref >= cutlass.Int32(kK): - fhit = cutlass.Int32(1) - fm = cute.arch.vote_ballot_sync(fhit != cutlass.Int32(0)) - ftl = cutlass.Int32(32) - cutlass.Int32(cute.arch.popc(fm)) - if lane == ftl: - base_f = fpref - fls - sb_star = cutlass.Int32(fbins - 1) - ra_fine = base_f - sd = cutlass.Int32(0) - for jb3 in cutlass.range_constexpr(fbpw): - sbi = ( - cutlass.Int32(fbins - 1) - - lane * cutlass.Int32(fbpw) - - cutlass.Int32(jb3) - ) - ra_b = base_f - base_f = base_f + smem_hist[sbi] - if base_f >= cutlass.Int32(kK) and sd == cutlass.Int32(0): - sb_star = sbi - ra_fine = ra_b - sd = cutlass.Int32(1) - smem_hist[2] = sb_star - smem_hist[3] = ra_fine - s_iscalars[4] = cutlass.Int32(0) # cnt_above - s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) - s_iscalars[1] = cutlass.Int32(0) # cnt_strad (b*, sub==sb*) + ra_b = base_f + base_f = base_f + smem_hist[sbi] + if base_f >= cutlass.Int32(kK) and sd == cutlass.Int32(0): + sb_star = sbi + ra_fine = ra_b + sd = cutlass.Int32(1) + smem_hist[2] = sb_star + smem_hist[3] = ra_fine + cute.arch.barrier() + if tidx == cutlass.Int32(0): + s_iscalars[4] = cutlass.Int32(0) # cnt_above + s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) + s_iscalars[1] = cutlass.Int32(0) # cnt_strad (b*, sub==sb*) cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc4 = cute.arch.clock64() From 9bf8b0c317d7b2134f35d34b8dc37f3cc9b2b9a1 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:42:53 -0700 Subject: [PATCH 047/117] [None][perf] GVR: closed-loop adaptive block-skip gate Layer variance makes any static block-skip rule wrong on hard layers (measured 12-80% tight-line pass rates across layers on the same shape): a pass rate past ~3/8 means the gather walk reads most of the row anyway PLUS the prefix, and dense wins. Emission side: while computing block_max the epilogue also counts the records clearing the loosest line (r_bmax is warp-uniform, lane0 accumulates, the seed-count flush redux sums warps) and lands the count in packed seed row col 6 via red.global.add.f32 (packed mode only; 0 = not provided, static behavior preserved). Kernel side: the R0 multi-line count call derives a skip veto from col 6 before building the active list - vetoed rows keep the dense walk and skip the whole wasted build. pack_seed() mirrors the count from an emu block_max for harness runs. Context-repro + unified_smoke (3 cells x 10 modes) + cold 480-cell sweep all exact with the gate wired. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 50 ++++++++++-- .../blackwell/top_k/gvr_topk_decode.py | 81 ++++++++++++++----- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 13 ++- 3 files changed, 118 insertions(+), 26 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index cd5fbbaa07bc..c7b4fcaf1335 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -1075,7 +1075,7 @@ def _flush_hit_agg( hacc_cnt[t] = cutlass.Int32(0) @cute.jit - def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane): + def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None): """Warp-redux the lane-local seed counters and fire one lane-0 red.global.add per (t, threshold). Caller zero-initializes the count slots each step; cross-CTA totals accumulate atomically. @@ -1100,6 +1100,18 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane): ) * cutlass.Int64(4) _red_global_add_s32(addr, w_cnt) scnt[t * 3 + j] = cutlass.Int32(0) + if cutlass.const_expr(self.seed_packed and spass is not None): + # packed col 6: adaptive-skip pass count (lane0-accumulated, + # so the warp redux is exactly the warp's record total) + for t in cutlass.range_constexpr(next_n): + w_bp = cute.arch.warp_redux_sync(spass[t], "add") + if meta_lane == cutlass.Int32(0): + row = q_idx * cutlass.Int32(next_n) + cutlass.Int32(t) + addr = base_addr + ( + cutlass.Int64(row) * cutlass.Int64(8) + cutlass.Int64(6) + ) * cutlass.Int64(4) + _red_global_add_f32(addr, cutlass.Float32(w_bp)) + spass[t] = cutlass.Int32(0) @cute.jit def _flush_cand_window(self, mCand, q_idx, cwbase, cwleft, meta_lane): @@ -2158,6 +2170,9 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): sthr = cute.make_fragment(next_n * 3, cutlass.Float32) scnt = cute.make_fragment(next_n * 3, cutlass.Int32) + spass = cute.make_fragment(next_n, cutlass.Int32) + for _i in cutlass.range_constexpr(next_n): + spass[_i] = cutlass.Int32(0) for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) @@ -2233,7 +2248,9 @@ def kernel( # the aligned padding region out of block_max. if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: - self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + self._flush_seed_counts( + mSeedCounts, q_idx_old, scnt, meta_lane, spass=spass + ) # (re)load this q's thresholds - gated on # emit_seed_counts, NOT emit_cand: counts- # only mode needs them too (a stale @@ -2484,6 +2501,15 @@ def kernel( for _j in cutlass.range_constexpr(3): ge_j = cutlass.Int32(f32_t >= sthr[t * 3 + _j]) scnt[t * 3 + _j] = scnt[t * 3 + _j] + (ge_j & valid_i1) + if cutlass.const_expr(self.seed_packed): + # adaptive-skip pass count: one record + # per (tile, warp); r_bmax is warp- + # uniform so lane0 alone accumulates + # (the flush redux then sums warps) + if meta_lane == cutlass.Int32(0): + spass[t] = spass[t] + cutlass.Int32( + r_bmax >= sthr[t * 3 + 0] + ) if cutlass.const_expr(self.emit_cand): # L2 pre-collect at t_0 with per-warp claim # WINDOWS: refills claim (hits + CAND_WIN) @@ -2648,7 +2674,7 @@ def kernel( ) if cutlass.const_expr(self.emit_seed_counts): if q_idx < batch_size: - self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane) + self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane, spass=spass) if cutlass.const_expr(self.emit_cand): if q_idx < batch_size: self._flush_cand_window(mCand, q_idx, cwbase, cwleft, meta_lane) @@ -2706,6 +2732,9 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): sthr = cute.make_fragment(next_n * 3, cutlass.Float32) scnt = cute.make_fragment(next_n * 3, cutlass.Int32) + spass = cute.make_fragment(next_n, cutlass.Int32) + for _i in cutlass.range_constexpr(next_n): + spass[_i] = cutlass.Int32(0) for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) @@ -2781,7 +2810,9 @@ def kernel( # the aligned padding region out of block_max. if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: - self._flush_seed_counts(mSeedCounts, q_idx_old, scnt, meta_lane) + self._flush_seed_counts( + mSeedCounts, q_idx_old, scnt, meta_lane, spass=spass + ) # (re)load this q's thresholds - gated on # emit_seed_counts, NOT emit_cand (see the # WG0 twin above) @@ -3025,6 +3056,15 @@ def kernel( for _j in cutlass.range_constexpr(3): ge_j = cutlass.Int32(f32_t >= sthr[t * 3 + _j]) scnt[t * 3 + _j] = scnt[t * 3 + _j] + (ge_j & valid_i1) + if cutlass.const_expr(self.seed_packed): + # adaptive-skip pass count: one record + # per (tile, warp); r_bmax is warp- + # uniform so lane0 alone accumulates + # (the flush redux then sums warps) + if meta_lane == cutlass.Int32(0): + spass[t] = spass[t] + cutlass.Int32( + r_bmax >= sthr[t * 3 + 0] + ) if cutlass.const_expr(self.emit_cand): # L2 pre-collect at t_0 with per-warp claim # WINDOWS: refills claim (hits + CAND_WIN) @@ -3189,7 +3229,7 @@ def kernel( ) if cutlass.const_expr(self.emit_seed_counts): if q_idx < batch_size: - self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane) + self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane, spass=spass) if cutlass.const_expr(self.emit_cand): if q_idx < batch_size: self._flush_cand_window(mCand, q_idx, cwbase, cwleft, meta_lane) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index b0ca43ac5a00..b164553ecc88 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -2078,6 +2078,7 @@ def block_count_ge_multi( block_max_row=None, # block-skip: per-32-position upper bounds smem_active=None, # block-skip: int16 active list s_active_cnt=None, # block-skip: [0]=list length, [1]=list-current flag + skip_veto=None, # adaptive gate: 1 = pass rate too high, walk dense ): M = cutlass.const_expr(self.M_thr) num_threads = cutlass.const_expr(self.num_threads) @@ -2127,6 +2128,15 @@ def block_count_ge_multi( skip_ok = cutlass.Int32(0) if blk_hi_g > cutlass.Int32(32767): skip_ok = cutlass.Int32(0) + # Closed-loop adaptive gate: when the emission (or harness) + # counted the blocks clearing the loosest line, a high pass + # rate proves the gather walk would read most of the row + # anyway PLUS the prefix - dense wins. Layer variance makes + # any static rule wrong on hard layers (measured 12-80% + # pass rates across layers on the same shape). + if cutlass.const_expr(skip_veto is not None): + if skip_veto == cutlass.Int32(1): + skip_ok = cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): if skip_ok == cutlass.Int32(1): head_end = ( @@ -6459,24 +6469,59 @@ def _run_phases( ] == cutlass.Int32(1): r0_par = cutlass.Int32(1) if r0_par == cutlass.Int32(0): - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) + if cutlass.const_expr(self.use_ext_counts and self.enable_block_skip): + # adaptive skip gate from the packed seed row: + # col 6 carries the emission's count of blocks + # clearing the loosest line (0 = not provided, + # keep static behavior). Pass rates past 3/8 + # make the gather walk lose to dense. + sv_r = cutlass.Int32(0) + bp_r = cutlass.Int32(seed_thr_row[6]) + nb_r = ( + slice_end - slice_start + cutlass.Int32(self.SKIP_BLOCK - 1) + ) >> cutlass.Int32(self.SKIP_BLOCK_LOG2) + if bp_r > cutlass.Int32(0) and bp_r * cutlass.Int32( + 8 + ) > nb_r * cutlass.Int32(3): + sv_r = cutlass.Int32(1) + self.block_count_ge_multi( + input_row, + slice_start, + slice_end, + s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, + tidx, + warp_id, + lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, + skip_veto=sv_r, + ) + else: + self.block_count_ge_multi( + input_row, + slice_start, + slice_end, + s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, + tidx, + warp_id, + lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, + ) cute.arch.barrier() if tidx == 0 and r0_par == cutlass.Int32(0): # tightest admissible rung = SMALLEST count in [K, kC]. diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 268264b9b956..58ca7336b6c7 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -520,16 +520,23 @@ def derive_seed_lines_v4( return out.contiguous() -def pack_seed(seed_thr: torch.Tensor, seed_counts: torch.Tensor) -> torch.Tensor: +def pack_seed( + seed_thr: torch.Tensor, + seed_counts: torch.Tensor, + block_max: torch.Tensor = None, +) -> torch.Tensor: """Pack lines + exact counts into one [rows, 8] fp32 seed row. Lines land at [0..2], counts as floats at [3..5] (exact to 2^24); - one 32B sector per row. Build ONCE per step, outside any timed - region. + col 6 optionally carries the adaptive-skip pass count (32-grain + block records clearing t_0; 0 = not provided). One 32B sector per + row. Build ONCE per step, outside any timed region. """ pack = torch.zeros((seed_thr.shape[0], 8), dtype=torch.float32, device=seed_thr.device) pack[:, 0:3] = seed_thr pack[:, 3:6] = seed_counts.float() + if block_max is not None: + pack[:, 6] = (block_max >= seed_thr[:, 0:1]).sum(dim=1).float() return pack.contiguous() From 71d569a9c6fe815be3f9646eb98d6c26a88d2ca8 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:54:11 -0700 Subject: [PATCH 048/117] [None][fix] GVR: drop the block-skip pass-rate veto, keep col-6 count Measured dose-response on va pro-256k (cold, 30 layers, B 1/2/8/16): veto at 3/8 pass rate costs 10-18%, at 7/8 still 3-10%, removed restores parity (x1.00-1.05 vs PR tip). Root cause: the rung- tightening build already salvages fat loosest-rung rows by rebuilding at a tighter line, so a loosest-line veto only preempts a smarter in-kernel mechanism. The emission-side pass count (packed seed row col 6) stays: zero-cost, useful for host-side routing diagnostics. The remaining 256k small-B weak band tracks va's count+collect cost, not skip quality - routing sends those shapes to the list path (wf, 1.7-2.1x there). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 89 ++++++------------- 1 file changed, 26 insertions(+), 63 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index b164553ecc88..c4c021dd314a 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -2078,7 +2078,6 @@ def block_count_ge_multi( block_max_row=None, # block-skip: per-32-position upper bounds smem_active=None, # block-skip: int16 active list s_active_cnt=None, # block-skip: [0]=list length, [1]=list-current flag - skip_veto=None, # adaptive gate: 1 = pass rate too high, walk dense ): M = cutlass.const_expr(self.M_thr) num_threads = cutlass.const_expr(self.num_threads) @@ -2128,15 +2127,6 @@ def block_count_ge_multi( skip_ok = cutlass.Int32(0) if blk_hi_g > cutlass.Int32(32767): skip_ok = cutlass.Int32(0) - # Closed-loop adaptive gate: when the emission (or harness) - # counted the blocks clearing the loosest line, a high pass - # rate proves the gather walk would read most of the row - # anyway PLUS the prefix - dense wins. Layer variance makes - # any static rule wrong on hard layers (measured 12-80% - # pass rates across layers on the same shape). - if cutlass.const_expr(skip_veto is not None): - if skip_veto == cutlass.Int32(1): - skip_ok = cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): if skip_ok == cutlass.Int32(1): head_end = ( @@ -6469,59 +6459,32 @@ def _run_phases( ] == cutlass.Int32(1): r0_par = cutlass.Int32(1) if r0_par == cutlass.Int32(0): - if cutlass.const_expr(self.use_ext_counts and self.enable_block_skip): - # adaptive skip gate from the packed seed row: - # col 6 carries the emission's count of blocks - # clearing the loosest line (0 = not provided, - # keep static behavior). Pass rates past 3/8 - # make the gather walk lose to dense. - sv_r = cutlass.Int32(0) - bp_r = cutlass.Int32(seed_thr_row[6]) - nb_r = ( - slice_end - slice_start + cutlass.Int32(self.SKIP_BLOCK - 1) - ) >> cutlass.Int32(self.SKIP_BLOCK_LOG2) - if bp_r > cutlass.Int32(0) and bp_r * cutlass.Int32( - 8 - ) > nb_r * cutlass.Int32(3): - sv_r = cutlass.Int32(1) - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - skip_veto=sv_r, - ) - else: - self.block_count_ge_multi( - input_row, - slice_start, - slice_end, - s_mt_thr, - smem_ptcnt_multi, - smem_wcnt_multi, - s_mt_cnt, - s_cluster_partial_m, - do_cluster_sync, - tidx, - warp_id, - lane, - smem_ptcnt=smem_ptcnt, - block_max_row=block_max_row, - smem_active=smem_active, - s_active_cnt=s_active_cnt, - ) + # NOTE: a loosest-line pass-rate veto (packed seed + # col 6) was measured here and REMOVED: the rung- + # tightening build below already salvages fat + # loosest-rung rows by rebuilding at a tighter + # line, so any veto threshold (3/8 and 7/8 both + # tried, cold, 30 layers) only preempts that and + # costs 3-18% across the board. Col 6 stays as a + # zero-cost diagnostic for host-side routing. + self.block_count_ge_multi( + input_row, + slice_start, + slice_end, + s_mt_thr, + smem_ptcnt_multi, + smem_wcnt_multi, + s_mt_cnt, + s_cluster_partial_m, + do_cluster_sync, + tidx, + warp_id, + lane, + smem_ptcnt=smem_ptcnt, + block_max_row=block_max_row, + smem_active=smem_active, + s_active_cnt=s_active_cnt, + ) cute.arch.barrier() if tidx == 0 and r0_par == cutlass.Int32(0): # tightest admissible rung = SMALLEST count in [K, kC]. From c10ae8c906274d7454f4b091d4432b64c5212ecf Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:59:45 -0700 Subject: [PATCH 049/117] [None][feat] GVR: host-side routing module (v1 constants) One place for the deployment thresholds measured on the B200 f15 layer-complete grid: emission-tier planning (list for latency-bound long rows, counts as the near-free default, rungs closed-loop fallback) and per-shape launch knobs (block-skip attach points, GPC-aware cluster split, 512-thread small-K list rule). Constants are data, not logic - the P4 sprint moved the mid-range list-path win to 1.41-1.79x, so the list tier will likely widen once the emission tax is re-measured end-to-end (noted inline). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_routing.py | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py new file mode 100644 index 000000000000..a3bd54ca6256 --- /dev/null +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -0,0 +1,121 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & +# AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Host-side routing for the GVR top-k decode family. + +One kernel, three assist tiers selected by which emission inputs the +indexer epilogue produced for this step: + + * ``list`` - bucketed candidate list + packed seed row (v5): the + top-k pass never re-reads the row on a hit. + * ``counts`` - packed seed row only ([rows, 8]: three lines + three + counts): one filtered row pass, no in-kernel counting. + * ``rungs`` - closed-loop lines only (no emission): in-kernel + multi-line count, then the stock collect. + * ``none`` - stock kernel (v1 path). + +``plan_emission`` decides which tier the epilogue should emit for the +NEXT step (the emission tax is shape-dependent); ``pick_config`` maps +(tier, B, N, K) to concrete launch knobs for THIS step. + +All thresholds are measured on B200 (f15 layer-complete grid, +2026-07-27, cold-L2 kernel-only protocol, validated on both the shared +grid dataset and first-party captures). They are deployment defaults, +not universal truths - keep them in one place so retuning is a +constant edit, not a logic edit. +""" + +from dataclasses import dataclass +from typing import Optional + +# ---- measured thresholds (B200, f15 grid) -------------------------------- + +# Block-skip prefix pays only when whole-row reads dominate. +SKIP_MIN_N_COUNTS = 65536 # va: attach block_max unconditionally here up +SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here +SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split on top of bm from here + +# Emission tax ~ B*N on the GEMM side: the candidate list is only worth +# emitting for latency-bound shapes (small B, long rows). +LIST_EMIT_MAX_B = 16 +LIST_EMIT_MIN_N = 65536 + +# rungs-tier block_max pays only at small K: with K=1024 the tight-line +# pass rate runs too high and the prefix read is pure overhead. +RUNGS_BM_MAX_K = 512 + +# 512-thread build wins for list-hit rows at small K (work is O(list)). +SMALL_K_LIST_THREADS = 512 +SMALL_K_MAX = 512 + +# GPC packing: cs=8 only while all row-clusters fit half the device +# (B=16 x cs8 wave-spill regression); cs4/2 keep a 10% headroom. +CS8_HALF_DEVICE = 2 +CS_HEADROOM_NUM = 9 +CS_HEADROOM_DEN = 10 + + +@dataclass +class TopkRoute: + """Launch knobs for one decode step of the GVR top-k kernel.""" + + tier: str # list | counts | rungs | none + cluster_size: int = 1 + num_threads: Optional[int] = None # None = runner heuristic + attach_block_max: bool = False + + +def plan_emission(batch: int, n_comp: int, k: int, have_epilogue: bool) -> str: + """Which assist tier the indexer epilogue should emit this step. + + ``n_comp``: compressed row length (post compress_ratio) - the + top-k kernel's N. Returns the tier name; the epilogue emits the + matching buffers and the next top-k launch routes on them. + """ + if not have_epilogue: + return "rungs" # closed-loop lines cost nothing to carry + if batch <= LIST_EMIT_MAX_B and n_comp >= LIST_EMIT_MIN_N: + return "list" # latency-bound long rows: list pays big + return "counts" # near-free tax, wins almost everywhere + + +def pick_config(tier: str, batch: int, n_comp: int, k: int, num_sms: int) -> TopkRoute: + """Map (tier, B, N, K) to launch knobs. Pure function of shape.""" + r = TopkRoute(tier=tier) + if tier == "none": + return r + if tier == "list": + if k <= SMALL_K_MAX: + r.num_threads = SMALL_K_LIST_THREADS + # list + block_max: miss rows fall back to a skip-walk instead + # of a dense re-scan (measured -19% on pro long chains). + r.attach_block_max = n_comp >= SKIP_MIN_N_COUNTS + return r + if tier == "counts": + r.attach_block_max = n_comp >= SKIP_MIN_N_COUNTS + return r + # rungs (vb) + if k <= RUNGS_BM_MAX_K and n_comp >= SKIP_MIN_N_RUNGS_FLASH: + r.attach_block_max = True + if n_comp >= 65536: + if batch * 8 <= num_sms // CS8_HALF_DEVICE: + r.cluster_size = 8 + elif batch * 4 <= (num_sms * CS_HEADROOM_NUM) // CS_HEADROOM_DEN: + r.cluster_size = 4 + elif batch * 2 <= (num_sms * CS_HEADROOM_NUM) // CS_HEADROOM_DEN: + r.cluster_size = 2 + if r.attach_block_max and n_comp < SKIP_CS_MIN_N_RUNGS: + r.cluster_size = 1 # bm without cs below the split point + return r From 07cac99e219d833140438373bb13a22fc7c4f3d9 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:26:38 -0700 Subject: [PATCH 050/117] [None][feat] FP4 indexer: bucketed candidate emission (v5 SoA contract) emit_cand_bucketed: the GEMM epilogue emits the top-k consumer's bucketed list directly - three fixed SoA segments (A=[0,segA) holds >= t2, B=[segA,2segA) holds [t1,t2), C=[2segA,2segA+capC) holds [t0,t1)), full segments spilling to the next looser one. A/B use EXACT ballot claims (one lane0 atomic per hit-tile per class): their prefixes must stay pad-free because the consumer's prefix math derives lenA/lenB from the value counts alone. C keeps the claim- window scheme (pads are legal there; window tails sentinel-fill BOTH SoA columns). Every warp collective sits at the top level of the warp-uniform bound gate - no collectives inside nested dynamic branches. ctl [rows,4] = {n0 incl C pads, void, n1, n2}: n0 accumulates exact A/B placements plus C window claims, n1/n2 mirror the L1 seed counters in the same flush redux, void marks C capacity overflow (fall back to the row-level path). Cursors live in a caller- zeroed cand_cur [rows,4]. Unit test checks segment invariants, exact n1/n2, full >= t0 coverage (union of live entries, roomy caps) and void on tight caps; 4/4 parametrizations pass on B200. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 86 ++- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 547 +++++++++++++++++- .../test_cute_dsl_fp4_paged_mqa_logits.py | 171 ++++++ 3 files changed, 791 insertions(+), 13 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 0f0b12cae380..263e805a8554 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -8256,12 +8256,15 @@ def _compile(cls, emit_seed_counts=False, seed_packed=False, emit_cand=False, - cand_cap=5120): + cand_cap=5120, + emit_cand_bucketed=False, + accept_cap=8192): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, - emit_seed_counts, seed_packed, emit_cand, cand_cap) + emit_seed_counts, seed_packed, emit_cand, cand_cap, + emit_cand_bucketed, accept_cap) if key in cls.kernel_cache: return @@ -8354,6 +8357,27 @@ def _compile(cls, cutlass.Int32, (cute.sym_int(), 2), stride_order=(1, 0), assumed_align=8) + cand_idx_fake = None + cand_cur_fake = None + if emit_cand_bucketed: + # bucketed SoA: cand slot reused as the fp32 VALUES tensor + wtot = 2 * accept_cap + cand_cap + cand_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (cute.sym_int(), wtot), + stride_order=(1, 0), + assumed_align=4) + cand_idx_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), wtot), + stride_order=(1, 0), + assumed_align=4) + cand_ctl_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), 4), + stride_order=(1, 0), + assumed_align=4) + cand_cur_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), 4), + stride_order=(1, 0), + assumed_align=4) seed_thr_fake = None seed_counts_fake = None if emit_seed_counts: @@ -8400,6 +8424,8 @@ def _compile(cls, seed_packed=seed_packed, emit_cand=emit_cand, cand_cap=cand_cap, + emit_cand_bucketed=emit_cand_bucketed, + accept_cap=accept_cap, ) compiled = cute.compile( @@ -8422,6 +8448,8 @@ def _compile(cls, seed_counts=seed_counts_fake, cand=cand_fake, cand_ctl=cand_ctl_fake, + cand_idx_t=cand_idx_fake, + cand_cur=cand_cur_fake, options="--enable-tvm-ffi", ) cls.kernel_cache[key] = compiled @@ -8453,6 +8481,10 @@ def forward( emit_cand: bool = False, cand_out: Optional[torch.Tensor] = None, cand_ctl_out: Optional[torch.Tensor] = None, + emit_cand_bucketed: bool = False, + accept_cap: int = 8192, + cand_idx_out: Optional[torch.Tensor] = None, + cand_cur_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Execute FP4 paged MQA logits kernel. @@ -8628,15 +8660,54 @@ def forward( "emit_cand requires a caller-zeroed cand_ctl_out " "int32 [B*next_n, 2]") cand_cap = cand_out.shape[1] // 2 + elif emit_cand_bucketed: + assert emit_seed_counts, ( + "emit_cand_bucketed requires emit_seed_counts") + # SoA v5 contract: cand_out = fp32 VALUES [rows, 2*segA+capC], + # cand_idx_out = int32 positions (same width), cand_cur_out = + # int32 [rows, 4] cursors (caller-zeroed), cand_ctl_out = + # int32 [rows, 4] {n0, void, n1, n2} (caller-zeroed) + W = cand_out.shape[1] + assert ( + cand_out is not None and cand_out.dtype == torch.float32 + and cand_out.is_cuda and cand_out.is_contiguous() + and cand_out.dim() == 2 and cand_out.shape[0] == B * next_n + and W > 2 * accept_cap), ( + "bucketed requires cand_out fp32 [rows, 2*segA+capC]") + assert (cand_idx_out is not None + and cand_idx_out.dtype == torch.int32 + and cand_idx_out.is_cuda + and cand_idx_out.is_contiguous() + and cand_idx_out.shape == cand_out.shape), ( + "bucketed requires cand_idx_out int32, same shape") + assert (cand_ctl_out is not None + and cand_ctl_out.dtype == torch.int32 + and cand_ctl_out.is_cuda + and cand_ctl_out.is_contiguous() + and cand_ctl_out.shape == (B * next_n, 4)), ( + "bucketed requires caller-zeroed cand_ctl_out " + "int32 [rows, 4]") + assert (cand_cur_out is not None + and cand_cur_out.dtype == torch.int32 + and cand_cur_out.is_cuda + and cand_cur_out.is_contiguous() + and cand_cur_out.shape == (B * next_n, 4)), ( + "bucketed requires caller-zeroed cand_cur_out " + "int32 [rows, 4]") + cand_cap = W - 2 * accept_cap else: cand_out = None cand_ctl_out = None + if not emit_cand_bucketed: + cand_idx_out = None + cand_cur_out = None # Compile if needed (fake tensors, no real data required) key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, - emit_seed_counts, seed_packed, emit_cand, cand_cap) + emit_seed_counts, seed_packed, emit_cand, cand_cap, + emit_cand_bucketed, accept_cap) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8654,7 +8725,9 @@ def forward( emit_seed_counts=emit_seed_counts, seed_packed=seed_packed, emit_cand=emit_cand, - cand_cap=cand_cap) + cand_cap=cand_cap, + emit_cand_bucketed=emit_cand_bucketed, + accept_cap=accept_cap) compiled = cls.kernel_cache[key] # TVM FFI: pass raw tensors, no dlpack/stream needed @@ -8662,11 +8735,12 @@ def forward( compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, block_max_out, hit_stats_out, hit_bitmap, seed_thr, - seed_counts_out, cand_out, cand_ctl_out) + seed_counts_out, cand_out, cand_ctl_out, cand_idx_out, + cand_cur_out) return logits, block_max_out, hit_stats_out compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, None, - None, None, None, None, None, None) + None, None, None, None, None, None, None, None) return logits @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index c7b4fcaf1335..2481a5c5b8b3 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -472,6 +472,8 @@ def __init__( seed_packed: bool = False, emit_cand: bool = False, cand_cap: int = 5120, + emit_cand_bucketed: bool = False, + accept_cap: int = 8192, ): # Static FP4 invariants — see plan Sanity checklist. assert num_heads == 64, "FP4 kernel hardcodes num_heads=64 for TMEM/SMEM budget" @@ -576,6 +578,21 @@ def __init__( raise ValueError("emit_cand requires emit_seed_counts (t_0 source)") self.emit_cand = emit_cand self.cand_cap = cand_cap + # emit_cand_bucketed (v5 list contract): three fixed SoA segments + # (A=[0,segA) holds >= t2, B=[segA,2segA) holds [t1,t2), C= + # [2segA,2segA+capC) holds [t0,t1)); a full segment spills to the + # next looser one. A/B use EXACT ballot claims (their prefixes + # must stay pad-free - the consumer's prefix math assumes it), C + # keeps the claim-window scheme (pads are legal there). Cursors + # live in caller-zeroed cand_cur [rows,4]; ctl [rows,4] carries + # {n0 incl C pads, void, n1, n2} with n1/n2 flushed from the L1 + # seed counters. + if emit_cand_bucketed and not emit_seed_counts: + raise ValueError("emit_cand_bucketed requires emit_seed_counts") + if emit_cand_bucketed and emit_cand: + raise ValueError("emit_cand_bucketed and emit_cand are exclusive") + self.emit_cand_bucketed = emit_cand_bucketed + self.accept_cap = accept_cap # Per-warp claim window: one atomic claims (hits + CAND_WIN) slots; # subsequent hits consume the window latency-free. The unconsumed # tail is sentinel-filled (idx = -1) at q-transition/loop end, so @@ -809,6 +826,8 @@ def __call__( seed_counts: cute.Tensor = None, # [num_rows, 3] int32 out, caller-zeroed cand: cute.Tensor = None, # [num_rows, CAP*2] int32 {val bits, idx} pairs cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 {claimed, void}, zeroed + cand_idx_t: cute.Tensor = None, # bucketed: [num_rows, 2*segA+capC] int32 SoA + cand_cur: cute.Tensor = None, # bucketed: [num_rows, 4] int32 cursors, zeroed ): # Derive KV data and SF views from the fused uint8 buffer. # Fused layout per phys block: [data half_head_dim*phys_block_kv bytes] @@ -1014,6 +1033,8 @@ class SharedStorage: seed_counts, cand, cand_ctl, + cand_idx_t, + cand_cur, self.cluster_layout_vmnk, self.a_smem_layout_staged, self.b_smem_layout_staged, @@ -1075,7 +1096,7 @@ def _flush_hit_agg( hacc_cnt[t] = cutlass.Int32(0) @cute.jit - def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None): + def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None, cand_ctl=None): """Warp-redux the lane-local seed counters and fire one lane-0 red.global.add per (t, threshold). Caller zero-initializes the count slots each step; cross-CTA totals accumulate atomically. @@ -1099,6 +1120,15 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None): cutlass.Int64(row) * cutlass.Int64(3) + cutlass.Int64(j) ) * cutlass.Int64(4) _red_global_add_s32(addr, w_cnt) + if cutlass.const_expr(self.emit_cand_bucketed): + if j >= 1: + # consumer contract: ctl = {n0, void, n1, n2} + if meta_lane == cutlass.Int32(0): + row_b = q_idx * cutlass.Int32(next_n) + cutlass.Int32(t) + ctl_a = cand_ctl.iterator.toint() + ( + cutlass.Int64(row_b) * cutlass.Int64(4) + cutlass.Int64(j + 1) + ) * cutlass.Int64(4) + _red_global_add_s32(ctl_a, w_cnt) scnt[t * 3 + j] = cutlass.Int32(0) if cutlass.const_expr(self.seed_packed and spass is not None): # packed col 6: adaptive-skip pass count (lane0-accumulated, @@ -1113,6 +1143,45 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None): _red_global_add_f32(addr, cutlass.Float32(w_bp)) spass[t] = cutlass.Int32(0) + @cute.jit + def _flush_cand_window_bucketed(self, mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane): + """Sentinel-fill the unconsumed C-window tail in BOTH SoA columns + (score -inf, idx -1: the v5 consumer pads by score) and invalidate + the window. Segment C sits at base 2*segA in each row.""" + next_n = cutlass.const_expr(self.next_n) + segA_f = cutlass.const_expr(self.accept_cap) + capC_f = cutlass.const_expr(self.cand_cap) + wtot_f = cutlass.const_expr(2 * self.accept_cap + self.cand_cap) + vbase_f = mCand.iterator.toint() + ibase_f = mCandIdx.iterator.toint() + for t in cutlass.range_constexpr(next_n): + if cwleft[t] > cutlass.Int32(0): + row_f = q_idx * cutlass.Int32(next_n) + cutlass.Int32(t) + sl_f = cwbase[t] + meta_lane + if meta_lane < cwleft[t] and sl_f < cutlass.Int32(capC_f): + off_f = ( + cutlass.Int64(row_f) * cutlass.Int64(wtot_f) + + cutlass.Int64(2 * segA_f + sl_f) + ) * cutlass.Int64(4) + vp_f = cute.make_ptr( + cutlass.Float32, + vbase_f + off_f, + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vp_f, cute.make_layout((1,)))[0] = cutlass.Float32( + _META_NEG_FLT_MAX + ) + ip_f = cute.make_ptr( + cutlass.Int32, + ibase_f + off_f, + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ip_f, cute.make_layout((1,)))[0] = cutlass.Int32(-1) + cwbase[t] = cutlass.Int32(0) + cwleft[t] = cutlass.Int32(0) + @cute.jit def _flush_cand_window(self, mCand, q_idx, cwbase, cwleft, meta_lane): """Sentinel-fill the unconsumed tail of each per-(warp, t) claim @@ -1165,6 +1234,8 @@ def kernel( mSeedCounts: cute.Tensor, # [num_rows, 3] int32 counts out (or None) mCand: cute.Tensor, # [num_rows, CAP*2] int32 pair scatter (or None) mCandCtl: cute.Tensor, # [num_rows, 2] int32 {claimed, void} (or None) + mCandIdx: cute.Tensor, # bucketed: [num_rows, 2*segA+capC] int32 SoA (or None) + mCandCur: cute.Tensor, # bucketed: [num_rows, 4] int32 cursors (or None) cluster_layout_vmnk: cute.Layout, a_smem_layout_staged: cute.ComposedLayout, b_smem_layout_staged: cute.ComposedLayout, @@ -2176,7 +2247,7 @@ def kernel( for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) - if cutlass.const_expr(self.emit_cand): + if cutlass.const_expr(self.emit_cand or self.emit_cand_bucketed): cwbase = cute.make_fragment(next_n, cutlass.Int32) cwleft = cute.make_fragment(next_n, cutlass.Int32) for _i in cutlass.range_constexpr(next_n): @@ -2249,7 +2320,12 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: self._flush_seed_counts( - mSeedCounts, q_idx_old, scnt, meta_lane, spass=spass + mSeedCounts, + q_idx_old, + scnt, + meta_lane, + spass=spass, + cand_ctl=mCandCtl, ) # (re)load this q's thresholds - gated on # emit_seed_counts, NOT emit_cand: counts- @@ -2263,6 +2339,11 @@ def kernel( self._flush_cand_window( mCand, q_idx_old, cwbase, cwleft, meta_lane ) + if cutlass.const_expr(self.emit_cand_bucketed): + if q_idx_old < batch_size: + self._flush_cand_window_bucketed( + mCand, mCandIdx, q_idx_old, cwbase, cwleft, meta_lane + ) ctx_cur = mContextLens[q_idx] # Process KV block for group 0 (kv_idx + 0) @@ -2603,6 +2684,220 @@ def kernel( cute.make_tensor(iptr_c, cute.make_layout((1,)))[0] = kv_pos cwbase[t] = cwbase[t] + cnt_c cwleft[t] = cwleft[t] - cnt_c + if cutlass.const_expr(self.emit_cand_bucketed): + # v5 bucketed SoA: A/B EXACT ballot claims + # (their prefixes must stay pad-free for + # the consumer's prefix math), C keeps the + # claim-window; a full segment spills to + # the next looser one. Every warp + # collective sits at the TOP level of this + # warp-uniform bound gate - no collectives + # inside nested dynamic branches (DSL). + if r_bmax >= sthr[t * 3 + 0]: + segA_k = cutlass.const_expr(self.accept_cap) + capC_k = cutlass.const_expr(self.cand_cap) + wtot_k = cutlass.const_expr(2 * self.accept_cap + self.cand_cap) + row_k = q_idx * next_n + t + vb_k = mCand.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(wtot_k) * cutlass.Int64(4) + ib_k = mCandIdx.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(wtot_k) * cutlass.Int64(4) + cur_k = mCandCur.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(16) + ctl_k = mCandCtl.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(16) + lmk_k = ( + cutlass.Uint32(1) << cutlass.Uint32(meta_lane) + ) - cutlass.Uint32(1) + # exclusive class predicates + pA_k = cutlass.Int32(0) + pB_k = cutlass.Int32(0) + pC_k = cutlass.Int32(0) + if meta_valid: + if f32_t >= sthr[t * 3 + 2]: + pA_k = cutlass.Int32(1) + if f32_t >= sthr[t * 3 + 1] and pA_k == cutlass.Int32(0): + pB_k = cutlass.Int32(1) + if ( + f32_t >= sthr[t * 3 + 0] + and pA_k == cutlass.Int32(0) + and pB_k == cutlass.Int32(0) + ): + pC_k = cutlass.Int32(1) + # ---- A: exact claim ---- + mA_k = cute.arch.vote_ballot_sync(pA_k != cutlass.Int32(0)) + cntA_k = cutlass.Int32(cute.arch.popc(mA_k)) + offA_k = cutlass.Int32(cute.arch.popc(mA_k & lmk_k)) + baseA_k = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0) and cntA_k > cutlass.Int32(0): + baseA_k = _atom_global_add_s32(cur_k, cntA_k) + baseA_k = cute.arch.shuffle_sync(baseA_k, cutlass.Int32(0)) + slotA_k = baseA_k + offA_k + spA_k = cutlass.Int32(0) + if pA_k != cutlass.Int32(0) and slotA_k >= cutlass.Int32( + segA_k + ): + spA_k = cutlass.Int32(1) + if pA_k != cutlass.Int32(0) and slotA_k < cutlass.Int32(segA_k): + vp_k = cute.make_ptr( + cutlass.Float32, + vb_k + cutlass.Int64(slotA_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vp_k, cute.make_layout((1,)))[0] = f32_t + ip_k = cute.make_ptr( + cutlass.Int32, + ib_k + cutlass.Int64(slotA_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ip_k, cute.make_layout((1,)))[0] = kv_pos + # ---- B: exact claim (native + A spill) ---- + pBe_k = cutlass.Int32(0) + if pB_k != cutlass.Int32(0) or spA_k != cutlass.Int32(0): + pBe_k = cutlass.Int32(1) + mB_k = cute.arch.vote_ballot_sync(pBe_k != cutlass.Int32(0)) + cntB_k = cutlass.Int32(cute.arch.popc(mB_k)) + offB_k = cutlass.Int32(cute.arch.popc(mB_k & lmk_k)) + baseB_k = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0) and cntB_k > cutlass.Int32(0): + baseB_k = _atom_global_add_s32( + cur_k + cutlass.Int64(4), cntB_k + ) + baseB_k = cute.arch.shuffle_sync(baseB_k, cutlass.Int32(0)) + slotB_k = baseB_k + offB_k + spB_k = cutlass.Int32(0) + if pBe_k != cutlass.Int32(0) and slotB_k >= cutlass.Int32( + segA_k + ): + spB_k = cutlass.Int32(1) + if pBe_k != cutlass.Int32(0) and slotB_k < cutlass.Int32( + segA_k + ): + vp2_k = cute.make_ptr( + cutlass.Float32, + vb_k + + cutlass.Int64(segA_k + slotB_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vp2_k, cute.make_layout((1,)))[0] = f32_t + ip2_k = cute.make_ptr( + cutlass.Int32, + ib_k + + cutlass.Int64(segA_k + slotB_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ip2_k, cute.make_layout((1,)))[0] = kv_pos + # n0 += exact placements in A and B + plc_k = ( + cntA_k + - cutlass.Int32( + cute.arch.popc( + cute.arch.vote_ballot_sync( + spA_k != cutlass.Int32(0) + ) + ) + ) + ) + ( + cntB_k + - cutlass.Int32( + cute.arch.popc( + cute.arch.vote_ballot_sync( + spB_k != cutlass.Int32(0) + ) + ) + ) + ) + if meta_lane == cutlass.Int32(0) and plc_k > cutlass.Int32(0): + _atom_global_add_s32(ctl_k, plc_k) + # ---- C: claim window (native + B spill) ---- + pCe_k = cutlass.Int32(0) + if pC_k != cutlass.Int32(0) or spB_k != cutlass.Int32(0): + pCe_k = cutlass.Int32(1) + mC_k = cute.arch.vote_ballot_sync(pCe_k != cutlass.Int32(0)) + cntC_k = cutlass.Int32(cute.arch.popc(mC_k)) + offC_k = cutlass.Int32(cute.arch.popc(mC_k & lmk_k)) + if cntC_k > cwleft[t]: + # sentinel-fill the old window tail + # (BOTH columns: the consumer pads + # by score -inf, idx -1) + slo_k = cwbase[t] + meta_lane + if meta_lane < cwleft[t] and slo_k < cutlass.Int32(capC_k): + vpo_k = cute.make_ptr( + cutlass.Float32, + vb_k + + cutlass.Int64(2 * segA_k + slo_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vpo_k, cute.make_layout((1,)))[0] = ( + cutlass.Float32(_META_NEG_FLT_MAX) + ) + ipo_k = cute.make_ptr( + cutlass.Int32, + ib_k + + cutlass.Int64(2 * segA_k + slo_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ipo_k, cute.make_layout((1,)))[0] = ( + cutlass.Int32(-1) + ) + mC2_k = cntC_k + cutlass.Int32(self.CAND_WIN) + nbC_k = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0): + nbC_k = _atom_global_add_s32( + cur_k + cutlass.Int64(8), mC2_k + ) + _atom_global_add_s32(ctl_k, mC2_k) + if nbC_k + mC2_k > cutlass.Int32( + capC_k + ) and nbC_k <= cutlass.Int32(capC_k): + vdp_k = cute.make_ptr( + cutlass.Int32, + ctl_k + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vdp_k, cute.make_layout((1,)))[ + 0 + ] = cutlass.Int32(1) + nbC_k = cute.arch.shuffle_sync(nbC_k, cutlass.Int32(0)) + cwbase[t] = nbC_k + cwleft[t] = mC2_k + slotC_k = cwbase[t] + offC_k + if pCe_k != cutlass.Int32(0) and slotC_k < cutlass.Int32( + capC_k + ): + vpc_k = cute.make_ptr( + cutlass.Float32, + vb_k + + cutlass.Int64(2 * segA_k + slotC_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vpc_k, cute.make_layout((1,)))[0] = f32_t + ipc_k = cute.make_ptr( + cutlass.Int32, + ib_k + + cutlass.Int64(2 * segA_k + slotC_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ipc_k, cute.make_layout((1,)))[0] = kv_pos + cwbase[t] = cwbase[t] + cntC_k + cwleft[t] = cwleft[t] - cntC_k if cutlass.const_expr(self.emit_hit_stats): # Lane-local accumulation, fully BRANCHLESS # (data-dependent `if meta_hit` compiled to @@ -2674,10 +2969,17 @@ def kernel( ) if cutlass.const_expr(self.emit_seed_counts): if q_idx < batch_size: - self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane, spass=spass) + self._flush_seed_counts( + mSeedCounts, q_idx, scnt, meta_lane, spass=spass, cand_ctl=mCandCtl + ) if cutlass.const_expr(self.emit_cand): if q_idx < batch_size: self._flush_cand_window(mCand, q_idx, cwbase, cwleft, meta_lane) + if cutlass.const_expr(self.emit_cand_bucketed): + if q_idx < batch_size: + self._flush_cand_window_bucketed( + mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane + ) # Release last Q stage (WG 0) if q_idx < batch_size: @@ -2738,7 +3040,7 @@ def kernel( for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) - if cutlass.const_expr(self.emit_cand): + if cutlass.const_expr(self.emit_cand or self.emit_cand_bucketed): cwbase = cute.make_fragment(next_n, cutlass.Int32) cwleft = cute.make_fragment(next_n, cutlass.Int32) for _i in cutlass.range_constexpr(next_n): @@ -2811,7 +3113,12 @@ def kernel( if cutlass.const_expr(self.emit_seed_counts): if q_idx_old < batch_size: self._flush_seed_counts( - mSeedCounts, q_idx_old, scnt, meta_lane, spass=spass + mSeedCounts, + q_idx_old, + scnt, + meta_lane, + spass=spass, + cand_ctl=mCandCtl, ) # (re)load this q's thresholds - gated on # emit_seed_counts, NOT emit_cand (see the @@ -2824,6 +3131,11 @@ def kernel( self._flush_cand_window( mCand, q_idx_old, cwbase, cwleft, meta_lane ) + if cutlass.const_expr(self.emit_cand_bucketed): + if q_idx_old < batch_size: + self._flush_cand_window_bucketed( + mCand, mCandIdx, q_idx_old, cwbase, cwleft, meta_lane + ) ctx_cur = mContextLens[q_idx] # Process KV block for group 1 (kv_idx + 1) @@ -3158,6 +3470,220 @@ def kernel( cute.make_tensor(iptr_c, cute.make_layout((1,)))[0] = kv_pos cwbase[t] = cwbase[t] + cnt_c cwleft[t] = cwleft[t] - cnt_c + if cutlass.const_expr(self.emit_cand_bucketed): + # v5 bucketed SoA: A/B EXACT ballot claims + # (their prefixes must stay pad-free for + # the consumer's prefix math), C keeps the + # claim-window; a full segment spills to + # the next looser one. Every warp + # collective sits at the TOP level of this + # warp-uniform bound gate - no collectives + # inside nested dynamic branches (DSL). + if r_bmax >= sthr[t * 3 + 0]: + segA_k = cutlass.const_expr(self.accept_cap) + capC_k = cutlass.const_expr(self.cand_cap) + wtot_k = cutlass.const_expr(2 * self.accept_cap + self.cand_cap) + row_k = q_idx * next_n + t + vb_k = mCand.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(wtot_k) * cutlass.Int64(4) + ib_k = mCandIdx.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(wtot_k) * cutlass.Int64(4) + cur_k = mCandCur.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(16) + ctl_k = mCandCtl.iterator.toint() + cutlass.Int64( + row_k + ) * cutlass.Int64(16) + lmk_k = ( + cutlass.Uint32(1) << cutlass.Uint32(meta_lane) + ) - cutlass.Uint32(1) + # exclusive class predicates + pA_k = cutlass.Int32(0) + pB_k = cutlass.Int32(0) + pC_k = cutlass.Int32(0) + if meta_valid: + if f32_t >= sthr[t * 3 + 2]: + pA_k = cutlass.Int32(1) + if f32_t >= sthr[t * 3 + 1] and pA_k == cutlass.Int32(0): + pB_k = cutlass.Int32(1) + if ( + f32_t >= sthr[t * 3 + 0] + and pA_k == cutlass.Int32(0) + and pB_k == cutlass.Int32(0) + ): + pC_k = cutlass.Int32(1) + # ---- A: exact claim ---- + mA_k = cute.arch.vote_ballot_sync(pA_k != cutlass.Int32(0)) + cntA_k = cutlass.Int32(cute.arch.popc(mA_k)) + offA_k = cutlass.Int32(cute.arch.popc(mA_k & lmk_k)) + baseA_k = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0) and cntA_k > cutlass.Int32(0): + baseA_k = _atom_global_add_s32(cur_k, cntA_k) + baseA_k = cute.arch.shuffle_sync(baseA_k, cutlass.Int32(0)) + slotA_k = baseA_k + offA_k + spA_k = cutlass.Int32(0) + if pA_k != cutlass.Int32(0) and slotA_k >= cutlass.Int32( + segA_k + ): + spA_k = cutlass.Int32(1) + if pA_k != cutlass.Int32(0) and slotA_k < cutlass.Int32(segA_k): + vp_k = cute.make_ptr( + cutlass.Float32, + vb_k + cutlass.Int64(slotA_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vp_k, cute.make_layout((1,)))[0] = f32_t + ip_k = cute.make_ptr( + cutlass.Int32, + ib_k + cutlass.Int64(slotA_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ip_k, cute.make_layout((1,)))[0] = kv_pos + # ---- B: exact claim (native + A spill) ---- + pBe_k = cutlass.Int32(0) + if pB_k != cutlass.Int32(0) or spA_k != cutlass.Int32(0): + pBe_k = cutlass.Int32(1) + mB_k = cute.arch.vote_ballot_sync(pBe_k != cutlass.Int32(0)) + cntB_k = cutlass.Int32(cute.arch.popc(mB_k)) + offB_k = cutlass.Int32(cute.arch.popc(mB_k & lmk_k)) + baseB_k = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0) and cntB_k > cutlass.Int32(0): + baseB_k = _atom_global_add_s32( + cur_k + cutlass.Int64(4), cntB_k + ) + baseB_k = cute.arch.shuffle_sync(baseB_k, cutlass.Int32(0)) + slotB_k = baseB_k + offB_k + spB_k = cutlass.Int32(0) + if pBe_k != cutlass.Int32(0) and slotB_k >= cutlass.Int32( + segA_k + ): + spB_k = cutlass.Int32(1) + if pBe_k != cutlass.Int32(0) and slotB_k < cutlass.Int32( + segA_k + ): + vp2_k = cute.make_ptr( + cutlass.Float32, + vb_k + + cutlass.Int64(segA_k + slotB_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vp2_k, cute.make_layout((1,)))[0] = f32_t + ip2_k = cute.make_ptr( + cutlass.Int32, + ib_k + + cutlass.Int64(segA_k + slotB_k) * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ip2_k, cute.make_layout((1,)))[0] = kv_pos + # n0 += exact placements in A and B + plc_k = ( + cntA_k + - cutlass.Int32( + cute.arch.popc( + cute.arch.vote_ballot_sync( + spA_k != cutlass.Int32(0) + ) + ) + ) + ) + ( + cntB_k + - cutlass.Int32( + cute.arch.popc( + cute.arch.vote_ballot_sync( + spB_k != cutlass.Int32(0) + ) + ) + ) + ) + if meta_lane == cutlass.Int32(0) and plc_k > cutlass.Int32(0): + _atom_global_add_s32(ctl_k, plc_k) + # ---- C: claim window (native + B spill) ---- + pCe_k = cutlass.Int32(0) + if pC_k != cutlass.Int32(0) or spB_k != cutlass.Int32(0): + pCe_k = cutlass.Int32(1) + mC_k = cute.arch.vote_ballot_sync(pCe_k != cutlass.Int32(0)) + cntC_k = cutlass.Int32(cute.arch.popc(mC_k)) + offC_k = cutlass.Int32(cute.arch.popc(mC_k & lmk_k)) + if cntC_k > cwleft[t]: + # sentinel-fill the old window tail + # (BOTH columns: the consumer pads + # by score -inf, idx -1) + slo_k = cwbase[t] + meta_lane + if meta_lane < cwleft[t] and slo_k < cutlass.Int32(capC_k): + vpo_k = cute.make_ptr( + cutlass.Float32, + vb_k + + cutlass.Int64(2 * segA_k + slo_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vpo_k, cute.make_layout((1,)))[0] = ( + cutlass.Float32(_META_NEG_FLT_MAX) + ) + ipo_k = cute.make_ptr( + cutlass.Int32, + ib_k + + cutlass.Int64(2 * segA_k + slo_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ipo_k, cute.make_layout((1,)))[0] = ( + cutlass.Int32(-1) + ) + mC2_k = cntC_k + cutlass.Int32(self.CAND_WIN) + nbC_k = cutlass.Int32(0) + if meta_lane == cutlass.Int32(0): + nbC_k = _atom_global_add_s32( + cur_k + cutlass.Int64(8), mC2_k + ) + _atom_global_add_s32(ctl_k, mC2_k) + if nbC_k + mC2_k > cutlass.Int32( + capC_k + ) and nbC_k <= cutlass.Int32(capC_k): + vdp_k = cute.make_ptr( + cutlass.Int32, + ctl_k + cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vdp_k, cute.make_layout((1,)))[ + 0 + ] = cutlass.Int32(1) + nbC_k = cute.arch.shuffle_sync(nbC_k, cutlass.Int32(0)) + cwbase[t] = nbC_k + cwleft[t] = mC2_k + slotC_k = cwbase[t] + offC_k + if pCe_k != cutlass.Int32(0) and slotC_k < cutlass.Int32( + capC_k + ): + vpc_k = cute.make_ptr( + cutlass.Float32, + vb_k + + cutlass.Int64(2 * segA_k + slotC_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(vpc_k, cute.make_layout((1,)))[0] = f32_t + ipc_k = cute.make_ptr( + cutlass.Int32, + ib_k + + cutlass.Int64(2 * segA_k + slotC_k) + * cutlass.Int64(4), + cute.AddressSpace.gmem, + assumed_align=4, + ) + cute.make_tensor(ipc_k, cute.make_layout((1,)))[0] = kv_pos + cwbase[t] = cwbase[t] + cntC_k + cwleft[t] = cwleft[t] - cntC_k if cutlass.const_expr(self.emit_hit_stats): # Lane-local accumulation, fully BRANCHLESS # (data-dependent `if meta_hit` compiled to @@ -3229,10 +3755,17 @@ def kernel( ) if cutlass.const_expr(self.emit_seed_counts): if q_idx < batch_size: - self._flush_seed_counts(mSeedCounts, q_idx, scnt, meta_lane, spass=spass) + self._flush_seed_counts( + mSeedCounts, q_idx, scnt, meta_lane, spass=spass, cand_ctl=mCandCtl + ) if cutlass.const_expr(self.emit_cand): if q_idx < batch_size: self._flush_cand_window(mCand, q_idx, cwbase, cwleft, meta_lane) + if cutlass.const_expr(self.emit_cand_bucketed): + if q_idx < batch_size: + self._flush_cand_window_bucketed( + mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane + ) # Release last Q stage (WG 1) if q_idx < batch_size: diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py index 9ac8c880846a..3b7bb55b4bab 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py @@ -1579,3 +1579,174 @@ def dg_fn(data=data, dg_ctx_2d=dg_ctx_2d, q_fp4_dg=q_fp4_dg): varlen=args.varlen, block_kv=args.block_kv, ) + + +@skip_not_sm100 +@pytest.mark.parametrize("batch_size", [1, 4]) +@pytest.mark.parametrize("next_n", [1, 2]) +@pytest.mark.parametrize("avg_ctx", [4096, 4224]) +@pytest.mark.parametrize("phys_block_kv", [64, 128]) +@pytest.mark.parametrize("cap_mode", ["roomy", "tight"]) +def test_cute_dsl_fp4_paged_mqa_logits_cand_bucketed( + batch_size, + next_n, + avg_ctx, + phys_block_kv, + cap_mode, +): + """emit_cand_bucketed (v5 SoA contract): three fixed segments with + pad-free A/B prefixes and spill-to-looser, ctl {n0, void, n1, n2} + with n1/n2 mirrored from the seed counters, C-window pads carrying + score -inf / idx -1. All invariants recomputed from the kernel's + own logits.""" + from tensorrt_llm._torch.custom_ops.cute_dsl_custom_ops import CuteDSLFP4PagedMQALogitsRunner + + torch.manual_seed(23) + torch.cuda.manual_seed(23) + num_heads, head_dim = 64, 128 + max_model_len = max(avg_ctx * 2, 2048) + device = "cuda" + context_lens = torch.full((batch_size,), avg_ctx, dtype=torch.int32, device=device) + num_blocks_per_seq = ceil_div_tensor(context_lens, phys_block_kv) + num_total_blocks = int(num_blocks_per_seq.sum().item()) + batch_size * 2 + max_blocks_per_seq = int(num_blocks_per_seq.max().item()) + block_table = torch.zeros((batch_size, max_blocks_per_seq), dtype=torch.int32, device=device) + pool = torch.randperm(num_total_blocks, device=device, dtype=torch.int32) + off = 0 + for i, n_blks in enumerate(num_blocks_per_seq.tolist()): + block_table[i, :n_blks] = pool[off : off + n_blks] + off += n_blks + q = torch.randn((batch_size, next_n, num_heads, head_dim), device=device, dtype=torch.bfloat16) + kv_cache = torch.randn( + (num_total_blocks, phys_block_kv, 1, head_dim), device=device, dtype=torch.bfloat16 + ) + weights = torch.randn((batch_size * next_n, num_heads), device=device, dtype=torch.float32) + q_packed, sf_q_packed = per_token_cast_to_fp4( + q.view(-1, head_dim), use_ue8m0=True, gran_k=32, use_packed_ue8m0=True + ) + q_fp4 = q_packed.view(torch.uint8).view(batch_size, next_n, num_heads, head_dim // 2) + sf_q = sf_q_packed.view(torch.int32).view(batch_size, next_n, num_heads) + remove_online_sf_transpose = phys_block_kv == 128 + kv_fused, _ = kv_cache_cast_to_fp4( + kv_cache, remove_online_sf_transpose=remove_online_sf_transpose + ) + DG_METADATA_BLOCK_KV = 64 + num_sms = deep_gemm.get_num_sms() + schedule_meta = deep_gemm.get_paged_mqa_logits_metadata( + context_lens.unsqueeze(-1), DG_METADATA_BLOCK_KV, num_sms + ) + aligned_max_ctx = align(max_model_len, 256) + nb_pad = aligned_max_ctx // 128 + num_rows = batch_size * next_n + nan = float("nan") + block_max = torch.full((num_rows, nb_pad * 4), nan, dtype=torch.float32, device=device) + common = dict( + num_epi_subtiles=1, + epi_dtype=torch.float32, + output_dtype=torch.bfloat16, + remove_online_sf_transpose=remove_online_sf_transpose, + ) + logits0, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( + q_fp4, + sf_q, + kv_fused, + weights, + context_lens, + block_table, + schedule_meta, + max_model_len, + emit_block_meta=True, + emit_hit_stats=False, + block_max_out=block_max, + **common, + ) + torch.cuda.synchronize() + lf0 = logits0.float() + seed_row = torch.zeros((num_rows, 8), dtype=torch.float32, device=device) + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + vals = lf0[row, :ctx] + seed_row[row, 0] = torch.quantile(vals, 0.60) + seed_row[row, 1] = torch.quantile(vals, 0.90) + seed_row[row, 2] = torch.quantile(vals, 0.99) + # segment caps: roomy fits everything; tight forces A/B spill and a + # C-window void + if cap_mode == "roomy": + segA, capC = 2048, 4096 + else: + segA, capC = 32, 128 + W = 2 * segA + capC + cand_vals = torch.full((num_rows, W), nan, dtype=torch.float32, device=device) + cand_idx = torch.full((num_rows, W), -7, dtype=torch.int32, device=device) + cand_ctl = torch.zeros((num_rows, 4), dtype=torch.int32, device=device) + cand_cur = torch.zeros((num_rows, 4), dtype=torch.int32, device=device) + block_max.fill_(nan) + logits, _, _ = CuteDSLFP4PagedMQALogitsRunner.forward( + q_fp4, + sf_q, + kv_fused, + weights, + context_lens, + block_table, + schedule_meta, + max_model_len, + emit_block_meta=True, + emit_hit_stats=False, + block_max_out=block_max, + emit_seed_counts=True, + seed_thr=seed_row, + emit_cand_bucketed=True, + accept_cap=segA, + cand_out=cand_vals, + cand_idx_out=cand_idx, + cand_ctl_out=cand_ctl, + cand_cur_out=cand_cur, + **common, + ) + torch.cuda.synchronize() + lf = logits.float() + for row in range(num_rows): + ctx = int(context_lens[row // next_n].item()) + t0, t1, t2 = (float(seed_row[row, j]) for j in range(3)) + v = lf[row, :ctx] + n0_ref = int((v >= t0).sum()) + n1_ref = int((v >= t1).sum()) + n2_ref = int((v >= t2).sum()) + n0c, voidc, n1c, n2c = (int(cand_ctl[row, j]) for j in range(4)) + tag = f"row={row} caps=({segA},{capC}) refs=({n0_ref},{n1_ref},{n2_ref})" + assert n1c == n1_ref and n2c == n2_ref, f"n1/n2 mismatch {tag} got {n1c},{n2c}" + curA, curB, curC = (int(cand_cur[row, j]) for j in range(3)) + lenA = min(n2_ref, segA) + lenB = min(n1_ref - n2_ref + max(n2_ref - segA, 0), segA) + assert min(curA, segA) >= lenA or curA == n2_ref, f"curA {curA} {tag}" + # A prefix: pad-free, every entry >= t2, positions valid + unique + pa = cand_idx[row, :lenA] + va = cand_vals[row, :lenA] + assert (pa >= 0).all() and (pa < ctx).all(), f"A idx {tag}" + assert (va >= t2).all(), f"A vals {tag}" + got_a = lf[row, pa.long()] + torch.testing.assert_close(got_a, va, atol=0.0, rtol=0.0) + # B prefix: pad-free, [t1, t2) or A-spill (>= t2) + pb = cand_idx[row, segA : segA + lenB] + vb = cand_vals[row, segA : segA + lenB] + assert (pb >= 0).all() and (pb < ctx).all(), f"B idx {tag}" + assert (vb >= t1).all(), f"B vals {tag}" + torch.testing.assert_close(lf[row, pb.long()], vb, atol=0.0, rtol=0.0) + if voidc == 0: + # full coverage: union of live entries == the >= t0 set + lenC = n0c - lenA - lenB + pc = cand_idx[row, 2 * segA : 2 * segA + lenC] + vc = cand_vals[row, 2 * segA : 2 * segA + lenC] + live = pc >= 0 + assert (vc[live] >= t0).all(), f"C vals {tag}" + # pads carry -FLT_MAX (never ranks; the emu uses -inf, the + # kernel the finite sentinel - both satisfy the contract) + assert (vc[~live] <= -3e38).all(), f"C pads {tag}" + allp = torch.cat([pa, pb, pc[live]]) + assert allp.unique().numel() == allp.numel() == n0_ref, ( + f"coverage {tag}: {allp.unique().numel()} vs {n0_ref}" + ) + else: + assert cap_mode == "tight", f"unexpected void {tag}" + if cap_mode == "tight": + assert int(cand_ctl[:, 1].sum()) > 0, "tight caps never voided" From 321f8eb2c9a1e364027b0725125a7fc2d8d7a772 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:06:17 -0700 Subject: [PATCH 051/117] [None][feat] GVR: emission-assisted tiers on the production top-k op trtllm::cute_dsl_gvr_topk_decode grows the ext face the emission suite feeds: packed seed row ([rows,8] -> counts tier; [rows,3] -> closed- loop rungs tier), bucketed SoA candidate list (+ ctl) for the list tier, block_max prefix, xstate closed-loop publish, plus num_threads / accept_cap / kc_override knobs. The runner derives the mode from which tensors arrive (mirrors gvr_routing.plan_emission), compiles the matching kernel flavor (fakes and ctor mirror the validated harness wrapper, incl. the 3-rung slot layout) and applies the small-K list 512-thread rule. Ext tiers are single-CTA/sort-path only (asserted against LB). Face test: counts / list / rungs / counts+block_max all bit-exact vs torch.topk on B200 with closed-loop state published. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 132 +++++++++++++++++- 1 file changed, 129 insertions(+), 3 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 263e805a8554..bf2fa3df843b 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -6716,12 +6716,22 @@ def _compile( return_output_values: bool, cluster_size: int, seqlen_sorted: bool, + enable_block_skip: bool = False, + use_ext_counts: bool = False, + emit_xstate: bool = False, + use_ext_cand: bool = False, + ext_rungs: bool = False, + cand_cap: int = 5120, + accept_cap: Optional[int] = None, + kc_override: Optional[int] = None, ) -> tuple: key = (dtype, top_k, next_n, enable_unroll_4, enable_phase3_unroll, use_constant_hint, min_blocks_per_mp, use_256bit_load, num_threads_per_block, enable_warp_parallel_reduce, compress_ratio, return_output_values, cluster_size, - seqlen_sorted) + seqlen_sorted, enable_block_skip, use_ext_counts, + emit_xstate, use_ext_cand, ext_rungs, cand_cap, accept_cap, + kc_override) if key in cls.kernel_cache: return key n_rows = cute.sym_int() @@ -6754,6 +6764,32 @@ def _compile( order_row_fake = (cute.runtime.make_fake_compact_tensor( cutlass.Int32, (n_batch, ), stride_order=(0, )) if seqlen_sorted else None) + # emission-assisted tiers (list/counts/rungs, see + # gvr_routing): fake shapes mirror the harness wrapper + block_max_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (n_rows, cute.sym_int()), + stride_order=(1, 0), + assumed_align=16) if enable_block_skip else None) + seed_thr_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (n_rows, 8 if use_ext_counts else 3), + stride_order=(1, 0), + assumed_align=4) if (use_ext_counts or ext_rungs) else None) + xstate_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (n_rows, 8), + stride_order=(1, 0), + assumed_align=4) if emit_xstate else None) + cand_vals_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Float32, (n_rows, cand_cap), + stride_order=(1, 0), + assumed_align=4) if use_ext_cand else None) + cand_idx_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, cand_cap), + stride_order=(1, 0), + assumed_align=4) if use_ext_cand else None) + cand_ctl_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, 4), + stride_order=(1, 0), + assumed_align=8) if use_ext_cand else None) fake_stream = cute.runtime.make_fake_stream( use_tvm_ffi_env_stream=True) @@ -6772,6 +6808,18 @@ def _compile( return_output_values=return_output_values, cluster_size=cluster_size, seqlen_sorted=seqlen_sorted, + enable_block_skip=enable_block_skip, + use_ext_counts=use_ext_counts, + emit_xstate=emit_xstate, + use_ext_cand=use_ext_cand, + ext_rungs=ext_rungs, + cand_cap=cand_cap, + accept_cap=accept_cap, + kc_override=kc_override, + # ext modes need 3 rung slots (M_thr == 3); the qfrac + # VALUES are irrelevant (P1b skipped), only slot count + r0_qfracs=((0.85, 0.35) if + (use_ext_counts or ext_rungs) else None), ) cls.kernel_cache[key] = cute.compile( kernel, @@ -6782,6 +6830,13 @@ def _compile( out_indices_fake, order_row_fake, stream=fake_stream, + block_max=block_max_fake, + seed_thr=seed_thr_fake, + seed_counts=None, + xstate=xstate_fake, + cand_vals=cand_vals_fake, + cand_idx=cand_idx_fake, + cand_ctl=cand_ctl_fake, options="--enable-tvm-ffi", ) logger.debug(f"[compile cute_dsl gvr_topk_decode] {key}") @@ -6891,6 +6946,15 @@ def forward( order_row: Optional[torch.Tensor] = None, counters: Optional[torch.Tensor] = None, max_batch_size: Optional[int] = None, + seed_thr: Optional[torch.Tensor] = None, + xstate: Optional[torch.Tensor] = None, + cand_vals: Optional[torch.Tensor] = None, + cand_idx: Optional[torch.Tensor] = None, + cand_ctl: Optional[torch.Tensor] = None, + block_max: Optional[torch.Tensor] = None, + num_threads: Optional[int] = None, + accept_cap: Optional[int] = None, + kc_override: Optional[int] = None, ) -> None: """Three paths, picked by ``(counters, order_row)``: @@ -7013,6 +7077,31 @@ def forward( ), ("order_row must be int32, CUDA, shape == seq_lens.shape " f"(={tuple(seq_lens.shape)}); got dtype={order_row.dtype} " f"shape={tuple(order_row.shape)}") + # emission-assisted tiers: mode from which ext tensors the + # caller handed in (see gvr_routing.plan_emission) + use_ext_counts = seed_thr is not None and seed_thr.shape[1] >= 6 + ext_rungs = seed_thr is not None and seed_thr.shape[1] == 3 + use_ext_cand = cand_vals is not None + enable_block_skip = block_max is not None + emit_xstate = xstate is not None + if use_ext_cand: + assert use_ext_counts, ( + "candidate list requires the packed seed row " + "([rows, 8]: lines + counts)") + assert (cand_idx is not None and cand_ctl is not None + and cand_vals.shape == cand_idx.shape + and cand_ctl.shape == (num_rows, 4)), ( + "list tier needs cand_vals/cand_idx same shape " + "+ cand_ctl [rows, 4]") + if (use_ext_counts or ext_rungs or use_ext_cand + or enable_block_skip): + assert not lb_mode and order_row is None, ( + "ext tiers are single-CTA/sort-path only") + if num_threads is not None: + tuning = dict(tuning, num_threads_per_block=num_threads) + elif use_ext_cand and top_k <= 512: + # small-K list rule: hit rows do O(list) work + tuning = dict(tuning, num_threads_per_block=512) key = cls._compile( cute_dtype, top_k, @@ -7021,10 +7110,20 @@ def forward( return_output_values=return_output_values, cluster_size=cluster_size, seqlen_sorted=seqlen_sorted, + enable_block_skip=enable_block_skip, + use_ext_counts=use_ext_counts, + emit_xstate=emit_xstate, + use_ext_cand=use_ext_cand, + ext_rungs=ext_rungs, + cand_cap=(cand_vals.shape[1] if use_ext_cand else 5120), + accept_cap=accept_cap, + kc_override=kc_override, **tuning, ) cls.kernel_cache[key](logits, pre_idx, seq_lens, None, - output_indices, order_row) + output_indices, order_row, block_max, + seed_thr, None, xstate, cand_vals, cand_idx, + cand_ctl) # TODO(dsa.py): wire ``order_row = argsort(seq_lens, descending=True)`` # (device-side, graph-safe) into the LJF row-reorder branch when @@ -7033,7 +7132,7 @@ def forward( # swap. Below that threshold the win is noise / can regress a few # percent (B200 N∈{8K,16K,32K} sweep 2026-06-23). @torch.library.custom_op("trtllm::cute_dsl_gvr_topk_decode", - mutates_args=("output_indices", ), + mutates_args=("output_indices", "xstate"), device_types="cuda") def cute_dsl_gvr_topk_decode( logits: torch.Tensor, @@ -7048,6 +7147,15 @@ def cute_dsl_gvr_topk_decode( order_row: Optional[torch.Tensor] = None, counters: Optional[torch.Tensor] = None, max_batch_size: Optional[int] = None, + seed_thr: Optional[torch.Tensor] = None, + xstate: Optional[torch.Tensor] = None, + cand_vals: Optional[torch.Tensor] = None, + cand_idx: Optional[torch.Tensor] = None, + cand_ctl: Optional[torch.Tensor] = None, + block_max: Optional[torch.Tensor] = None, + num_threads: Optional[int] = None, + accept_cap: Optional[int] = None, + kc_override: Optional[int] = None, ) -> None: """CuTe DSL GVR (Guess-Verify-Refine) Top-K decode for Blackwell. @@ -7118,6 +7226,15 @@ def cute_dsl_gvr_topk_decode( order_row=order_row, counters=counters, max_batch_size=max_batch_size, + seed_thr=seed_thr, + xstate=xstate, + cand_vals=cand_vals, + cand_idx=cand_idx, + cand_ctl=cand_ctl, + block_max=block_max, + num_threads=num_threads, + accept_cap=accept_cap, + kc_override=kc_override, ) @torch.library.register_fake("trtllm::cute_dsl_gvr_topk_decode") @@ -7134,6 +7251,15 @@ def _( order_row: Optional[torch.Tensor] = None, counters: Optional[torch.Tensor] = None, max_batch_size: Optional[int] = None, + seed_thr: Optional[torch.Tensor] = None, + xstate: Optional[torch.Tensor] = None, + cand_vals: Optional[torch.Tensor] = None, + cand_idx: Optional[torch.Tensor] = None, + cand_ctl: Optional[torch.Tensor] = None, + block_max: Optional[torch.Tensor] = None, + num_threads: Optional[int] = None, + accept_cap: Optional[int] = None, + kc_override: Optional[int] = None, ) -> None: return None From 6d652af49e7ffd8b90c847666a22740f310dd336 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:57:51 -0700 Subject: [PATCH 052/117] [None][feat] DSA: emission-assisted GVR decode wiring (env opt-in) Wire the validated emission suite into the DSA decode path, gated on TRTLLM_GVR_EXT=1 (plus the cute-dsl topk + FP4 DSL indexer flags) so the default path stays byte-identical. - gvr_ext.GvrExtState: persistent stable-address buffers (packed seed row, bucketed SoA list, block_max, xstate, prev-topk feedback - the same feedback-loop shape as heuristic_prev_topk), the device-side closed-loop seed-row update (pure tensor ops, graph-capturable, 11/11-step replay-exact) and the per-step tier plan/route via gvr_routing. Cold-start rows publish non-finite lines so the kernel validity guard keeps exactness independent of host state. - fp4 paged-MQA op grows the emission face (block_max / packed seed counts / bucketed candidate list as caller-owned mutates). - dsa.py: the indexer call merges the planned emit kwargs; the decode top-k branch consumes the previous step's emission through trtllm::cute_dsl_gvr_topk_decode with warm-start from the ext feedback buffer. Scoped to next_n == 1 without atom-split; every other shape takes the unchanged paths. Verified: glue-logic CPU unit (tier transitions, kwargs shapes, cold start), fp4 DSL decode pytest regression with the flag off (4 passed), op-level tiers + emission E2E + graph replay all bit-exact from the prior commits. Real-model TP4 A/B on DeepSeek V4-Flash is the remaining acceptance run. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 70 +++++++- .../attention_backend/sparse/gvr_ext.py | 166 ++++++++++++++++++ .../_torch/custom_ops/cute_dsl_custom_ops.py | 37 +++- 3 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index fa71d2790e40..15cad0268b2f 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -1811,6 +1811,16 @@ def __init__(self, self.use_cute_dsl_paged_mqa_logits = ( sparse_params.use_cute_dsl_paged_mqa_logits and IS_CUTLASS_DSL_AVAILABLE) + # GVR emission-assisted decode (opt-in, experimental): the FP4 + # indexer epilogue emits seed counts / the bucketed candidate + # list and the top-k consumes them (see gvr_ext / gvr_routing). + # Env-gated so the default path stays byte-identical. + self.use_gvr_ext = (os.environ.get("TRTLLM_GVR_EXT", "0") == "1" + and self.use_cute_dsl_topk + and self.use_cute_dsl_paged_mqa_logits + and sparse_params.indexer_k_dtype == "fp4") + self._gvr_ext = None # lazy GvrExtState (first decode step) + self._gvr_route = None self.weight_scale_factor = self.softmax_scale * self.n_heads**-0.5 self._enable_heuristic_topk = (sparse_params.enable_heuristic_topk @@ -2766,10 +2776,35 @@ def sparse_attn_indexer( dsl_schedule_meta = ( metadata.scheduler_metadata_buffer_expanded) + gvr_emit_kwargs = {} + if (self.use_gvr_ext and next_n == 1 + and not dsl_atom_split): + from .gvr_ext import GvrExtState + if self._gvr_ext is None: + self._gvr_ext = GvrExtState( + max_rows=metadata.max_num_sequences, + top_k=self.index_topk, + device=q_fp8.device) + st = self._gvr_ext + n_comp = indexer_max_seq_len // max( + self.compress_ratio, 1) + emit_tier, self._gvr_route = st.plan( + batch_size, n_comp, + torch.cuda.get_device_properties( + q_fp8.device).multi_processor_count) + st.update_seed_rows(batch_size) + gvr_emit_kwargs = st.indexer_emit_kwargs( + emit_tier, batch_size) + if self._gvr_route.attach_block_max or emit_tier in ( + "counts", "list"): + gvr_emit_kwargs["block_max_out"] = ( + st.ensure_block_max( + indexer_max_seq_len // + max(self.compress_ratio, 1))[:batch_size]) logits_decode = torch.ops.trtllm.cute_dsl_fp4_paged_mqa_logits( dsl_q, decode_q_scale, k_cache, weights_decode, dsl_context_lens, dsl_block_table, dsl_schedule_meta, - indexer_max_seq_len) + indexer_max_seq_len, **gvr_emit_kwargs) else: # FP8 DSL kernel natively supports next_n ∈ {1, 2, 3, 4}. # Atom-split benefits small-batch / low-ntask configs by @@ -2832,7 +2867,38 @@ def sparse_attn_indexer( metadata.heuristic_scratch_values[ :num_gen_tokens] - if self.use_cute_dsl_topk and self._enable_heuristic_topk: + # CuTE DSL top-k allocates O(num_gen_tokens * kv_len) global + # memory. Beyond 256 tokens the extra memory becomes significant, + # so we cap it at 256 for now and fall back to the CUDA C++ + # indexer_topk_decode. This limit can be removed if GPU memory + # is not a bottleneck. + if (self.use_gvr_ext and self._gvr_ext is not None + and self._gvr_route is not None and next_n == 1 + and num_gen_tokens <= 256): + # emission-assisted GVR: consume what the indexer + # epilogue emitted this step (packed seed row / + # bucketed list / block_max per the picked route) + st = self._gvr_ext + out_slice = topk_indices_buffer[ + num_ctx_tokens:num_ctx_tokens + num_gen_tokens, :] + seq_1d = (context_lens if self.compress_ratio > 1 else + gen_kv_lens_cuda).reshape(-1)[:num_gen_tokens] + ext_kw = st.topk_ext_kwargs( + self._gvr_route, num_gen_tokens, + st.block_max[:num_gen_tokens] + if st.block_max is not None else None) + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits_decode, + st.prev_topk[:num_gen_tokens], + seq_1d.contiguous(), + out_slice, + self.index_topk, + next_n, + self.compress_ratio, + max_seq_len=indexer_max_seq_len, + **ext_kw) + st.prev_topk[:num_gen_tokens].copy_(out_slice) + elif self.use_cute_dsl_topk and self._enable_heuristic_topk: # GVR DSL: supports all compress_ratio and next_n values. torch.ops.trtllm.cute_dsl_gvr_topk_decode( logits_decode, diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py new file mode 100644 index 000000000000..e1ca588cc3b5 --- /dev/null +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & +# AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Emission-assisted GVR top-k state for the DSA decode path. + +Owns the persistent (graph-address-stable) buffers the emission tiers +ride on, the device-side closed-loop seed-row update (pure tensor ops: +CUDA-graph capturable, validated 11/11-step replay-exact against eager) +and the per-step routing decision. All of it is opt-in: without the +flag the DSA decode path is byte-identical to before. + +Tier semantics (see gvr_routing): + * this step's TOP-K consumes what the PREVIOUS step's indexer + epilogue emitted; + * this step's INDEXER emits what the routing planned for the NEXT + step. N changes by at most one slot per step, so tier flapping is + a non-issue. +""" + +from typing import Optional + +import torch + +from ...cute_dsl_kernels.blackwell.top_k.gvr_routing import TopkRoute, pick_config, plan_emission + +# Bucketed list geometry (validated defaults: B* = 8192 segment cap, +# 24576-entry C segment; see the f15/f17 sweeps). +LIST_SEG_A = 8192 +LIST_CAP_C = 24576 +LIST_WIDTH = 2 * LIST_SEG_A + LIST_CAP_C + +# Closed-loop line derivation around the published k-th anchor: t1 +# hugs the k-th value from below, t0/t2 guard by the (anchor - kth) +# span. Matches the graph_test.py-validated update. +GUARD_LO = 2.0 +GUARD_HI = 0.5 + + +class GvrExtState: + """Per-attention-backend emission state (persistent buffers).""" + + def __init__( + self, max_rows: int, top_k: int, device: torch.device, enable_list_tier: bool = True + ): + self.max_rows = max_rows + self.top_k = top_k + # packed seed row: lines at cols 0..2, counts (emission-filled) + # at 3..5, adaptive-skip pass count at 6 + self.seed_row = torch.zeros((max_rows, 8), dtype=torch.float32, device=device) + self.xstate = torch.zeros((max_rows, 8), dtype=torch.float32, device=device) + self.cand_vals: Optional[torch.Tensor] = None + self.cand_idx: Optional[torch.Tensor] = None + self.cand_ctl: Optional[torch.Tensor] = None + self.cand_cur: Optional[torch.Tensor] = None + if enable_list_tier: + self.cand_vals = torch.zeros((max_rows, LIST_WIDTH), dtype=torch.float32, device=device) + self.cand_idx = torch.zeros((max_rows, LIST_WIDTH), dtype=torch.int32, device=device) + self.cand_ctl = torch.zeros((max_rows, 4), dtype=torch.int32, device=device) + self.cand_cur = torch.zeros((max_rows, 4), dtype=torch.int32, device=device) + # GVR warm-start feedback: this layer's previous-step top-k + # (same stable-address feedback-loop shape as + # heuristic_prev_topk; zero-init -> first step's pre_idx points + # at index 0, a valid benign candidate) + self.prev_topk = torch.zeros((max_rows, top_k), dtype=torch.int32, device=device) + # block_max prefix ([rows, nb_pad*4] fp32 warp-partials), + # allocated lazily once max_seq_len is known + self.block_max: Optional[torch.Tensor] = None + # tier the PREVIOUS indexer call emitted (what this step's + # top-k may consume); "rungs" until the first emission lands + self.emitted_tier = "rungs" + + def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: + nb4 = ((max_seq_len + 255) // 256 * 256) // 128 * 4 + if self.block_max is None or self.block_max.shape[1] < nb4: + self.block_max = torch.zeros( + (self.max_rows, nb4), dtype=torch.float32, device=self.seed_row.device + ) + return self.block_max + + def plan(self, batch: int, n_comp: int, num_sms: int) -> tuple[str, TopkRoute]: + """Route this step: (tier to EMIT next, launch knobs to CONSUME + what was emitted last step).""" + emit_tier = plan_emission(batch, n_comp, self.top_k, have_epilogue=True) + route = pick_config(self.emitted_tier, batch, n_comp, self.top_k, num_sms) + return emit_tier, route + + def update_seed_rows(self, num_rows: int) -> None: + """Device-side closed-loop line update from the last publish. + + Pure tensor ops (graph-capturable). Rows whose xstate is not + valid (col 0 == 0, e.g. cold start) get non-finite lines, which + the kernel's validity guard routes to the stock path - the + closed loop never rides on host data quality. + """ + s = self.seed_row[:num_rows] + x = self.xstate[:num_rows] + kth = x[:, 1] + anch = torch.maximum(x[:, 2], kth + 1e-5) + span = (anch - kth).clamp_min(1e-4) + valid = x[:, 0] > 0 + inf = torch.full_like(kth, float("inf")) + s[:, 0] = torch.where(valid, kth - GUARD_LO * span, inf) + s[:, 1] = torch.where(valid, kth - 1e-6, inf) + s[:, 2] = torch.where(valid, kth + GUARD_HI * span, inf) + s[:, 3:8] = 0.0 + if self.cand_ctl is not None: + self.cand_ctl[:num_rows].zero_() + self.cand_cur[:num_rows].zero_() + + def indexer_emit_kwargs(self, emit_tier: str, num_rows: int) -> dict: + """kwargs for CuteDSLFP4PagedMQALogitsRunner.forward covering the + planned emission tier (caller merges into its call).""" + kw: dict = {} + if emit_tier in ("counts", "list"): + kw.update(emit_seed_counts=True, seed_thr=self.seed_row[:num_rows]) + if emit_tier == "list": + kw.update( + emit_cand_bucketed=True, + accept_cap=LIST_SEG_A, + cand_out=self.cand_vals[:num_rows], + cand_idx_out=self.cand_idx[:num_rows], + cand_ctl_out=self.cand_ctl[:num_rows], + cand_cur_out=self.cand_cur[:num_rows], + ) + self.emitted_tier = emit_tier + return kw + + def topk_ext_kwargs( + self, route: TopkRoute, num_rows: int, block_max: Optional[torch.Tensor] + ) -> dict: + """kwargs for trtllm::cute_dsl_gvr_topk_decode consuming the + PREVIOUS step's emission per the picked route.""" + kw: dict = { + "xstate": self.xstate[:num_rows], + "cluster_size": route.cluster_size, + } + if route.num_threads is not None: + kw["num_threads"] = route.num_threads + if route.tier in ("counts", "list"): + kw["seed_thr"] = self.seed_row[:num_rows] + # rungs tier (first step / no emission yet): pass no seed at + # all - cold-start xstate is invalid so the lines would be + # non-finite anyway; the plain stock path is the right fallback + # (a [rows, 3] column view of the packed row is non-contiguous + # and would trip the runner's contract assert) + if route.tier == "list": + kw.update( + cand_vals=self.cand_vals[:num_rows], + cand_idx=self.cand_idx[:num_rows], + cand_ctl=self.cand_ctl[:num_rows], + ) + if route.attach_block_max and block_max is not None: + kw["block_max"] = block_max + return kw diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index bf2fa3df843b..0c2ee1664a67 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -8870,7 +8870,9 @@ def forward( return logits @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", - mutates_args=(), + mutates_args=("block_max_out", "seed_thr", + "cand_out", "cand_idx_out", + "cand_ctl_out", "cand_cur_out"), device_types="cuda") def cute_dsl_fp4_paged_mqa_logits( q: torch.Tensor, @@ -8885,6 +8887,13 @@ def cute_dsl_fp4_paged_mqa_logits( epi_dtype: torch.dtype = torch.float32, output_dtype: torch.dtype = torch.float32, remove_online_sf_transpose: bool = False, + block_max_out: Optional[torch.Tensor] = None, + seed_thr: Optional[torch.Tensor] = None, + cand_out: Optional[torch.Tensor] = None, + cand_idx_out: Optional[torch.Tensor] = None, + cand_ctl_out: Optional[torch.Tensor] = None, + cand_cur_out: Optional[torch.Tensor] = None, + accept_cap: int = 8192, ) -> torch.Tensor: if not is_sm_100f(): raise ValueError( @@ -8910,7 +8919,7 @@ def cute_dsl_fp4_paged_mqa_logits( f"epi_dtype={epi_dtype} output_dtype={output_dtype}", key="cute_dsl_fp4_paged_mqa_logits_inputs", ) - return CuteDSLFP4PagedMQALogitsRunner.forward( + ret = CuteDSLFP4PagedMQALogitsRunner.forward( q, sf_q, kv_fused, @@ -8922,7 +8931,22 @@ def cute_dsl_fp4_paged_mqa_logits( num_epi_subtiles=num_epi_subtiles, epi_dtype=epi_dtype, output_dtype=output_dtype, - remove_online_sf_transpose=remove_online_sf_transpose) + remove_online_sf_transpose=remove_online_sf_transpose, + emit_block_meta=block_max_out is not None, + emit_hit_stats=False, + block_max_out=block_max_out, + emit_seed_counts=seed_thr is not None, + seed_thr=seed_thr, + emit_cand_bucketed=cand_out is not None, + accept_cap=accept_cap, + cand_out=cand_out, + cand_idx_out=cand_idx_out, + cand_ctl_out=cand_ctl_out, + cand_cur_out=cand_cur_out) + # with emission on, the runner returns (logits, block_max, + # hit_stats) - the emission buffers are caller-owned mutates, + # the op face stays logits-only + return ret[0] if isinstance(ret, tuple) else ret @torch.library.register_fake("trtllm::cute_dsl_fp4_paged_mqa_logits") def _( @@ -8938,6 +8962,13 @@ def _( epi_dtype: torch.dtype = torch.float32, output_dtype: torch.dtype = torch.float32, remove_online_sf_transpose: bool = False, + block_max_out: Optional[torch.Tensor] = None, + seed_thr: Optional[torch.Tensor] = None, + cand_out: Optional[torch.Tensor] = None, + cand_idx_out: Optional[torch.Tensor] = None, + cand_ctl_out: Optional[torch.Tensor] = None, + cand_cur_out: Optional[torch.Tensor] = None, + accept_cap: int = 8192, ) -> torch.Tensor: B = q.shape[0] next_n = q.shape[1] From 3a4f15d49bdfcc6a3dd6cb38b2d9c1efe5d86acc Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:04:00 -0700 Subject: [PATCH 053/117] [None][fix] cute-dsl ops: keep optional emission tensors out of mutates_args torch.library's in-place bookkeeping IndexErrors when a mutates_args entry is an optional tensor left at its None default - the bare default-path call of the fp4 paged-MQA op crashed outright. Follow the existing precedent (heuristic_scratch on trtllm::indexer_topk_decode is written but unlisted): drop the optional emission tensors and xstate from mutates_args on both extended ops. Bare-op default path verified bit-identical to the runner-direct call on B200. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 0c2ee1664a67..902932cf2393 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -7131,8 +7131,10 @@ def forward( # full SM-row's worth of CTAs so the sort has long-vs-short rows to # swap. Below that threshold the win is noise / can regress a few # percent (B200 N∈{8K,16K,32K} sweep 2026-06-23). + # xstate is written by the kernel but stays out of mutates_args + # (optional-mutate None-default IndexError; see the fp4 op note) @torch.library.custom_op("trtllm::cute_dsl_gvr_topk_decode", - mutates_args=("output_indices", "xstate"), + mutates_args=("output_indices", ), device_types="cuda") def cute_dsl_gvr_topk_decode( logits: torch.Tensor, @@ -8869,10 +8871,13 @@ def forward( None, None, None, None, None, None, None, None) return logits + # NOTE: the optional emission tensors ARE written by the kernel but + # deliberately not in mutates_args - torch.library's in-place + # bookkeeping IndexErrors on optional mutates left at their None + # default (same precedent as heuristic_scratch on + # trtllm::indexer_topk_decode). @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", - mutates_args=("block_max_out", "seed_thr", - "cand_out", "cand_idx_out", - "cand_ctl_out", "cand_cur_out"), + mutates_args=(), device_types="cuda") def cute_dsl_fp4_paged_mqa_logits( q: torch.Tensor, From 143453c2940f859fa53bf250650759aa08737f64 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:20:01 -0700 Subject: [PATCH 054/117] [None][fix] GVR top-k: rescue degenerate preIdx seeds instead of identity output The stock seed path derives the P2 refine bracket from the preIdx gather; its exactness invariant (count(>= gather min) >= K) only holds when the preIdx row carries K distinct in-range positions. A degenerate gather (duplicate or invalid indices) produced an unusable bracket and the old shortcut emitted identity indices [0, K) - NOT the top-K on real data. Production hit: the first decode step of every request feeds the zero-init prev_topk feedback buffer (512 copies of index 0), so every indexer layer returned identity on step 1. Real-model V4-Flash TP2 acceptance showed 42/231 dumped rows (= 21 layers x 2 sequences, first step each) with a wrong score multiset and no boundary ties. Also reachable via stale batch-slot reuse (all indices past the new row's N_eff) and an all-tied gather (FP4 quantization). Fix: P1r data reseed - when the bracket is degenerate and N > K, rebuild it from the row itself (full-row min/max, cnt_lo = N), which restores the P2 invariant unconditionally, then run the normal pipeline. A still-degenerate bracket after the rescue means every in-range value is identical (or N <= K), where identity output is provably exact - the shortcut is kept for exactly those rows. Non-degenerate rows pay nothing. Validation: real-model acceptance re-run 231/231 rows score-multiset exact (in-run selection, all layers/steps); 37-cell degenerate unit battery green; ext tiers (counts/list/rungs/counts+bm) EXACT, CUDA graph replay 11/11, bare-op default path identical. A new xfail test documents the pre-existing beyond-kC tie-flood limitation (index dups when the kth tie class alone exceeds kC; value multiset still exact; reproduces on the unmodified upstream kernel). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 90 +++++++++++- .../sparse/test_cute_dsl_gvr_topk_decode.py | 138 ++++++++++++++++++ 2 files changed, 226 insertions(+), 2 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index c4c021dd314a..8c975806e2c3 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -1088,6 +1088,68 @@ def phase1_preidx_stats( s_iscalars[4] = cutlass.Int32(0) cute.arch.barrier() + # ------------------------------------------------------------------ + # P1r — degenerate-seed rescue: rebuild the refine bracket from the + # data itself. Runs only when the preIdx gather produced an unusable + # bracket (duplicate or invalid preIdx: cold-start zero-init slots, + # stale slots pointing past N, or an all-tied gather). A full-row + # min/max restores the P2 invariant count(>= v_lo) >= K, so the + # normal pipeline stays exact; the extra row scan is paid only by + # the (rare) degenerate rows. Bounds are clamped to +-FLT_MAX/2 so + # the secant range arithmetic stays finite against inf-laden rows. + # ------------------------------------------------------------------ + @cute.jit + def phase1r_data_reseed( + self, + input_row, # cute.Tensor [N] (row-major slice of the row) + N, # runtime row length + smem_wmin_f32, # cute.Tensor [NUM_WARPS] float32 (reused P1 buffer) + smem_wmax_f32, # cute.Tensor [NUM_WARPS] float32 (reused P1 buffer) + s_thr, # cute.Tensor [3] float32: [threshold, val_lo, val_hi] + s_iscalars, # [cand_count, done, cnt_lo, cnt_hi, out_count, ...] + s_mt_thr, # rung columns (r0_vseed parks the seed line here) + tidx, + warp_id, + lane, + ): + local_min = cutlass.Float32(self.FLT_MAX) + local_max = cutlass.Float32(self.NEG_FLT_MAX) + i = cutlass.Int32(tidx) + while i < N: + v = self._load_fp32(input_row, i) + local_max = cute.arch.fmax(local_max, v) + local_min = _fmin_f32_inline(local_min, v) + i = i + cutlass.Int32(self.num_threads) + wmin = self.warp_reduce_min_f32(local_min) + wmax = self.warp_reduce_max_f32(local_max) + if lane == 0: + smem_wmin_f32[warp_id] = wmin + smem_wmax_f32[warp_id] = wmax + cute.arch.barrier() + if tidx == 0: + rmin = cutlass.Float32(self.FLT_MAX) + rmax = cutlass.Float32(self.NEG_FLT_MAX) + for w in cutlass.range_constexpr(self.num_warps): + rmax = cute.arch.fmax(rmax, smem_wmax_f32[w]) + rmin = _fmin_f32_inline(rmin, smem_wmin_f32[w]) + # finite clamp keeps rng = val_hi - val_lo representable; rows + # with mass beyond +-FLT_MAX/2 are adversarial-only (production + # indexer scores are small finite values). + rmin = cute.arch.fmax(rmin, cutlass.Float32(self.NEG_FLT_MAX * 0.5)) + rmax = _fmin_f32_inline(rmax, cutlass.Float32(self.FLT_MAX * 0.5)) + mid = (rmin + rmax) * cutlass.Float32(0.5) + s_thr[0] = mid + s_thr[1] = rmin + s_thr[2] = rmax + if cutlass.const_expr(self.r0_vseed): + s_mt_thr[self.M_thr - 1] = mid + s_iscalars[0] = cutlass.Int32(0) # cand_count + s_iscalars[1] = cutlass.Int32(0) # done + s_iscalars[2] = N # cnt_lo: count(>= row min) = N, truthful + s_iscalars[3] = cutlass.Int32(1) # cnt_hi seed (same as P1) + s_iscalars[4] = cutlass.Int32(0) # out_count + cute.arch.barrier() + # ------------------------------------------------------------------ # P1b — 256-bin SMEM histogram over the prev-topK gathered values # (band [v_lo, v_hi] = P1's pmin/pmax = s_thr[1]/s_thr[2]), then M @@ -5632,8 +5694,32 @@ def _run_phases( ) # Degenerate threshold init: val_hi <= -self.FLT_MAX or val_lo >= val_hi. - # When preIdx values produce an unusable bracket (e.g. all -inf or - # identical), skip Phase 2-4 and emit identity output instead. + # A duplicate/invalid preIdx gather (cold-start zero-init slots, stale + # slots pointing past N, an all-tied gather) produces an unusable + # bracket. When N > K real selection work remains, so rebuild the + # bracket from the data itself (P1r) and run the normal pipeline — + # the old identity shortcut here returned indices [0, K), which is + # NOT the top-K on real data (production hit: the first decode step + # of every request feeds the zero-init prev_topk feedback buffer). + # If the bracket is STILL degenerate after the rescue, every + # in-range value is identical (or N <= K), and identity output is + # then exact — keep the shortcut for exactly those rows. + v_lo = s_thr[1] + v_hi = s_thr[2] + if v_hi <= cutlass.Float32(self.NEG_FLT_MAX) or v_lo >= v_hi: + if N > cutlass.Int32(self.top_k): + self.phase1r_data_reseed( + input_row, + N, + smem_wmin, + smem_wmax, + s_thr, + s_iscalars, + s_mt_thr, + tidx, + warp_id, + lane, + ) v_lo = s_thr[1] v_hi = s_thr[2] if v_hi <= cutlass.Float32(self.NEG_FLT_MAX) or v_lo >= v_hi: diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py index f8f7215eb6ee..7a5bae65a719 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py @@ -1135,3 +1135,141 @@ def test_cute_dsl_gvr_topk_decode_launch_autoconfig(dtype, top_k, N, batch_size) _tie_aware_check(out_sec, logits, seq_lens, top_k, next_n=1, compress_ratio=1) if dtype == torch.float32: assert torch.equal(out.sort(dim=-1).values, out_sec.sort(dim=-1).values) + + +# =========================================================================== +# Degenerate preIdx states (P1r data reseed). +# +# The stock seed path derives the P2 refine bracket from the preIdx gather; +# its exactness invariant (count(>= gather min) >= K) holds only when the +# preIdx row carries K DISTINCT in-range positions. Production-reachable +# violations: the first decode step of a request feeds the zero-init +# prev_topk feedback buffer (all-duplicate index 0); a reused batch slot can +# carry stale indices past the new row's N_eff (all-invalid); FP4-quantized +# logits can tie the whole gather (zero-width bracket). The old degenerate +# shortcut emitted identity indices [0, K) — NOT the top-K on real data. +# P1r rebuilds the bracket from the row itself, so all cases must pass the +# strict multiset check. +# =========================================================================== + + +@skip_not_sm100 +@pytest.mark.parametrize( + "dtype,top_k", + [ + (torch.bfloat16, 512), + (torch.float32, 2048), + ], +) +@pytest.mark.parametrize("compress_ratio", [1, 4]) +@pytest.mark.parametrize("pre_mode", ["zero", "dup", "oob"]) +@pytest.mark.parametrize("data_mode", ["random", "all_tied", "tie_flood"]) +def test_cute_dsl_gvr_topk_decode_degenerate_preidx( + dtype, top_k, compress_ratio, pre_mode, data_mode +): + """Degenerate preIdx rows must still produce an exact top-K.""" + N = 4096 + num_rows = 4 + torch.manual_seed(3) + torch.cuda.manual_seed(3) + if data_mode == "random": + logits = (torch.randn(num_rows, N, device="cuda") * 2.0).to(dtype) + elif data_mode == "all_tied": + # rescue re-degenerates (row min == max) -> identity output, which + # is exact here because every value is identical + logits = torch.full((num_rows, N), 5.0, device="cuda", dtype=dtype) + else: # tie_flood: kth sits inside a large tie class (within kC — + # count(>= tie value) must stay under the candidate capacity; the + # beyond-kC flood is a known pre-existing limitation, see the + # xfail test below) + logits = (torch.rand(num_rows, N, device="cuda") * 0.5).to(dtype) + for r in range(num_rows): + perm = torch.randperm(N, device="cuda") + logits[r, perm[: top_k * 2]] = 1.0 + logits[r, perm[top_k * 2 : top_k * 2 + top_k // 4]] = 2.0 + + if pre_mode == "zero": + pre_idx = torch.zeros(num_rows, top_k, dtype=torch.int32, device="cuda") + elif pre_mode == "dup": + pre_idx = torch.full((num_rows, top_k), 37, dtype=torch.int32, device="cuda") + else: # oob: every slot past N_eff (stale-slot state, pcnt == 0) + pre_idx = torch.full((num_rows, top_k), N + 7, dtype=torch.int32, device="cuda") + + seq_lens = torch.full((num_rows,), N * compress_ratio, dtype=torch.int32, device="cuda") + out_indices = torch.empty(num_rows, top_k, dtype=torch.int32, device="cuda") + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, + pre_idx, + seq_lens, + out_indices, + top_k=top_k, + next_n=1, + compress_ratio=compress_ratio, + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1, compress_ratio=compress_ratio) + + +@skip_not_sm100 +def test_cute_dsl_gvr_topk_decode_degenerate_preidx_cs4(): + """cs>1 rows run the rescue per-CTA (redundant full-row scan) — the + cluster path must stay exact for the zero-init cold-start state too.""" + N = 65536 + top_k = 512 + num_rows = 2 + torch.manual_seed(5) + logits = (torch.randn(num_rows, N, device="cuda") * 2.0).to(torch.bfloat16) + pre_idx = torch.zeros(num_rows, top_k, dtype=torch.int32, device="cuda") + seq_lens = torch.full((num_rows,), N, dtype=torch.int32, device="cuda") + out_indices = torch.empty(num_rows, top_k, dtype=torch.int32, device="cuda") + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, + pre_idx, + seq_lens, + out_indices, + top_k=top_k, + next_n=1, + compress_ratio=1, + cluster_size=4, + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1, compress_ratio=1) + + +@skip_not_sm100 +@pytest.mark.xfail( + reason="known pre-existing limitation (upstream lineage, reproduces on " + "the PR-tip kernel unmodified): when the kth tie class alone exceeds " + "the candidate capacity kC, no threshold lands in [K, kC]; the selected " + "VALUE multiset is still exact but the index list can contain " + "duplicate / unwritten (-1) slots. Requires >kC exactly-equal scores " + "at the boundary — unreachable for real FP4 indexer logits observed " + "so far. Tracked as a follow-up; independent of the P1r rescue.", + strict=False, +) +def test_cute_dsl_gvr_topk_decode_tie_flood_beyond_capacity(): + N = 4096 + top_k = 512 + num_rows = 4 + torch.manual_seed(3) + torch.cuda.manual_seed(3) + logits = torch.ones(num_rows, N, device="cuda", dtype=torch.bfloat16) + for r in range(num_rows): + hot = torch.randperm(N, device="cuda")[: top_k // 4] + logits[r, hot] = 2.0 + # healthiest possible pre (true previous top-k) — the flood defect is + # independent of preIdx quality + pre_idx = torch.topk(logits.float(), top_k, dim=-1).indices.int().contiguous() + seq_lens = torch.full((num_rows,), N, dtype=torch.int32, device="cuda") + out_indices = torch.empty(num_rows, top_k, dtype=torch.int32, device="cuda") + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, + pre_idx, + seq_lens, + out_indices, + top_k=top_k, + next_n=1, + compress_ratio=1, + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1, compress_ratio=1) From 5390d7f083c7f2c269df69319cf147f169a0a09e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:20:46 -0700 Subject: [PATCH 055/117] [None][fix] DSA GVR ext wiring: live seq domains, block_max width, dump hook Fixes found during real-model (DeepSeek V4-Flash TP2) bring-up of the emission-assisted GVR decode path: - The GVR op takes RAW-domain seq_lens; on the DSL indexer path the live compressed lengths are in gen_indexer_kv_lens_cuda_runtime (kv_lens_cuda_2d stays zero there), so multiply those by compress_ratio instead of passing context_lens. - Same trap on the radix baseline branch: pass the live compressed lens reshaped to 1-D (the op requires ndim == 1; the old context_lens path handed it a [B, 1] view and cr > 1 runs died). - ensure_block_max allocates the exact validated width (the runner asserts shape equality, a wider reused buffer trips it). - topk_ext_kwargs emits tensors only; the Runner-level mode booleans are derived inside the op from tensor presence. - TRTLLM_GVR_DUMP diagnostic hook (graph-free runs only): saves logits / raw seq lens / pre-op prev_topk / the in-run selection per layer so an offline pass can verify score-multiset exactness of the exact production path; guarded to skip warmup rows. Acceptance with all of the above plus the P1r kernel rescue: 231/231 dumped rows (21 layers x all captured steps x 2 sequences) score- multiset exact vs torch.topk on the in-run selection. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 53 ++++++++++++++++--- .../attention_backend/sparse/gvr_ext.py | 7 +-- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 15cad0268b2f..7d0dd651976f 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2799,8 +2799,7 @@ def sparse_attn_indexer( "counts", "list"): gvr_emit_kwargs["block_max_out"] = ( st.ensure_block_max( - indexer_max_seq_len // - max(self.compress_ratio, 1))[:batch_size]) + indexer_max_seq_len)[:batch_size]) logits_decode = torch.ops.trtllm.cute_dsl_fp4_paged_mqa_logits( dsl_q, decode_q_scale, k_cache, weights_decode, dsl_context_lens, dsl_block_table, dsl_schedule_meta, @@ -2881,8 +2880,18 @@ def sparse_attn_indexer( st = self._gvr_ext out_slice = topk_indices_buffer[ num_ctx_tokens:num_ctx_tokens + num_gen_tokens, :] - seq_1d = (context_lens if self.compress_ratio > 1 else - gen_kv_lens_cuda).reshape(-1)[:num_gen_tokens] + # the GVR op takes RAW-domain seq_lens (its kernel + # ceil-divides by compress_ratio internally). On the + # DSL indexer path the live compressed lengths are in + # gen_indexer_kv_lens_cuda_runtime (kv_lens_cuda_2d + # stays zero there - same trap as the indexer call). + if self.compress_ratio > 1: + gvr_lens = metadata.gen_indexer_kv_lens_cuda_runtime + assert gvr_lens is not None + seq_1d = (gvr_lens.reshape(-1)[:num_gen_tokens] * + self.compress_ratio) + else: + seq_1d = gen_kv_lens_cuda.reshape(-1)[:num_gen_tokens] ext_kw = st.topk_ext_kwargs( self._gvr_route, num_gen_tokens, st.block_max[:num_gen_tokens] @@ -2897,6 +2906,27 @@ def sparse_attn_indexer( self.compress_ratio, max_seq_len=indexer_max_seq_len, **ext_kw) + # diagnostic dump (NO_GRAPH runs only): capture the + # inputs AND the in-run selection so an offline pass + # can check score-multiset equality vs torch.topk on + # the exact production path. prev_topk still holds + # the pre-op value here (copy-back is below). The + # min-seq guard skips warmup/dummy rows (n < K). + if (os.environ.get("TRTLLM_GVR_DUMP") + and getattr(self, "_gvr_dumped", 0) < 6 + and int(seq_1d.min()) + >= self.index_topk * self.compress_ratio): + self._gvr_dumped = getattr(self, "_gvr_dumped", 0) + 1 + torch.save( + { + "logits": logits_decode.clone().cpu(), + "seq_raw": seq_1d.clone().cpu(), + "pre": + st.prev_topk[:num_gen_tokens].clone().cpu(), + "sel": out_slice.clone().cpu(), + "layer": self.layer_idx, + }, f"/tmp/siyid_e2e/dump_l{self.layer_idx}_" + f"{self._gvr_dumped}.pt") st.prev_topk[:num_gen_tokens].copy_(out_slice) elif self.use_cute_dsl_topk and self._enable_heuristic_topk: # GVR DSL: supports all compress_ratio and next_n values. @@ -2917,9 +2947,20 @@ def sparse_attn_indexer( # significant, so we cap it at 256 and fall back to C++. elif (self.use_cute_dsl_topk and num_gen_tokens <= 256 and (self.compress_ratio == 1 or next_n == 1)): + # request-level seq_lens must be 1-D; on the DSL + # indexer path the live compressed lengths are in + # gen_indexer_kv_lens_cuda_runtime (kv_lens_cuda_2d + # stays zero there) + if self.compress_ratio > 1: + radix_lens = (metadata.gen_indexer_kv_lens_cuda_runtime + if self.use_cute_dsl_paged_mqa_logits and + metadata.gen_indexer_kv_lens_cuda_runtime + is not None else context_lens) + radix_lens = radix_lens.reshape(-1) + else: + radix_lens = gen_kv_lens_cuda torch.ops.trtllm.cute_dsl_indexer_topk_decode( - logits_decode, context_lens - if self.compress_ratio > 1 else gen_kv_lens_cuda, + logits_decode, radix_lens, topk_indices_buffer[num_ctx_tokens:num_ctx_tokens + num_gen_tokens, :], self.index_topk, next_n) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index e1ca588cc3b5..da2a5fff3beb 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -83,7 +83,9 @@ def __init__( def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: nb4 = ((max_seq_len + 255) // 256 * 256) // 128 * 4 - if self.block_max is None or self.block_max.shape[1] < nb4: + # exact width: the runner asserts shape == (rows, nrec), so a + # wider reused buffer would trip it + if self.block_max is None or self.block_max.shape[1] != nb4: self.block_max = torch.zeros( (self.max_rows, nb4), dtype=torch.float32, device=self.seed_row.device ) @@ -124,10 +126,9 @@ def indexer_emit_kwargs(self, emit_tier: str, num_rows: int) -> dict: planned emission tier (caller merges into its call).""" kw: dict = {} if emit_tier in ("counts", "list"): - kw.update(emit_seed_counts=True, seed_thr=self.seed_row[:num_rows]) + kw["seed_thr"] = self.seed_row[:num_rows] if emit_tier == "list": kw.update( - emit_cand_bucketed=True, accept_cap=LIST_SEG_A, cand_out=self.cand_vals[:num_rows], cand_idx_out=self.cand_idx[:num_rows], From af9df05683be3b07f99162e917f1aa78e0445abe Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:18:45 -0700 Subject: [PATCH 056/117] [None][fix] GVR: xstate anchor visibility barrier + defensive contract checks Review follow-ups (coderabbit round 1): - List rows publish the closed-loop anchor by reading output_indices_row[K-1], written by peer threads in rank-scatter / tail repair; the eager-position path dropped the swap loop's trailing block barrier, so the read could race the last writes. Publish visibility explicitly with a barrier gated on list_used (uniform: admission is decided from shared control words). Counts/stock rows pay nothing. - GvrExtState.plan() demotes the list tier to counts when the state was constructed without candidate buffers (enable_list_tier=False previously routed into a None subscript). - ensure_block_max() raises instead of silently allocating inside CUDA graph capture (a captured dangling address would corrupt replays). - The TRTLLM_GVR_DUMP diagnostic hook takes the dump directory from the env value instead of a hard-coded path. - The op face rejects unexpected seed_thr widths (4/5 silently disabled both ext modes) and checks cand_out presence before reading its shape. - Debug env knobs parse malformed values without raising at import. - tie_flood unit case sizes its tie class as 1.5*K so it stays a genuine flood at top_k=2048 (2*K covered the whole N=4096 row and collapsed into all_tied). Validation: unified smoke 30/30 exact (list path exercises the new barrier), degenerate/flood battery 37 passed + 1 xfail, ext tiers EXACT, CUDA graph replay 11/11, bare-op path identical, GvrExtState CPU unit incl. the new demotion case. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 12 ++++++---- .../attention_backend/sparse/gvr_ext.py | 14 +++++++++++ .../_torch/custom_ops/cute_dsl_custom_ops.py | 13 ++++++++--- .../blackwell/top_k/gvr_topk_decode.py | 23 ++++++++++++++++--- .../sparse/test_cute_dsl_gvr_topk_decode.py | 9 ++++++-- 5 files changed, 59 insertions(+), 12 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 7d0dd651976f..d3ca287eba94 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2912,10 +2912,12 @@ def sparse_attn_indexer( # the exact production path. prev_topk still holds # the pre-op value here (copy-back is below). The # min-seq guard skips warmup/dummy rows (n < K). - if (os.environ.get("TRTLLM_GVR_DUMP") - and getattr(self, "_gvr_dumped", 0) < 6 + # TRTLLM_GVR_DUMP holds the target directory. + gvr_dump_dir = os.environ.get("TRTLLM_GVR_DUMP") + if (gvr_dump_dir and getattr(self, "_gvr_dumped", 0) < 6 and int(seq_1d.min()) >= self.index_topk * self.compress_ratio): + os.makedirs(gvr_dump_dir, exist_ok=True) self._gvr_dumped = getattr(self, "_gvr_dumped", 0) + 1 torch.save( { @@ -2925,8 +2927,10 @@ def sparse_attn_indexer( st.prev_topk[:num_gen_tokens].clone().cpu(), "sel": out_slice.clone().cpu(), "layer": self.layer_idx, - }, f"/tmp/siyid_e2e/dump_l{self.layer_idx}_" - f"{self._gvr_dumped}.pt") + }, + os.path.join( + gvr_dump_dir, f"dump_l{self.layer_idx}_" + f"{self._gvr_dumped}.pt")) st.prev_topk[:num_gen_tokens].copy_(out_slice) elif self.use_cute_dsl_topk and self._enable_heuristic_topk: # GVR DSL: supports all compress_ratio and next_n values. diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index da2a5fff3beb..0611bbd10df7 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -86,6 +86,16 @@ def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: # exact width: the runner asserts shape == (rows, nrec), so a # wider reused buffer would trip it if self.block_max is None or self.block_max.shape[1] != nb4: + # max_seq_len is engine-static, so this allocates once on the + # first (eager warmup) step; allocating inside CUDA graph + # capture would bake a dangling address into the graph, so + # fail loudly instead of corrupting the capture. + if torch.cuda.is_current_stream_capturing(): + raise RuntimeError( + "GvrExtState.ensure_block_max: (re)allocation requested " + "during CUDA graph capture; the block_max buffer must be " + "created by a warmup step before capture" + ) self.block_max = torch.zeros( (self.max_rows, nb4), dtype=torch.float32, device=self.seed_row.device ) @@ -95,6 +105,10 @@ def plan(self, batch: int, n_comp: int, num_sms: int) -> tuple[str, TopkRoute]: """Route this step: (tier to EMIT next, launch knobs to CONSUME what was emitted last step).""" emit_tier = plan_emission(batch, n_comp, self.top_k, have_epilogue=True) + if emit_tier == "list" and self.cand_vals is None: + # constructed with enable_list_tier=False: no candidate + # buffers to emit into, demote to the counts tier + emit_tier = "counts" route = pick_config(self.emitted_tier, batch, n_comp, self.top_k, num_sms) return emit_tier, route diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 902932cf2393..a2d6a5e09a2b 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -7079,6 +7079,11 @@ def forward( f"shape={tuple(order_row.shape)}") # emission-assisted tiers: mode from which ext tensors the # caller handed in (see gvr_routing.plan_emission) + if seed_thr is not None: + assert seed_thr.shape[1] == 3 or seed_thr.shape[1] >= 6, ( + "seed_thr width must be 3 (rungs) or >= 6 (packed " + f"counts row); got {seed_thr.shape[1]} - refusing to " + "silently ignore the seed") use_ext_counts = seed_thr is not None and seed_thr.shape[1] >= 6 ext_rungs = seed_thr is not None and seed_thr.shape[1] == 3 use_ext_cand = cand_vals is not None @@ -8795,11 +8800,13 @@ def forward( # cand_idx_out = int32 positions (same width), cand_cur_out = # int32 [rows, 4] cursors (caller-zeroed), cand_ctl_out = # int32 [rows, 4] {n0, void, n1, n2} (caller-zeroed) + assert cand_out is not None, ( + "emit_cand_bucketed requires cand_out") W = cand_out.shape[1] assert ( - cand_out is not None and cand_out.dtype == torch.float32 - and cand_out.is_cuda and cand_out.is_contiguous() - and cand_out.dim() == 2 and cand_out.shape[0] == B * next_n + cand_out.dtype == torch.float32 and cand_out.is_cuda + and cand_out.is_contiguous() and cand_out.dim() == 2 + and cand_out.shape[0] == B * next_n and W > 2 * accept_cap), ( "bucketed requires cand_out fp32 [rows, 2*segA+capC]") assert (cand_idx_out is not None diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 8c975806e2c3..10de11addfc8 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -42,14 +42,21 @@ from ..utils import TRTLLM_ENABLE_PDL, griddepcontrol_launch_dependents, griddepcontrol_wait from .block_scan import warp_scan + +def _env_flag(name: str) -> bool: + """Debug-knob parse that never raises at import time: any of + ""/0/false/off/no (case-insensitive) is False, everything else True.""" + return os.environ.get(name, "0").strip().lower() not in ("", "0", "false", "off", "no") + + # Diagnostic knob: compile per-phase clock64 stamps of the list path # into the spare xstate slots (harness-side analysis). Off by default; # NEVER set in production. -_P4_TAIL_DBG = bool(int(os.environ.get("GVR_P4_TAIL_DBG", "0"))) +_P4_TAIL_DBG = _env_flag("GVR_P4_TAIL_DBG") # P4 sub-phase clock64 breakdown -> xstate[1,2,4,5,6,7] (debug: clobbers # the closed-loop thr/anch publish; single-shot cells only, not chains) -_P4_SUB_DBG = bool(int(os.environ.get("GVR_P4_SUB_DBG", "0"))) -_SKIP_DBG = bool(int(os.environ.get("GVR_SKIP_DBG", "0"))) +_P4_SUB_DBG = _env_flag("GVR_P4_SUB_DBG") +_SKIP_DBG = _env_flag("GVR_SKIP_DBG") # --------------------------------------------------------------------------- @@ -6960,6 +6967,16 @@ def _run_phases( # tight lower bound of the true kth), [2] accepted # threshold, [3] cand_count. The next step derives its # seed rung group from these. + if list_used == cutlass.Int32(1): + # the anchor below reads output_indices_row[K-1], + # written by peer threads in rank-scatter / tail + # repair; not every exit of that phase ends in a + # block barrier (the eager-position path dropped + # the swap loop's trailing one), so publish + # visibility explicitly. list_used is uniform + # (admission is decided from shared control + # words), so the barrier is block-safe. + cute.arch.barrier() if tidx == cutlass.Int32(0): xstate_row[0] = cutlass.Float32(1.0) thr_pub = s_thr[0] diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py index 7a5bae65a719..ea382f8b4145 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py @@ -1183,10 +1183,15 @@ def test_cute_dsl_gvr_topk_decode_degenerate_preidx( # beyond-kC flood is a known pre-existing limitation, see the # xfail test below) logits = (torch.rand(num_rows, N, device="cuda") * 0.5).to(dtype) + # tie class sized to 1.5*K so the case stays a genuine flood for + # every top_k parametrization (2*K would cover the whole row at + # top_k=2048/N=4096 and collapse into all_tied) while + # count(>= 1.0) = 1.75*K stays inside the candidate capacity + tie_n = top_k + top_k // 2 for r in range(num_rows): perm = torch.randperm(N, device="cuda") - logits[r, perm[: top_k * 2]] = 1.0 - logits[r, perm[top_k * 2 : top_k * 2 + top_k // 4]] = 2.0 + logits[r, perm[:tie_n]] = 1.0 + logits[r, perm[tie_n : tie_n + top_k // 4]] = 2.0 if pre_mode == "zero": pre_idx = torch.zeros(num_rows, top_k, dtype=torch.int32, device="cuda") From 58ebe349f14ef750ae5b08592aaf63dc6e01db35 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 01:50:13 -0700 Subject: [PATCH 057/117] [None][fix] test: seed-row col 6 is the adaptive-skip pass count, not a stray write The packed seed-row contract grew a column after this assertion was written: the emission epilogue publishes the adaptive-skip pass count into col 6 (lane0-accumulated; the top-k consumer reads it when block_max rides along), and this test always emits block_max, so col 6 is a legitimate output on every packed case. The stray-write guard now covers only col 7, and col 6 gets a semantic bound check instead (0 <= pass count <= block-max record count). Fixes the 96 seed_counts[True-...] CI failures on B300/DGX_B200 (pipeline #50383). Full-file validation on B200: 1066 passed (emission), 708 passed + 1 xfailed (top-k). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../sparse/test_cute_dsl_fp4_paged_mqa_logits.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py index 3b7bb55b4bab..7f0142af1fb8 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py @@ -906,7 +906,15 @@ def test_cute_dsl_fp4_paged_mqa_logits_seed_counts( torch.testing.assert_close(lf[row, :ctx], lf0[row, :ctx], atol=0.0, rtol=0.0) if packed: assert torch.equal(seed_row[:, 0:3], seed_thr), "lines clobbered" - assert (seed_row[:, 6:8] == 0).all(), "stray write past counts" + # col 6 carries the adaptive-skip pass count (lane0-accumulated + # diagnostic; the top-k consumer reads it when block_max rides + # along), so it is a legitimate output here - bounded by the + # block-max record count. Only col 7 must stay untouched. + assert (seed_row[:, 7] == 0).all(), "stray write past counts" + nrec = block_max.shape[1] + assert ((seed_row[:, 6] >= 0) & (seed_row[:, 6] <= nrec)).all(), ( + "adaptive-skip pass count out of range" + ) seed_counts = seed_row[:, 3:6].to(torch.int32) for row in range(num_rows): ctx = int(context_lens[row // next_n].item()) From 8af7c59e9e87deb9e5a5c3f71793f788ef4fc3e4 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:32:47 -0700 Subject: [PATCH 058/117] [None][perf] Fused indexer->top-k handshake: PDL early release + per-row arrival gating Eliminates the kernel-boundary tax between the FP4 indexer and the GVR top-k (measured: launch staging + consumer cold prologue + the global barrier that blocks per-row pipelining; PDL alone recovers only 0.2-1.5us of it because the indexer never triggered early). Producer (fp4_paged_mqa_logits, emit_arrival mode): - every CTA calls griddepcontrol.launch_dependents right after reading its schedule slice - the dependent top-k grid stages onto SMs as producer CTAs retire, and data safety moves entirely to the per-row arrival counters; - each of the 8 math warps counts the splits it processed for a row and publishes the count at the existing q-transition / loop-exit flush points with sync_warp + fence.acq_rel.gpu + red.relaxed.gpu (warp-sync cumulativity orders every lane's logits stores and emission atomics before the counter add). Consumer (gvr_topk_decode, handshake mode): - after row-view setup, thread 0 spins on ld.global.acquire.gpu until the row's counter reaches 8 * ceil(num_kv_tiles / 2), then resets it with st.release for the next layer's reuse (the next indexer cannot race the reset: its launch is chained behind this kernel's trailing launch_dependents); - MVP scope pins next_n == 1 and cluster_size == 1 (ctor-enforced). Wiring: opt-in via TRTLLM_GVR_FUSE=1 on top of TRTLLM_GVR_EXT; the arrival buffer lives in GvrExtState (zero-init once, consumer self-resets); the DSA call site only engages the handshake when BOTH sides of the step run an ext tier at cluster_size 1 - the first step emits counts while its top-k still routes rungs, and emitting arrival there would leave residue that un-gates the next step early. Validation: three-cell functional battery PASS x4 iterations (exact vs torch.topk, fused == unfused as index sets, counters self-reset); CUDA-graph capture/replay 8/8; real-model DeepSeek V4-Flash TP2 with TRTLLM_GVR_FUSE=1: 231/231 dumped rows score-multiset exact, graph mode runs clean. Non-fused regression untouched: degenerate battery 37 passed + 1 xfailed, ext tiers EXACT, bare-op path identical. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 26 ++++++- .../attention_backend/sparse/gvr_ext.py | 13 +++- .../_torch/custom_ops/cute_dsl_custom_ops.py | 58 +++++++++++--- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 76 +++++++++++++++++++ .../blackwell/top_k/gvr_topk_decode.py | 29 +++++++ 5 files changed, 191 insertions(+), 11 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index d3ca287eba94..6b520a5b19da 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -1819,6 +1819,12 @@ def __init__(self, and self.use_cute_dsl_topk and self.use_cute_dsl_paged_mqa_logits and sparse_params.indexer_k_dtype == "fp4") + # fused-handshake mode (TRTLLM_GVR_FUSE=1, requires GVR_EXT): the + # indexer emits per-row arrival counters and releases the top-k + # grid early via PDL; the top-k spin-gates each row on its counter + # instead of waiting for the whole indexer grid. + self.use_gvr_fuse = (os.environ.get("TRTLLM_GVR_FUSE", "0") == "1" + and self.use_gvr_ext) self._gvr_ext = None # lazy GvrExtState (first decode step) self._gvr_route = None self.weight_scale_factor = self.softmax_scale * self.n_heads**-0.5 @@ -2784,7 +2790,8 @@ def sparse_attn_indexer( self._gvr_ext = GvrExtState( max_rows=metadata.max_num_sequences, top_k=self.index_topk, - device=q_fp8.device) + device=q_fp8.device, + enable_fused_handshake=self.use_gvr_fuse) st = self._gvr_ext n_comp = indexer_max_seq_len // max( self.compress_ratio, 1) @@ -2800,6 +2807,19 @@ def sparse_attn_indexer( gvr_emit_kwargs["block_max_out"] = ( st.ensure_block_max( indexer_max_seq_len)[:batch_size]) + # fused handshake only when BOTH sides of this + # step run an ext tier: the first step emits + # counts but its top-k still routes "rungs" and + # would never consume/reset the counters - + # skipping emission keeps them clean + self._gvr_emitted_arrival = ( + st.arrival is not None + and emit_tier in ("counts", "list") + and self._gvr_route.tier in ("counts", "list") + and self._gvr_route.cluster_size == 1) + if self._gvr_emitted_arrival: + gvr_emit_kwargs["arrival_out"] = ( + st.arrival[:batch_size]) logits_decode = torch.ops.trtllm.cute_dsl_fp4_paged_mqa_logits( dsl_q, decode_q_scale, k_cache, weights_decode, dsl_context_lens, dsl_block_table, dsl_schedule_meta, @@ -2896,6 +2916,10 @@ def sparse_attn_indexer( self._gvr_route, num_gen_tokens, st.block_max[:num_gen_tokens] if st.block_max is not None else None) + if getattr(self, "_gvr_emitted_arrival", False): + # fused handshake: gate each row on this step's + # indexer arrival counter (consumer resets it) + ext_kw["arrival"] = st.arrival[:num_gen_tokens] torch.ops.trtllm.cute_dsl_gvr_topk_decode( logits_decode, st.prev_topk[:num_gen_tokens], diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index 0611bbd10df7..ed3b30e5a17b 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -52,7 +52,12 @@ class GvrExtState: """Per-attention-backend emission state (persistent buffers).""" def __init__( - self, max_rows: int, top_k: int, device: torch.device, enable_list_tier: bool = True + self, + max_rows: int, + top_k: int, + device: torch.device, + enable_list_tier: bool = True, + enable_fused_handshake: bool = False, ): self.max_rows = max_rows self.top_k = top_k @@ -80,6 +85,12 @@ def __init__( # tier the PREVIOUS indexer call emitted (what this step's # top-k may consume); "rungs" until the first emission lands self.emitted_tier = "rungs" + # fused-handshake arrival counters (zero-once: the consumer + # resets each row right after gating on it, so steady-state + # layer-to-layer reuse needs no host-side zeroing) + self.arrival: Optional[torch.Tensor] = None + if enable_fused_handshake: + self.arrival = torch.zeros((max_rows,), dtype=torch.int32, device=device) def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: nb4 = ((max_seq_len + 255) // 256 * 256) // 128 * 4 diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index a2d6a5e09a2b..5e30e87a400b 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -6724,6 +6724,7 @@ def _compile( cand_cap: int = 5120, accept_cap: Optional[int] = None, kc_override: Optional[int] = None, + handshake: bool = False, ) -> tuple: key = (dtype, top_k, next_n, enable_unroll_4, enable_phase3_unroll, use_constant_hint, min_blocks_per_mp, use_256bit_load, @@ -6731,7 +6732,7 @@ def _compile( compress_ratio, return_output_values, cluster_size, seqlen_sorted, enable_block_skip, use_ext_counts, emit_xstate, use_ext_cand, ext_rungs, cand_cap, accept_cap, - kc_override) + kc_override, handshake) if key in cls.kernel_cache: return key n_rows = cute.sym_int() @@ -6778,6 +6779,9 @@ def _compile( cutlass.Float32, (n_rows, 8), stride_order=(1, 0), assumed_align=4) if emit_xstate else None) + arrival_fake = (cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (n_rows, ), stride_order=(0, ), assumed_align=4) + if handshake else None) cand_vals_fake = (cute.runtime.make_fake_compact_tensor( cutlass.Float32, (n_rows, cand_cap), stride_order=(1, 0), @@ -6811,6 +6815,7 @@ def _compile( enable_block_skip=enable_block_skip, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, + handshake=handshake, use_ext_cand=use_ext_cand, ext_rungs=ext_rungs, cand_cap=cand_cap, @@ -6834,6 +6839,7 @@ def _compile( seed_thr=seed_thr_fake, seed_counts=None, xstate=xstate_fake, + arrival=arrival_fake, cand_vals=cand_vals_fake, cand_idx=cand_idx_fake, cand_ctl=cand_ctl_fake, @@ -6952,6 +6958,7 @@ def forward( cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, block_max: Optional[torch.Tensor] = None, + arrival: Optional[torch.Tensor] = None, num_threads: Optional[int] = None, accept_cap: Optional[int] = None, kc_override: Optional[int] = None, @@ -7089,6 +7096,12 @@ def forward( use_ext_cand = cand_vals is not None enable_block_skip = block_max is not None emit_xstate = xstate is not None + handshake = arrival is not None + if handshake: + assert (arrival.dtype == torch.int32 and arrival.is_cuda + and arrival.is_contiguous() and arrival.numel() + >= num_rows), ("arrival must be int32 [>= num_rows]") + assert next_n == 1, "handshake requires next_n == 1" if use_ext_cand: assert use_ext_counts, ( "candidate list requires the packed seed row " @@ -7123,12 +7136,13 @@ def forward( cand_cap=(cand_vals.shape[1] if use_ext_cand else 5120), accept_cap=accept_cap, kc_override=kc_override, + handshake=handshake, **tuning, ) cls.kernel_cache[key](logits, pre_idx, seq_lens, None, output_indices, order_row, block_max, seed_thr, None, xstate, cand_vals, cand_idx, - cand_ctl) + cand_ctl, arrival) # TODO(dsa.py): wire ``order_row = argsort(seq_lens, descending=True)`` # (device-side, graph-safe) into the LJF row-reorder branch when @@ -7156,6 +7170,7 @@ def cute_dsl_gvr_topk_decode( max_batch_size: Optional[int] = None, seed_thr: Optional[torch.Tensor] = None, xstate: Optional[torch.Tensor] = None, + arrival: Optional[torch.Tensor] = None, cand_vals: Optional[torch.Tensor] = None, cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, @@ -7235,6 +7250,7 @@ def cute_dsl_gvr_topk_decode( max_batch_size=max_batch_size, seed_thr=seed_thr, xstate=xstate, + arrival=arrival, cand_vals=cand_vals, cand_idx=cand_idx, cand_ctl=cand_ctl, @@ -7260,6 +7276,7 @@ def _( max_batch_size: Optional[int] = None, seed_thr: Optional[torch.Tensor] = None, xstate: Optional[torch.Tensor] = None, + arrival: Optional[torch.Tensor] = None, cand_vals: Optional[torch.Tensor] = None, cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, @@ -8391,13 +8408,14 @@ def _compile(cls, emit_cand=False, cand_cap=5120, emit_cand_bucketed=False, - accept_cap=8192): + accept_cap=8192, + emit_arrival=False): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, emit_seed_counts, seed_packed, emit_cand, cand_cap, - emit_cand_bucketed, accept_cap) + emit_cand_bucketed, accept_cap, emit_arrival) if key in cls.kernel_cache: return @@ -8511,6 +8529,12 @@ def _compile(cls, cutlass.Int32, (cute.sym_int(), 4), stride_order=(1, 0), assumed_align=4) + arrival_fake = None + if emit_arrival: + arrival_fake = cute.runtime.make_fake_compact_tensor( + cutlass.Int32, (cute.sym_int(), ), + stride_order=(0, ), + assumed_align=4) seed_thr_fake = None seed_counts_fake = None if emit_seed_counts: @@ -8559,6 +8583,7 @@ def _compile(cls, cand_cap=cand_cap, emit_cand_bucketed=emit_cand_bucketed, accept_cap=accept_cap, + emit_arrival=emit_arrival, ) compiled = cute.compile( @@ -8583,6 +8608,7 @@ def _compile(cls, cand_ctl=cand_ctl_fake, cand_idx_t=cand_idx_fake, cand_cur=cand_cur_fake, + arrival=arrival_fake, options="--enable-tvm-ffi", ) cls.kernel_cache[key] = compiled @@ -8618,6 +8644,7 @@ def forward( accept_cap: int = 8192, cand_idx_out: Optional[torch.Tensor] = None, cand_cur_out: Optional[torch.Tensor] = None, + arrival_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Execute FP4 paged MQA logits kernel. @@ -8838,11 +8865,20 @@ def forward( cand_cur_out = None # Compile if needed (fake tensors, no real data required) + emit_arrival = arrival_out is not None + if emit_arrival: + assert (arrival_out.dtype == torch.int32 and arrival_out.is_cuda + and arrival_out.is_contiguous() + and arrival_out.numel() >= B * next_n), ( + "arrival_out must be int32 [>= B*next_n] " + "(zero-initialized; consumer self-resets)") + assert emit_block_meta and next_n == 1, ( + "emit_arrival requires emit_block_meta and next_n == 1") key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, emit_seed_counts, seed_packed, emit_cand, cand_cap, - emit_cand_bucketed, accept_cap) + emit_cand_bucketed, accept_cap, emit_arrival) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8862,7 +8898,8 @@ def forward( emit_cand=emit_cand, cand_cap=cand_cap, emit_cand_bucketed=emit_cand_bucketed, - accept_cap=accept_cap) + accept_cap=accept_cap, + emit_arrival=emit_arrival) compiled = cls.kernel_cache[key] # TVM FFI: pass raw tensors, no dlpack/stream needed @@ -8871,11 +8908,11 @@ def forward( context_lens, schedule_meta, num_phys_blocks, B, block_max_out, hit_stats_out, hit_bitmap, seed_thr, seed_counts_out, cand_out, cand_ctl_out, cand_idx_out, - cand_cur_out) + cand_cur_out, arrival_out) return logits, block_max_out, hit_stats_out compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, None, - None, None, None, None, None, None, None, None) + None, None, None, None, None, None, None, None, None) return logits # NOTE: the optional emission tensors ARE written by the kernel but @@ -8906,6 +8943,7 @@ def cute_dsl_fp4_paged_mqa_logits( cand_ctl_out: Optional[torch.Tensor] = None, cand_cur_out: Optional[torch.Tensor] = None, accept_cap: int = 8192, + arrival_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: if not is_sm_100f(): raise ValueError( @@ -8954,7 +8992,8 @@ def cute_dsl_fp4_paged_mqa_logits( cand_out=cand_out, cand_idx_out=cand_idx_out, cand_ctl_out=cand_ctl_out, - cand_cur_out=cand_cur_out) + cand_cur_out=cand_cur_out, + arrival_out=arrival_out) # with emission on, the runner returns (logits, block_max, # hit_stats) - the emission buffers are caller-owned mutates, # the op face stays logits-only @@ -8981,6 +9020,7 @@ def _( cand_ctl_out: Optional[torch.Tensor] = None, cand_cur_out: Optional[torch.Tensor] = None, accept_cap: int = 8192, + arrival_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: B = q.shape[0] next_n = q.shape[1] diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index 2481a5c5b8b3..8bb3ac4a3190 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -60,6 +60,9 @@ from cutlass.cutlass_dsl import T, dsl_user_op from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait +from ..top_k.single_pass_multi_cta_radix_topk import red_release_gpu +from ..utils import griddepcontrol_launch_dependents + # CuTe DSL CUDA 13 validates rounding modes as string literals. The string # form is also accepted by older wrappers, so keep it version-independent. _RND_RN = "rn" @@ -469,6 +472,7 @@ def __init__( emit_block_meta: bool = False, emit_hit_stats: bool = True, emit_seed_counts: bool = False, + emit_arrival: bool = False, seed_packed: bool = False, emit_cand: bool = False, cand_cap: int = 5120, @@ -562,6 +566,15 @@ def __init__( if emit_seed_counts and not emit_block_meta: raise ValueError("emit_seed_counts requires emit_block_meta") self.emit_seed_counts = emit_seed_counts + # emit_arrival (fused-handshake mode): per-row arrival counters - + # every math warp publishes its split count for a finished row with + # a gpu-scope release; the dependent top-k kernel spin-gates each + # row on 8 * ceil(num_kv_tiles / 2) instead of waiting for the + # whole grid. Rides the q-transition flush points, so it needs the + # meta machinery. + if emit_arrival and not emit_block_meta: + raise ValueError("emit_arrival requires emit_block_meta") + self.emit_arrival = emit_arrival # seed_packed: single [num_rows, 8] fp32 seed row per the top-k # pre-packed contract - lines at cols 0..2, counts ACCUMULATED AS # FLOATS at cols 3..5 (exact to 2^24; red.global.add.f32). The @@ -828,6 +841,7 @@ def __call__( cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 {claimed, void}, zeroed cand_idx_t: cute.Tensor = None, # bucketed: [num_rows, 2*segA+capC] int32 SoA cand_cur: cute.Tensor = None, # bucketed: [num_rows, 4] int32 cursors, zeroed + arrival: cute.Tensor = None, # [num_rows] int32 arrival counters, zeroed ): # Derive KV data and SF views from the fused uint8 buffer. # Fused layout per phys block: [data half_head_dim*phys_block_kv bytes] @@ -1035,6 +1049,7 @@ class SharedStorage: cand_ctl, cand_idx_t, cand_cur, + arrival, self.cluster_layout_vmnk, self.a_smem_layout_staged, self.b_smem_layout_staged, @@ -1236,6 +1251,7 @@ def kernel( mCandCtl: cute.Tensor, # [num_rows, 2] int32 {claimed, void} (or None) mCandIdx: cute.Tensor, # bucketed: [num_rows, 2*segA+capC] int32 SoA (or None) mCandCur: cute.Tensor, # bucketed: [num_rows, 4] int32 cursors (or None) + mArrival: cute.Tensor, # [num_rows] int32 arrival counters (or None) cluster_layout_vmnk: cute.Layout, a_smem_layout_staged: cute.ComposedLayout, b_smem_layout_staged: cute.ComposedLayout, @@ -1290,6 +1306,12 @@ def kernel( start_q_clamped = min(start_q, batch_size - 1) current_num_kv = (mContextLens[start_q_clamped] + self.block_kv - 1) // self.block_kv + if cutlass.const_expr(self.emit_arrival): + # fused handshake: release the dependent (top-k) grid NOW - + # data safety rides on the per-row arrival counters, PDL only + # lets consumer CTAs stage onto SMs as producer CTAs retire. + griddepcontrol_launch_dependents() + if is_tma_warp: cpasync.prefetch_descriptor(tma_atom_a) cpasync.prefetch_descriptor(tma_atom_b) @@ -2277,6 +2299,9 @@ def kernel( meta_j = cutlass.Int32(0) hitw_batch = cutlass.Int32(0) + # fused handshake: this warp's split count for the current row + arr_cnt = cutlass.Int32(0) + while has_work: # fetch_next_task: commit next → current q_idx_old = q_idx @@ -2344,6 +2369,21 @@ def kernel( self._flush_cand_window_bucketed( mCand, mCandIdx, q_idx_old, cwbase, cwleft, meta_lane ) + if cutlass.const_expr(self.emit_arrival): + if q_idx_old < batch_size: + # publish this warp's split count for + # the finished row: sync_warp + the + # fence inside red_release_gpu order + # every prior write of this warp's + # lanes (logits STGs + emission + # atomics) before the add lands + cute.arch.sync_warp() + if meta_lane == 0: + red_release_gpu( + mArrival.iterator + q_idx_old, + arr_cnt, + ) + arr_cnt = cutlass.Int32(0) ctx_cur = mContextLens[q_idx] # Process KV block for group 0 (kv_idx + 0) @@ -2949,6 +2989,9 @@ def kernel( out_row = q_idx * next_n + t mLogits[(out_row, kv_pos)] = result_arr[t] + if cutlass.const_expr(self.emit_arrival): + arr_cnt = arr_cnt + cutlass.Int32(1) + # Advance: inline fetch_next_task next_kv_idx = kv_idx + NUM_MATH_WG if next_kv_idx >= num_kv: @@ -2981,6 +3024,12 @@ def kernel( mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane ) + if cutlass.const_expr(self.emit_arrival): + if q_idx < batch_size: + cute.arch.sync_warp() + if meta_lane == 0: + red_release_gpu(mArrival.iterator + q_idx, arr_cnt) + # Release last Q stage (WG 0) if q_idx < batch_size: q_pipeline.consumer_release(q_cons_state) @@ -3070,6 +3119,9 @@ def kernel( meta_j = cutlass.Int32(0) hitw_batch = cutlass.Int32(0) + # fused handshake: this warp's split count for the current row + arr_cnt = cutlass.Int32(0) + while has_work: # fetch_next_task: commit next → current q_idx_old = q_idx @@ -3136,6 +3188,21 @@ def kernel( self._flush_cand_window_bucketed( mCand, mCandIdx, q_idx_old, cwbase, cwleft, meta_lane ) + if cutlass.const_expr(self.emit_arrival): + if q_idx_old < batch_size: + # publish this warp's split count for + # the finished row: sync_warp + the + # fence inside red_release_gpu order + # every prior write of this warp's + # lanes (logits STGs + emission + # atomics) before the add lands + cute.arch.sync_warp() + if meta_lane == 0: + red_release_gpu( + mArrival.iterator + q_idx_old, + arr_cnt, + ) + arr_cnt = cutlass.Int32(0) ctx_cur = mContextLens[q_idx] # Process KV block for group 1 (kv_idx + 1) @@ -3735,6 +3802,9 @@ def kernel( out_row = q_idx * next_n + t mLogits[(out_row, kv_pos)] = result_arr[t] + if cutlass.const_expr(self.emit_arrival): + arr_cnt = arr_cnt + cutlass.Int32(1) + # Advance: inline fetch_next_task next_kv_idx = kv_idx + NUM_MATH_WG if next_kv_idx >= num_kv: @@ -3767,6 +3837,12 @@ def kernel( mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane ) + if cutlass.const_expr(self.emit_arrival): + if q_idx < batch_size: + cute.arch.sync_warp() + if meta_lane == 0: + red_release_gpu(mArrival.iterator + q_idx, arr_cnt) + # Release last Q stage (WG 1) if q_idx < batch_size: q_pipeline.consumer_release(q_cons_state) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 10de11addfc8..9ac9dd8189ce 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -41,6 +41,7 @@ from ..utils import TRTLLM_ENABLE_PDL, griddepcontrol_launch_dependents, griddepcontrol_wait from .block_scan import warp_scan +from .single_pass_multi_cta_radix_topk import ld_acquire_gpu, st_release_gpu def _env_flag(name: str) -> bool: @@ -274,6 +275,7 @@ def __init__( smem_cache_elems: int = 32768, seqlen_sorted: bool = False, kc_diet: Optional[bool] = None, + handshake: bool = False, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -527,6 +529,12 @@ def __init__( # kernel passes False for BOTH member instances so their SMEM layouts # stay byte-identical (the DSL sizes the launch from the last-traced # SmemAllocator only; see GvrTopKLBKernel). + # handshake (fused-op mode): spin-gate each row on the producer + # indexer's per-row arrival counter instead of relying on the + # grid-wide PDL wait. MVP scope pins next_n == 1 / cluster_size == 1. + self.handshake = bool(handshake) + if self.handshake and (next_n != 1 or cluster_size != 1): + raise ValueError("handshake requires next_n == 1 and cluster_size == 1") if kc_diet is None: kc_diet = cluster_size == 1 if enable_r0 and top_k == 512 and kc_diet and self.kC > 3072: @@ -4809,6 +4817,7 @@ def gvr_topk_kernel( cand_vals: cute.Tensor, # [numRows, CAP] fp32 scores (or None) cand_idx: cute.Tensor, # [numRows, CAP] int32 positions (or None) cand_ctl: cute.Tensor, # [numRows, 2] int32 {claimed, void} (or None) + arrival: cute.Tensor, # [numRows] int32 arrival counters (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -4867,6 +4876,7 @@ def gvr_topk_kernel( cand_vals=cand_vals, cand_idx=cand_idx, cand_ctl=cand_ctl, + arrival=arrival, ) @cute.jit @@ -4885,6 +4895,7 @@ def run_one_row( cand_vals: cute.Tensor = None, # [numRows, CAP] fp32 (ext cand) cand_idx: cute.Tensor = None, # [numRows, CAP] int32 (ext cand) cand_ctl: cute.Tensor = None, # [numRows, 2] int32 (ext cand) + arrival: cute.Tensor = None, # [numRows] int32 arrival counters ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -5217,6 +5228,22 @@ def run_one_row( s_cluster_partial_m = None smem_gath = None + if cutlass.const_expr(self.handshake): + # fused handshake: gate this row on the producer indexer's + # arrival counter (8 math warps each publish their split count; + # total = 8 * ceil(num_kv_tiles / 2)), then reset the counter + # for the next layer's reuse. The reset cannot race the next + # indexer launch: its CTAs only start after this kernel's + # trailing launch_dependents (PDL stream chain). + nkv_hs = (N + cutlass.Int32(127)) // cutlass.Int32(128) + target_hs = cutlass.Int32(8) * ((nkv_hs + cutlass.Int32(1)) // cutlass.Int32(2)) + arr_ptr_hs = arrival.iterator + row_idx + if tidx == cutlass.Int32(0): + while ld_acquire_gpu(arr_ptr_hs) < target_hs: + pass + st_release_gpu(arr_ptr_hs, cutlass.Int32(0)) + cute.arch.barrier() + # ---- Per-row dispatch ---- # Three branches: # 1. Degenerate (N <= top_k): no GVR work, leader emits identity. @@ -7159,6 +7186,7 @@ def __call__( cand_vals: cute.Tensor = None, # [num_rows, CAP] fp32 (ext cand) cand_idx: cute.Tensor = None, # [num_rows, CAP] int32 (ext cand) cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 (ext cand) + arrival: cute.Tensor = None, # [num_rows] int32 (handshake) ): num_rows = input_data.shape[0] cluster_size = cutlass.const_expr(self.cluster_size) @@ -7187,6 +7215,7 @@ def __call__( cand_vals, cand_idx, cand_ctl, + arrival, ).launch( grid=(total_ctas, 1, 1), block=(self.num_threads, 1, 1), From bc2621b1ba394b31284c8b1ab3e73754b7003ee9 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:24:38 -0700 Subject: [PATCH 059/117] Revert "[None][perf] Fused indexer->top-k handshake: PDL early release + per-row arrival gating" This reverts commit 8af7c59e9e. The handshake is measurably net-negative and the reason is structural, not a tuning miss. Cold graph-replay A/B across six shapes put the fused pair at 0.81-0.97x of the unfused one, and the large-batch hypothesis - that rows become CTA-local and therefore finish early - was falsified too (0.91-0.97x at B32-256). A three-arm ablation attributes the loss to the protocol: the producer-side fence and counter cost +3.2us at small batch and the consumer spin adds another 1.8-3.7us, against a measured boundary cost of only 2.2-2.6us. Root cause: the DeepGEMM scheduler deliberately spreads each row across many CTAs for load balance (256-token halves, split without regard to row boundaries), which is antagonistic to per-row early completion - a row is ready only when essentially the whole grid is, so the gate buys nothing while the protocol still charges. With equal row lengths no schedule can help, because every row finishes at the same time. The implementation is kept on perf/fused-handshake-falsified. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 26 +------ .../attention_backend/sparse/gvr_ext.py | 13 +--- .../_torch/custom_ops/cute_dsl_custom_ops.py | 58 +++----------- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 76 ------------------- .../blackwell/top_k/gvr_topk_decode.py | 29 ------- 5 files changed, 11 insertions(+), 191 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 6b520a5b19da..d3ca287eba94 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -1819,12 +1819,6 @@ def __init__(self, and self.use_cute_dsl_topk and self.use_cute_dsl_paged_mqa_logits and sparse_params.indexer_k_dtype == "fp4") - # fused-handshake mode (TRTLLM_GVR_FUSE=1, requires GVR_EXT): the - # indexer emits per-row arrival counters and releases the top-k - # grid early via PDL; the top-k spin-gates each row on its counter - # instead of waiting for the whole indexer grid. - self.use_gvr_fuse = (os.environ.get("TRTLLM_GVR_FUSE", "0") == "1" - and self.use_gvr_ext) self._gvr_ext = None # lazy GvrExtState (first decode step) self._gvr_route = None self.weight_scale_factor = self.softmax_scale * self.n_heads**-0.5 @@ -2790,8 +2784,7 @@ def sparse_attn_indexer( self._gvr_ext = GvrExtState( max_rows=metadata.max_num_sequences, top_k=self.index_topk, - device=q_fp8.device, - enable_fused_handshake=self.use_gvr_fuse) + device=q_fp8.device) st = self._gvr_ext n_comp = indexer_max_seq_len // max( self.compress_ratio, 1) @@ -2807,19 +2800,6 @@ def sparse_attn_indexer( gvr_emit_kwargs["block_max_out"] = ( st.ensure_block_max( indexer_max_seq_len)[:batch_size]) - # fused handshake only when BOTH sides of this - # step run an ext tier: the first step emits - # counts but its top-k still routes "rungs" and - # would never consume/reset the counters - - # skipping emission keeps them clean - self._gvr_emitted_arrival = ( - st.arrival is not None - and emit_tier in ("counts", "list") - and self._gvr_route.tier in ("counts", "list") - and self._gvr_route.cluster_size == 1) - if self._gvr_emitted_arrival: - gvr_emit_kwargs["arrival_out"] = ( - st.arrival[:batch_size]) logits_decode = torch.ops.trtllm.cute_dsl_fp4_paged_mqa_logits( dsl_q, decode_q_scale, k_cache, weights_decode, dsl_context_lens, dsl_block_table, dsl_schedule_meta, @@ -2916,10 +2896,6 @@ def sparse_attn_indexer( self._gvr_route, num_gen_tokens, st.block_max[:num_gen_tokens] if st.block_max is not None else None) - if getattr(self, "_gvr_emitted_arrival", False): - # fused handshake: gate each row on this step's - # indexer arrival counter (consumer resets it) - ext_kw["arrival"] = st.arrival[:num_gen_tokens] torch.ops.trtllm.cute_dsl_gvr_topk_decode( logits_decode, st.prev_topk[:num_gen_tokens], diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index ed3b30e5a17b..0611bbd10df7 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -52,12 +52,7 @@ class GvrExtState: """Per-attention-backend emission state (persistent buffers).""" def __init__( - self, - max_rows: int, - top_k: int, - device: torch.device, - enable_list_tier: bool = True, - enable_fused_handshake: bool = False, + self, max_rows: int, top_k: int, device: torch.device, enable_list_tier: bool = True ): self.max_rows = max_rows self.top_k = top_k @@ -85,12 +80,6 @@ def __init__( # tier the PREVIOUS indexer call emitted (what this step's # top-k may consume); "rungs" until the first emission lands self.emitted_tier = "rungs" - # fused-handshake arrival counters (zero-once: the consumer - # resets each row right after gating on it, so steady-state - # layer-to-layer reuse needs no host-side zeroing) - self.arrival: Optional[torch.Tensor] = None - if enable_fused_handshake: - self.arrival = torch.zeros((max_rows,), dtype=torch.int32, device=device) def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: nb4 = ((max_seq_len + 255) // 256 * 256) // 128 * 4 diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 5e30e87a400b..a2d6a5e09a2b 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -6724,7 +6724,6 @@ def _compile( cand_cap: int = 5120, accept_cap: Optional[int] = None, kc_override: Optional[int] = None, - handshake: bool = False, ) -> tuple: key = (dtype, top_k, next_n, enable_unroll_4, enable_phase3_unroll, use_constant_hint, min_blocks_per_mp, use_256bit_load, @@ -6732,7 +6731,7 @@ def _compile( compress_ratio, return_output_values, cluster_size, seqlen_sorted, enable_block_skip, use_ext_counts, emit_xstate, use_ext_cand, ext_rungs, cand_cap, accept_cap, - kc_override, handshake) + kc_override) if key in cls.kernel_cache: return key n_rows = cute.sym_int() @@ -6779,9 +6778,6 @@ def _compile( cutlass.Float32, (n_rows, 8), stride_order=(1, 0), assumed_align=4) if emit_xstate else None) - arrival_fake = (cute.runtime.make_fake_compact_tensor( - cutlass.Int32, (n_rows, ), stride_order=(0, ), assumed_align=4) - if handshake else None) cand_vals_fake = (cute.runtime.make_fake_compact_tensor( cutlass.Float32, (n_rows, cand_cap), stride_order=(1, 0), @@ -6815,7 +6811,6 @@ def _compile( enable_block_skip=enable_block_skip, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, - handshake=handshake, use_ext_cand=use_ext_cand, ext_rungs=ext_rungs, cand_cap=cand_cap, @@ -6839,7 +6834,6 @@ def _compile( seed_thr=seed_thr_fake, seed_counts=None, xstate=xstate_fake, - arrival=arrival_fake, cand_vals=cand_vals_fake, cand_idx=cand_idx_fake, cand_ctl=cand_ctl_fake, @@ -6958,7 +6952,6 @@ def forward( cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, block_max: Optional[torch.Tensor] = None, - arrival: Optional[torch.Tensor] = None, num_threads: Optional[int] = None, accept_cap: Optional[int] = None, kc_override: Optional[int] = None, @@ -7096,12 +7089,6 @@ def forward( use_ext_cand = cand_vals is not None enable_block_skip = block_max is not None emit_xstate = xstate is not None - handshake = arrival is not None - if handshake: - assert (arrival.dtype == torch.int32 and arrival.is_cuda - and arrival.is_contiguous() and arrival.numel() - >= num_rows), ("arrival must be int32 [>= num_rows]") - assert next_n == 1, "handshake requires next_n == 1" if use_ext_cand: assert use_ext_counts, ( "candidate list requires the packed seed row " @@ -7136,13 +7123,12 @@ def forward( cand_cap=(cand_vals.shape[1] if use_ext_cand else 5120), accept_cap=accept_cap, kc_override=kc_override, - handshake=handshake, **tuning, ) cls.kernel_cache[key](logits, pre_idx, seq_lens, None, output_indices, order_row, block_max, seed_thr, None, xstate, cand_vals, cand_idx, - cand_ctl, arrival) + cand_ctl) # TODO(dsa.py): wire ``order_row = argsort(seq_lens, descending=True)`` # (device-side, graph-safe) into the LJF row-reorder branch when @@ -7170,7 +7156,6 @@ def cute_dsl_gvr_topk_decode( max_batch_size: Optional[int] = None, seed_thr: Optional[torch.Tensor] = None, xstate: Optional[torch.Tensor] = None, - arrival: Optional[torch.Tensor] = None, cand_vals: Optional[torch.Tensor] = None, cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, @@ -7250,7 +7235,6 @@ def cute_dsl_gvr_topk_decode( max_batch_size=max_batch_size, seed_thr=seed_thr, xstate=xstate, - arrival=arrival, cand_vals=cand_vals, cand_idx=cand_idx, cand_ctl=cand_ctl, @@ -7276,7 +7260,6 @@ def _( max_batch_size: Optional[int] = None, seed_thr: Optional[torch.Tensor] = None, xstate: Optional[torch.Tensor] = None, - arrival: Optional[torch.Tensor] = None, cand_vals: Optional[torch.Tensor] = None, cand_idx: Optional[torch.Tensor] = None, cand_ctl: Optional[torch.Tensor] = None, @@ -8408,14 +8391,13 @@ def _compile(cls, emit_cand=False, cand_cap=5120, emit_cand_bucketed=False, - accept_cap=8192, - emit_arrival=False): + accept_cap=8192): """Compile kernel using fake tensors + TVM FFI.""" key = (compute_block_kv, phys_block_kv, num_heads, head_dim, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, emit_seed_counts, seed_packed, emit_cand, cand_cap, - emit_cand_bucketed, accept_cap, emit_arrival) + emit_cand_bucketed, accept_cap) if key in cls.kernel_cache: return @@ -8529,12 +8511,6 @@ def _compile(cls, cutlass.Int32, (cute.sym_int(), 4), stride_order=(1, 0), assumed_align=4) - arrival_fake = None - if emit_arrival: - arrival_fake = cute.runtime.make_fake_compact_tensor( - cutlass.Int32, (cute.sym_int(), ), - stride_order=(0, ), - assumed_align=4) seed_thr_fake = None seed_counts_fake = None if emit_seed_counts: @@ -8583,7 +8559,6 @@ def _compile(cls, cand_cap=cand_cap, emit_cand_bucketed=emit_cand_bucketed, accept_cap=accept_cap, - emit_arrival=emit_arrival, ) compiled = cute.compile( @@ -8608,7 +8583,6 @@ def _compile(cls, cand_ctl=cand_ctl_fake, cand_idx_t=cand_idx_fake, cand_cur=cand_cur_fake, - arrival=arrival_fake, options="--enable-tvm-ffi", ) cls.kernel_cache[key] = compiled @@ -8644,7 +8618,6 @@ def forward( accept_cap: int = 8192, cand_idx_out: Optional[torch.Tensor] = None, cand_cur_out: Optional[torch.Tensor] = None, - arrival_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: """Execute FP4 paged MQA logits kernel. @@ -8865,20 +8838,11 @@ def forward( cand_cur_out = None # Compile if needed (fake tensors, no real data required) - emit_arrival = arrival_out is not None - if emit_arrival: - assert (arrival_out.dtype == torch.int32 and arrival_out.is_cuda - and arrival_out.is_contiguous() - and arrival_out.numel() >= B * next_n), ( - "arrival_out must be int32 [>= B*next_n] " - "(zero-initialized; consumer self-resets)") - assert emit_block_meta and next_n == 1, ( - "emit_arrival requires emit_block_meta and next_n == 1") key = (compute_block_kv, phys_block_kv, H, D, next_n, num_sms, num_epi_subtiles, epi_dtype, output_dtype, remove_online_sf_transpose, emit_block_meta, emit_hit_stats, emit_seed_counts, seed_packed, emit_cand, cand_cap, - emit_cand_bucketed, accept_cap, emit_arrival) + emit_cand_bucketed, accept_cap) if key not in cls.kernel_cache: cls._compile( compute_block_kv, @@ -8898,8 +8862,7 @@ def forward( emit_cand=emit_cand, cand_cap=cand_cap, emit_cand_bucketed=emit_cand_bucketed, - accept_cap=accept_cap, - emit_arrival=emit_arrival) + accept_cap=accept_cap) compiled = cls.kernel_cache[key] # TVM FFI: pass raw tensors, no dlpack/stream needed @@ -8908,11 +8871,11 @@ def forward( context_lens, schedule_meta, num_phys_blocks, B, block_max_out, hit_stats_out, hit_bitmap, seed_thr, seed_counts_out, cand_out, cand_ctl_out, cand_idx_out, - cand_cur_out, arrival_out) + cand_cur_out) return logits, block_max_out, hit_stats_out compiled(kv_flat, q_3d, sf_q_2d, w_2d, logits, block_table, context_lens, schedule_meta, num_phys_blocks, B, None, - None, None, None, None, None, None, None, None, None) + None, None, None, None, None, None, None, None) return logits # NOTE: the optional emission tensors ARE written by the kernel but @@ -8943,7 +8906,6 @@ def cute_dsl_fp4_paged_mqa_logits( cand_ctl_out: Optional[torch.Tensor] = None, cand_cur_out: Optional[torch.Tensor] = None, accept_cap: int = 8192, - arrival_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: if not is_sm_100f(): raise ValueError( @@ -8992,8 +8954,7 @@ def cute_dsl_fp4_paged_mqa_logits( cand_out=cand_out, cand_idx_out=cand_idx_out, cand_ctl_out=cand_ctl_out, - cand_cur_out=cand_cur_out, - arrival_out=arrival_out) + cand_cur_out=cand_cur_out) # with emission on, the runner returns (logits, block_max, # hit_stats) - the emission buffers are caller-owned mutates, # the op face stays logits-only @@ -9020,7 +8981,6 @@ def _( cand_ctl_out: Optional[torch.Tensor] = None, cand_cur_out: Optional[torch.Tensor] = None, accept_cap: int = 8192, - arrival_out: Optional[torch.Tensor] = None, ) -> torch.Tensor: B = q.shape[0] next_n = q.shape[1] diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index 8bb3ac4a3190..2481a5c5b8b3 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -60,9 +60,6 @@ from cutlass.cutlass_dsl import T, dsl_user_op from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait -from ..top_k.single_pass_multi_cta_radix_topk import red_release_gpu -from ..utils import griddepcontrol_launch_dependents - # CuTe DSL CUDA 13 validates rounding modes as string literals. The string # form is also accepted by older wrappers, so keep it version-independent. _RND_RN = "rn" @@ -472,7 +469,6 @@ def __init__( emit_block_meta: bool = False, emit_hit_stats: bool = True, emit_seed_counts: bool = False, - emit_arrival: bool = False, seed_packed: bool = False, emit_cand: bool = False, cand_cap: int = 5120, @@ -566,15 +562,6 @@ def __init__( if emit_seed_counts and not emit_block_meta: raise ValueError("emit_seed_counts requires emit_block_meta") self.emit_seed_counts = emit_seed_counts - # emit_arrival (fused-handshake mode): per-row arrival counters - - # every math warp publishes its split count for a finished row with - # a gpu-scope release; the dependent top-k kernel spin-gates each - # row on 8 * ceil(num_kv_tiles / 2) instead of waiting for the - # whole grid. Rides the q-transition flush points, so it needs the - # meta machinery. - if emit_arrival and not emit_block_meta: - raise ValueError("emit_arrival requires emit_block_meta") - self.emit_arrival = emit_arrival # seed_packed: single [num_rows, 8] fp32 seed row per the top-k # pre-packed contract - lines at cols 0..2, counts ACCUMULATED AS # FLOATS at cols 3..5 (exact to 2^24; red.global.add.f32). The @@ -841,7 +828,6 @@ def __call__( cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 {claimed, void}, zeroed cand_idx_t: cute.Tensor = None, # bucketed: [num_rows, 2*segA+capC] int32 SoA cand_cur: cute.Tensor = None, # bucketed: [num_rows, 4] int32 cursors, zeroed - arrival: cute.Tensor = None, # [num_rows] int32 arrival counters, zeroed ): # Derive KV data and SF views from the fused uint8 buffer. # Fused layout per phys block: [data half_head_dim*phys_block_kv bytes] @@ -1049,7 +1035,6 @@ class SharedStorage: cand_ctl, cand_idx_t, cand_cur, - arrival, self.cluster_layout_vmnk, self.a_smem_layout_staged, self.b_smem_layout_staged, @@ -1251,7 +1236,6 @@ def kernel( mCandCtl: cute.Tensor, # [num_rows, 2] int32 {claimed, void} (or None) mCandIdx: cute.Tensor, # bucketed: [num_rows, 2*segA+capC] int32 SoA (or None) mCandCur: cute.Tensor, # bucketed: [num_rows, 4] int32 cursors (or None) - mArrival: cute.Tensor, # [num_rows] int32 arrival counters (or None) cluster_layout_vmnk: cute.Layout, a_smem_layout_staged: cute.ComposedLayout, b_smem_layout_staged: cute.ComposedLayout, @@ -1306,12 +1290,6 @@ def kernel( start_q_clamped = min(start_q, batch_size - 1) current_num_kv = (mContextLens[start_q_clamped] + self.block_kv - 1) // self.block_kv - if cutlass.const_expr(self.emit_arrival): - # fused handshake: release the dependent (top-k) grid NOW - - # data safety rides on the per-row arrival counters, PDL only - # lets consumer CTAs stage onto SMs as producer CTAs retire. - griddepcontrol_launch_dependents() - if is_tma_warp: cpasync.prefetch_descriptor(tma_atom_a) cpasync.prefetch_descriptor(tma_atom_b) @@ -2299,9 +2277,6 @@ def kernel( meta_j = cutlass.Int32(0) hitw_batch = cutlass.Int32(0) - # fused handshake: this warp's split count for the current row - arr_cnt = cutlass.Int32(0) - while has_work: # fetch_next_task: commit next → current q_idx_old = q_idx @@ -2369,21 +2344,6 @@ def kernel( self._flush_cand_window_bucketed( mCand, mCandIdx, q_idx_old, cwbase, cwleft, meta_lane ) - if cutlass.const_expr(self.emit_arrival): - if q_idx_old < batch_size: - # publish this warp's split count for - # the finished row: sync_warp + the - # fence inside red_release_gpu order - # every prior write of this warp's - # lanes (logits STGs + emission - # atomics) before the add lands - cute.arch.sync_warp() - if meta_lane == 0: - red_release_gpu( - mArrival.iterator + q_idx_old, - arr_cnt, - ) - arr_cnt = cutlass.Int32(0) ctx_cur = mContextLens[q_idx] # Process KV block for group 0 (kv_idx + 0) @@ -2989,9 +2949,6 @@ def kernel( out_row = q_idx * next_n + t mLogits[(out_row, kv_pos)] = result_arr[t] - if cutlass.const_expr(self.emit_arrival): - arr_cnt = arr_cnt + cutlass.Int32(1) - # Advance: inline fetch_next_task next_kv_idx = kv_idx + NUM_MATH_WG if next_kv_idx >= num_kv: @@ -3024,12 +2981,6 @@ def kernel( mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane ) - if cutlass.const_expr(self.emit_arrival): - if q_idx < batch_size: - cute.arch.sync_warp() - if meta_lane == 0: - red_release_gpu(mArrival.iterator + q_idx, arr_cnt) - # Release last Q stage (WG 0) if q_idx < batch_size: q_pipeline.consumer_release(q_cons_state) @@ -3119,9 +3070,6 @@ def kernel( meta_j = cutlass.Int32(0) hitw_batch = cutlass.Int32(0) - # fused handshake: this warp's split count for the current row - arr_cnt = cutlass.Int32(0) - while has_work: # fetch_next_task: commit next → current q_idx_old = q_idx @@ -3188,21 +3136,6 @@ def kernel( self._flush_cand_window_bucketed( mCand, mCandIdx, q_idx_old, cwbase, cwleft, meta_lane ) - if cutlass.const_expr(self.emit_arrival): - if q_idx_old < batch_size: - # publish this warp's split count for - # the finished row: sync_warp + the - # fence inside red_release_gpu order - # every prior write of this warp's - # lanes (logits STGs + emission - # atomics) before the add lands - cute.arch.sync_warp() - if meta_lane == 0: - red_release_gpu( - mArrival.iterator + q_idx_old, - arr_cnt, - ) - arr_cnt = cutlass.Int32(0) ctx_cur = mContextLens[q_idx] # Process KV block for group 1 (kv_idx + 1) @@ -3802,9 +3735,6 @@ def kernel( out_row = q_idx * next_n + t mLogits[(out_row, kv_pos)] = result_arr[t] - if cutlass.const_expr(self.emit_arrival): - arr_cnt = arr_cnt + cutlass.Int32(1) - # Advance: inline fetch_next_task next_kv_idx = kv_idx + NUM_MATH_WG if next_kv_idx >= num_kv: @@ -3837,12 +3767,6 @@ def kernel( mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane ) - if cutlass.const_expr(self.emit_arrival): - if q_idx < batch_size: - cute.arch.sync_warp() - if meta_lane == 0: - red_release_gpu(mArrival.iterator + q_idx, arr_cnt) - # Release last Q stage (WG 1) if q_idx < batch_size: q_pipeline.consumer_release(q_cons_state) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 9ac9dd8189ce..10de11addfc8 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -41,7 +41,6 @@ from ..utils import TRTLLM_ENABLE_PDL, griddepcontrol_launch_dependents, griddepcontrol_wait from .block_scan import warp_scan -from .single_pass_multi_cta_radix_topk import ld_acquire_gpu, st_release_gpu def _env_flag(name: str) -> bool: @@ -275,7 +274,6 @@ def __init__( smem_cache_elems: int = 32768, seqlen_sorted: bool = False, kc_diet: Optional[bool] = None, - handshake: bool = False, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -529,12 +527,6 @@ def __init__( # kernel passes False for BOTH member instances so their SMEM layouts # stay byte-identical (the DSL sizes the launch from the last-traced # SmemAllocator only; see GvrTopKLBKernel). - # handshake (fused-op mode): spin-gate each row on the producer - # indexer's per-row arrival counter instead of relying on the - # grid-wide PDL wait. MVP scope pins next_n == 1 / cluster_size == 1. - self.handshake = bool(handshake) - if self.handshake and (next_n != 1 or cluster_size != 1): - raise ValueError("handshake requires next_n == 1 and cluster_size == 1") if kc_diet is None: kc_diet = cluster_size == 1 if enable_r0 and top_k == 512 and kc_diet and self.kC > 3072: @@ -4817,7 +4809,6 @@ def gvr_topk_kernel( cand_vals: cute.Tensor, # [numRows, CAP] fp32 scores (or None) cand_idx: cute.Tensor, # [numRows, CAP] int32 positions (or None) cand_ctl: cute.Tensor, # [numRows, 2] int32 {claimed, void} (or None) - arrival: cute.Tensor, # [numRows] int32 arrival counters (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -4876,7 +4867,6 @@ def gvr_topk_kernel( cand_vals=cand_vals, cand_idx=cand_idx, cand_ctl=cand_ctl, - arrival=arrival, ) @cute.jit @@ -4895,7 +4885,6 @@ def run_one_row( cand_vals: cute.Tensor = None, # [numRows, CAP] fp32 (ext cand) cand_idx: cute.Tensor = None, # [numRows, CAP] int32 (ext cand) cand_ctl: cute.Tensor = None, # [numRows, 2] int32 (ext cand) - arrival: cute.Tensor = None, # [numRows] int32 arrival counters ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -5228,22 +5217,6 @@ def run_one_row( s_cluster_partial_m = None smem_gath = None - if cutlass.const_expr(self.handshake): - # fused handshake: gate this row on the producer indexer's - # arrival counter (8 math warps each publish their split count; - # total = 8 * ceil(num_kv_tiles / 2)), then reset the counter - # for the next layer's reuse. The reset cannot race the next - # indexer launch: its CTAs only start after this kernel's - # trailing launch_dependents (PDL stream chain). - nkv_hs = (N + cutlass.Int32(127)) // cutlass.Int32(128) - target_hs = cutlass.Int32(8) * ((nkv_hs + cutlass.Int32(1)) // cutlass.Int32(2)) - arr_ptr_hs = arrival.iterator + row_idx - if tidx == cutlass.Int32(0): - while ld_acquire_gpu(arr_ptr_hs) < target_hs: - pass - st_release_gpu(arr_ptr_hs, cutlass.Int32(0)) - cute.arch.barrier() - # ---- Per-row dispatch ---- # Three branches: # 1. Degenerate (N <= top_k): no GVR work, leader emits identity. @@ -7186,7 +7159,6 @@ def __call__( cand_vals: cute.Tensor = None, # [num_rows, CAP] fp32 (ext cand) cand_idx: cute.Tensor = None, # [num_rows, CAP] int32 (ext cand) cand_ctl: cute.Tensor = None, # [num_rows, 2] int32 (ext cand) - arrival: cute.Tensor = None, # [num_rows] int32 (handshake) ): num_rows = input_data.shape[0] cluster_size = cutlass.const_expr(self.cluster_size) @@ -7215,7 +7187,6 @@ def __call__( cand_vals, cand_idx, cand_ctl, - arrival, ).launch( grid=(total_ctas, 1, 1), block=(self.num_threads, 1, 1), From b3410e21aafaeb605cfe7e2292cc150336aa1a3e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 07:37:51 -0700 Subject: [PATCH 060/117] [None][perf] GVR top-k: take the PDL wait after the prologue, not at entry The kernel called griddepcontrol_wait() as its first statement, so a dependent CTA blocked before doing any of its own setup. Everything above the first read of producer-written data - row resolution, the whole SMEM allocation, config folding, and (on a cold call) the instruction fetch for all of it - can just as well run while the producer kernel drains: dependent CTAs stage onto SMs as producer CTAs retire. Instruction-fetch starvation is 37-44% of this kernel's stall cycles on the small-N cells (ncu, cache-control all), so warming that under the producer's shadow is worth real time. Safety: nothing before the new wait site touches producer output. seq_lens and pre_idx come from host-side metadata and the feedback buffer; logits / block_max / seed_thr / cand are first read in the dispatch below it. The placement is a ctor knob (pdl_wait_late, default on) so both variants compile into one process for A/B - cross-run cold drift on a shared node is +-3us, larger than the effect, and only interleaved in-process comparison resolves it. Interleaved cold graph-replay A/B of [indexer + top-k], 24 reps per arm, median (B x compressed-N): 2x8k 28.51 -> 28.03us (1.017x) 8x8k 32.03 -> 30.82us (1.040x) 64x8k 39.30 -> 38.69us (1.016x) 2x64k 35.97 -> 35.62us (1.010x) 8x64k 40.13 -> 40.16us (1.000x) Exactness unchanged (same computation, later sync): unified smoke 30/30, degenerate battery 37 passed + 1 xfailed, ext tiers EXACT, CUDA-graph replay 11/11, bare-op path identical. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 24 ++++++++++++++++++- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 10de11addfc8..156da70a9c10 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -274,6 +274,7 @@ def __init__( smem_cache_elems: int = 32768, seqlen_sorted: bool = False, kc_diet: Optional[bool] = None, + pdl_wait_late: bool = True, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -527,6 +528,13 @@ def __init__( # kernel passes False for BOTH member instances so their SMEM layouts # stay byte-identical (the DSL sizes the launch from the last-traced # SmemAllocator only; see GvrTopKLBKernel). + # pdl_wait_late: place the PDL wait after the prologue (row + # resolution + SMEM allocation + config folding) instead of at + # kernel entry, so that work - and its cold instruction fetch - + # overlaps the producer's tail. Kept as a knob so both + # placements can be A/B'd inside one process (cross-run cold + # drift on shared nodes is larger than the effect). + self.pdl_wait_late = bool(pdl_wait_late) if kc_diet is None: kc_diet = cluster_size == 1 if enable_r0 and top_k == 512 and kc_diet and self.kC > 3072: @@ -4990,7 +4998,8 @@ def run_one_row( output_indices_row = output_indices[row_idx, None] pre_idx_count = pre_idx.shape[1] - griddepcontrol_wait() + if cutlass.const_expr(not self.pdl_wait_late): + griddepcontrol_wait() # ---- Shared memory allocation ---- smem = SmemAllocator() @@ -5217,6 +5226,19 @@ def run_one_row( s_cluster_partial_m = None smem_gath = None + # PDL wait placed as late as possible: everything above (row + # resolution, SMEM allocation, config folding - and, on a cold + # call, the instruction fetch for all of it) overlaps with the + # producer indexer's tail, because dependent CTAs stage onto SMs + # as producer CTAs retire. Nothing above reads producer-written + # data: seq_lens/pre_idx come from the host-side metadata and the + # feedback buffer, while logits / block_max / seed_thr / cand are + # first touched below. Instruction-fetch starvation is 37-44% of + # this kernel's stall cycles on the small-N cells, so warming it + # under the producer's shadow is the point. + if cutlass.const_expr(self.pdl_wait_late): + griddepcontrol_wait() + # ---- Per-row dispatch ---- # Three branches: # 1. Degenerate (N <= top_k): no GVR work, leader emits identity. diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 58ca7336b6c7..24e928474d5f 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -66,6 +66,7 @@ def _compile( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, enable_block_skip: bool = False, + pdl_wait_late: bool = True, use_ext_counts: bool = False, emit_xstate: bool = False, use_ext_cand: bool = False, @@ -203,6 +204,7 @@ def _compile( p4_warp_redundant=p4_warp_redundant, p2_warp_redundant=p2_warp_redundant, enable_block_skip=enable_block_skip, + pdl_wait_late=pdl_wait_late, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, use_ext_cand=use_ext_cand, @@ -664,6 +666,7 @@ def gvr_topk_decode( order_row: Optional[torch.Tensor] = None, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, + pdl_wait_late: bool = True, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, seed_thr: Optional[torch.Tensor] = None, @@ -950,6 +953,7 @@ def gvr_topk_decode( p4_warp_redundant, p2_warp_redundant, enable_block_skip, + pdl_wait_late, use_ext_counts, emit_xstate, use_ext_cand, From 6949d7fc48b9eb78e2b29147c9a6958d24ef8d60 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:47:18 -0700 Subject: [PATCH 061/117] [None][perf] GVR P4: filter the fine recursion by value range, not by bin recompute Phase 4's fine recursion exists only to locate the handful of candidates that land in the straddling coarse bin, yet it recomputed every candidate's coarse bin to find them: a subtract, a multiply and two clamps per candidate, then a compare against b*. Membership in bin b* is exactly 'value in [f_lo, f_hi)', so two compares do the same job. The clamped ends of the binning fold out-of-range values into bin 0 and bin kBins-1, so those two bins drop the matching side of the range test and stay bit-identical. Phase-4 sub-phase stamps (clock64, small-N cells) put the fine recursion at 44% of the phase - 1.7us of 3.9us - which is what made this the first cut. Interleaved cold graph-replay A/B of [indexer + top-k], K=512 cr=4 (V4-Flash production shape), 24 reps per arm, median: B2 x 8k 26.24 -> 26.08us (1.006x) B8 x 8k 31.74 -> 30.02us (1.058x) B64 x 8k 34.46 -> 34.56us (0.997x) B2 x 64k 43.84 -> 42.37us (1.035x) B8 x 64k 44.80 -> 44.70us (1.002x) Also measured and rejected on the way: caching each thread's candidates in registers during the coarse build so the fine pass skips the SMEM sweep entirely. It removes more work but the extra live registers push the fp32 kernel past its budget - 0.95-0.99x on 4 of 5 cells. The knob keeps the old path available for A/B. Exactness: full top-k unit file 708 passed + 1 xfailed, unified smoke 30/30, ext tiers EXACT, CUDA-graph replay 11/11, bare-op identical. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 87 ++++++++++++++----- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 + 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 156da70a9c10..9a529523b915 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -56,6 +56,9 @@ def _env_flag(name: str) -> bool: # P4 sub-phase clock64 breakdown -> xstate[1,2,4,5,6,7] (debug: clobbers # the closed-loop thr/anch publish; single-shot cells only, not chains) _P4_SUB_DBG = _env_flag("GVR_P4_SUB_DBG") +# GVR_P4_SUB_DBG=2: publish the P4 HEAD triple (minmax / histogram build / +# coarse search) instead of the tail triple, so the phase budget adds up. +_P4_SUB_HEAD = os.environ.get("GVR_P4_SUB_DBG", "0").strip() == "2" _SKIP_DBG = _env_flag("GVR_SKIP_DBG") @@ -275,6 +278,7 @@ def __init__( seqlen_sorted: bool = False, kc_diet: Optional[bool] = None, pdl_wait_late: bool = True, + p4_fine_rangetest: Optional[bool] = None, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -553,6 +557,15 @@ def __init__( # measured as a wash under the same protocol and stay stock. if enable_r0 and top_k == 2048 and self.kNumBins > 512: self.kNumBins = 512 + # p4_fine_rangetest: the fine recursion exists only to locate + # the handful of candidates inside the straddling coarse bin, so + # filter them with a value-range compare instead of recomputing + # each candidate's bin (subtract + multiply + two clamps). It is + # 44% of Phase 4 on the small-N cells. Caching the candidates in + # registers during the coarse build was measured and rejected: + # the extra live registers push the fp32 kernel past its budget + # (0.95-0.99x on 4 of 5 cells). + self.p4_fine_rangetest = True if p4_fine_rangetest is None else bool(p4_fine_rangetest) self.r0_qfracs = tuple(float(q) for q in r0_qfracs) if r0_qfracs else () if self.r0_qfracs: assert all(0.0 < q < 1.0 for q in self.r0_qfracs), self.r0_qfracs @@ -3649,22 +3662,50 @@ def phase4_rank_scatter( smem_hist[iz] = cutlass.Int32(0) iz = iz + cutlass.Int32(num_threads) cute.arch.barrier() - ifb = tidx - while ifb < cand_count: - vf = smem_keys[ifb] - cb = cutlass.Int32((vf - bmin_r) * inv1) - if cb < cutlass.Int32(0): - cb = cutlass.Int32(0) - if cb > cutlass.Int32(kBins - 1): - cb = cutlass.Int32(kBins - 1) - if cb == b_star: - sb = cutlass.Int32((vf - f_lo) * finv) - if sb < cutlass.Int32(0): - sb = cutlass.Int32(0) - if sb > cutlass.Int32(fbins - 1): - sb = cutlass.Int32(fbins - 1) - atomicAdd(smem_hist.iterator + sb, cutlass.Int32(1)) - ifb = ifb + cutlass.Int32(num_threads) + if cutlass.const_expr(self.p4_fine_rangetest): + # A candidate belongs to bin b* exactly when its value + # lies in [f_lo, f_hi), so the filter is two compares - + # the bin recompute (subtract + multiply + two clamps) + # per candidate is redundant work. The clamped ends of + # the binning fold out-of-range values INTO bin 0 and + # bin kBins-1, so those two bins drop the matching side + # of the range test to stay bit-identical. + f_hi = f_lo + cutlass.Float32(1.0) / inv1 + lo_edge = b_star == cutlass.Int32(0) + hi_edge = b_star == cutlass.Int32(kBins - 1) + ifb = tidx + while ifb < cand_count: + vf = smem_keys[ifb] + inb = vf >= f_lo and vf < f_hi + if lo_edge: + inb = vf < f_hi + if hi_edge: + inb = vf >= f_lo + if inb: + sb = cutlass.Int32((vf - f_lo) * finv) + if sb < cutlass.Int32(0): + sb = cutlass.Int32(0) + if sb > cutlass.Int32(fbins - 1): + sb = cutlass.Int32(fbins - 1) + atomicAdd(smem_hist.iterator + sb, cutlass.Int32(1)) + ifb = ifb + cutlass.Int32(num_threads) + else: + ifb = tidx + while ifb < cand_count: + vfo = smem_keys[ifb] + cbo = cutlass.Int32((vfo - bmin_r) * inv1) + if cbo < cutlass.Int32(0): + cbo = cutlass.Int32(0) + if cbo > cutlass.Int32(kBins - 1): + cbo = cutlass.Int32(kBins - 1) + if cbo == b_star: + sbo = cutlass.Int32((vfo - f_lo) * finv) + if sbo < cutlass.Int32(0): + sbo = cutlass.Int32(0) + if sbo > cutlass.Int32(fbins - 1): + sbo = cutlass.Int32(fbins - 1) + atomicAdd(smem_hist.iterator + sbo, cutlass.Int32(1)) + ifb = ifb + cutlass.Int32(num_threads) cute.arch.barrier() # fine 3-step search seeded at rank_above (over fbins bins) fws = cutlass.Int32(0) @@ -7035,10 +7076,16 @@ def _run_phases( # (minmax/hist/coarse, wcnt[8..10]) are not # published. xstate_row[1] = s_thr[1] - xstate_row[4] = cutlass.Float32(smem_wcnt[11]) - xstate_row[5] = cutlass.Float32(smem_wcnt[12]) - xstate_row[6] = cutlass.Float32(smem_wcnt[13]) - xstate_row[7] = cutlass.Float32(cutlass.Int32(ck_sw1 - ck_sw0)) + if cutlass.const_expr(_P4_SUB_HEAD): + xstate_row[4] = cutlass.Float32(smem_wcnt[8]) + xstate_row[5] = cutlass.Float32(smem_wcnt[9]) + xstate_row[6] = cutlass.Float32(smem_wcnt[10]) + xstate_row[7] = cutlass.Float32(smem_wcnt[11]) + else: + xstate_row[4] = cutlass.Float32(smem_wcnt[11]) + xstate_row[5] = cutlass.Float32(smem_wcnt[12]) + xstate_row[6] = cutlass.Float32(smem_wcnt[13]) + xstate_row[7] = cutlass.Float32(cutlass.Int32(ck_sw1 - ck_sw0)) # cand_count_p4 = pre-P4 snapshot (P4 repurposes # the s_iscalars slots). xstate_row[3] = cutlass.Float32(cand_count_p4) diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 24e928474d5f..23285c5a333f 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -67,6 +67,7 @@ def _compile( p2_warp_redundant: bool = True, enable_block_skip: bool = False, pdl_wait_late: bool = True, + p4_fine_rangetest: "bool | None" = None, use_ext_counts: bool = False, emit_xstate: bool = False, use_ext_cand: bool = False, @@ -205,6 +206,7 @@ def _compile( p2_warp_redundant=p2_warp_redundant, enable_block_skip=enable_block_skip, pdl_wait_late=pdl_wait_late, + p4_fine_rangetest=p4_fine_rangetest, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, use_ext_cand=use_ext_cand, @@ -667,6 +669,7 @@ def gvr_topk_decode( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, pdl_wait_late: bool = True, + p4_fine_rangetest: Optional[bool] = None, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, seed_thr: Optional[torch.Tensor] = None, @@ -954,6 +957,7 @@ def gvr_topk_decode( p2_warp_redundant, enable_block_skip, pdl_wait_late, + p4_fine_rangetest, use_ext_counts, emit_xstate, use_ext_cand, From d66fa12dfeb06cd073d070923d7325fe8d1c083f Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:55:08 -0700 Subject: [PATCH 062/117] [None][perf] GVR P4: lane-parallel warp bin sums in both bin searches Both bin searches opened with each warp summing its slice of the histogram, but the loop indexed by warp only - all 32 lanes walked the same bins_per_warp bins and built the same running sum. That is a dependency chain as long as the segment (64 adds at kBins=1024, 16 warps) with the whole warp doing identical work. Spreading the segment across the lanes and closing with one warp reduction turns it into 2 loads plus 5 shuffles. Same fix in the coarse search and in the fine recursion's search. Measurement (this is the methodology the numbers below rely on): interleaved cold graph replay of [indexer + top-k], both variants compiled in one process, arms alternated inside each rep, and the PAIRED difference taken per rep so slow drift cancels. 48 pairs per cell, K=512 cr=4 (V4-Flash production shape). Comparing per-arm medians instead - the earlier approach - buries a 3-5% effect under +-1.3us of per-cell noise; the paired sign test resolves it. Cells with no effect land at ~50% wins, which is the built-in control. B2 x 8k +1.54us 28/48 wins (inside noise) B8 x 8k +1.76us 41/48 wins B64 x 8k +0.13us 24/48 wins (neutral) B2 x 64k +1.54us 40/48 wins B8 x 64k -0.10us 22/48 wins (neutral) Exactness: full top-k unit file 708 passed + 1 xfailed, unified smoke 30/30, ext tiers EXACT, CUDA-graph replay 11/11, bare-op identical. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py | 7 +++++++ tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 9a529523b915..938f72a4b587 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -279,6 +279,7 @@ def __init__( kc_diet: Optional[bool] = None, pdl_wait_late: bool = True, p4_fine_rangetest: Optional[bool] = None, + p4_lane_binsum: bool = True, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -566,6 +567,12 @@ def __init__( # the extra live registers push the fp32 kernel past its budget # (0.95-0.99x on 4 of 5 cells). self.p4_fine_rangetest = True if p4_fine_rangetest is None else bool(p4_fine_rangetest) + # p4_lane_binsum: the per-warp bin sums that open both bin + # searches indexed by warp only, so all 32 lanes walked the same + # bins_per_warp bins - a dependency chain as long as the segment, + # with the whole warp doing identical work. Spread the segment + # across the lanes and close with one warp reduction instead. + self.p4_lane_binsum = bool(p4_lane_binsum) self.r0_qfracs = tuple(float(q) for q in r0_qfracs) if r0_qfracs else () if self.r0_qfracs: assert all(0.0 < q < 1.0 for q in self.r0_qfracs), self.r0_qfracs diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 23285c5a333f..7910c5b3b7fe 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -68,6 +68,7 @@ def _compile( enable_block_skip: bool = False, pdl_wait_late: bool = True, p4_fine_rangetest: "bool | None" = None, + p4_lane_binsum: bool = True, use_ext_counts: bool = False, emit_xstate: bool = False, use_ext_cand: bool = False, @@ -207,6 +208,7 @@ def _compile( enable_block_skip=enable_block_skip, pdl_wait_late=pdl_wait_late, p4_fine_rangetest=p4_fine_rangetest, + p4_lane_binsum=p4_lane_binsum, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, use_ext_cand=use_ext_cand, @@ -670,6 +672,7 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, pdl_wait_late: bool = True, p4_fine_rangetest: Optional[bool] = None, + p4_lane_binsum: bool = True, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, seed_thr: Optional[torch.Tensor] = None, @@ -958,6 +961,7 @@ def gvr_topk_decode( enable_block_skip, pdl_wait_late, p4_fine_rangetest, + p4_lane_binsum, use_ext_counts, emit_xstate, use_ext_cand, From bdeabeb6a6fb51137a1e4f84d731f242d9e67650 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:58:13 -0700 Subject: [PATCH 063/117] [None][perf] GVR P4: classify the scatter by value range too The scatter walks every candidate to split it into above / inside / below the straddling bin, and recomputed the candidate's bin index to decide - the third O(candidates) loop in Phase 4 paying a subtract, a multiply and two clamps for a question the value-range compare already answers. The bin-b* edge constants now live above the fine recursion so both loops share them. bin_i is only ever compared against b*, so the class is encoded as b*-1 / b* / b*+1 and every branch below stays as it was. Paired cold graph-replay A/B (48 pairs per cell, K=512 cr=4, production V4-Flash shape; arms alternate inside each rep and the per-rep difference is taken, so slow drift cancels): B2 x 8k -0.16us 22/48 wins (neutral) B8 x 8k +1.28us 36/48 wins B64 x 8k 0.00us 23/48 wins (neutral) B2 x 64k +1.44us 43/48 wins B8 x 64k +0.03us 24/48 wins (neutral) With the two earlier cuts (fine-recursion filter, lane-parallel bin sums) the three together are worth ~4.8us on B8 x 8k and ~4.4us on B2 x 64k. All three share one root cause: Phase 4's three O(candidates) loops each re-derived a bin index for a question that is a comparison against the straddling bin's value range. Exactness: full top-k unit file 708 passed + 1 xfailed, unified smoke 30/30, ext tiers EXACT, CUDA-graph replay 11/11, bare-op identical. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 47 +++++++++++++++---- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 938f72a4b587..ecfc165bad48 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -280,6 +280,7 @@ def __init__( pdl_wait_late: bool = True, p4_fine_rangetest: Optional[bool] = None, p4_lane_binsum: bool = True, + p4_scat_rangetest: bool = True, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -573,6 +574,12 @@ def __init__( # with the whole warp doing identical work. Spread the segment # across the lanes and close with one warp reduction instead. self.p4_lane_binsum = bool(p4_lane_binsum) + # p4_scat_rangetest: the scatter classifies each candidate as + # above / inside / below the straddling bin and recomputed the + # bin index to do it; the same value-range compare the fine + # recursion uses answers it without the subtract, multiply and + # two clamps per candidate. + self.p4_scat_rangetest = bool(p4_scat_rangetest) self.r0_qfracs = tuple(float(q) for q in r0_qfracs) if r0_qfracs else () if self.r0_qfracs: assert all(0.0 < q < 1.0 for q in self.r0_qfracs), self.r0_qfracs @@ -3663,6 +3670,12 @@ def phase4_rank_scatter( # bin b* value range under the inv1 binning: [f_lo, f_lo + 1/inv1) f_lo = bmin_r + cutlass.Float32(b_star) / inv1 finv = (cutlass.Float32(fbins - 1) + cutlass.Float32(0.99)) * inv1 + # bin b* spans [f_lo, f_hi) under the coarse binning; the + # clamped ends fold out-of-range values INTO bin 0 and bin + # kBins-1, so those two bins drop the matching side. + f_hi = f_lo + cutlass.Float32(1.0) / inv1 + lo_edge = b_star == cutlass.Int32(0) + hi_edge = b_star == cutlass.Int32(kBins - 1) # re-zero (only fbins slots) + build fine sub-hist of bin-b* cands iz = tidx while iz < cutlass.Int32(fbins): @@ -3677,9 +3690,6 @@ def phase4_rank_scatter( # the binning fold out-of-range values INTO bin 0 and # bin kBins-1, so those two bins drop the matching side # of the range test to stay bit-identical. - f_hi = f_lo + cutlass.Float32(1.0) / inv1 - lo_edge = b_star == cutlass.Int32(0) - hi_edge = b_star == cutlass.Int32(kBins - 1) ifb = tidx while ifb < cand_count: vf = smem_keys[ifb] @@ -3782,11 +3792,32 @@ def phase4_rank_scatter( isc = tidx while isc < cand_count: v = smem_keys[isc] - bin_i = cutlass.Int32((v - bmin_r) * inv1) - if bin_i < cutlass.Int32(0): - bin_i = cutlass.Int32(0) - if bin_i > cutlass.Int32(kBins - 1): - bin_i = cutlass.Int32(kBins - 1) + if cutlass.const_expr(self.p4_scat_rangetest): + # same three-way split as the bin recompute, by value: + # above b* <=> v >= f_hi (impossible at the top bin, + # which absorbs everything higher), inside b* <=> v in + # [f_lo, f_hi) with the edge bins dropping their + # absorbed side. bin_i is only ever compared against + # b*, so encoding the class as b*-1 / b* / b*+1 keeps + # the branches below unchanged. + abv = v >= f_hi + inb2 = v >= f_lo and v < f_hi + if lo_edge: + inb2 = v < f_hi + if hi_edge: + abv = False + inb2 = v >= f_lo + bin_i = b_star - cutlass.Int32(1) + if abv: + bin_i = b_star + cutlass.Int32(1) + if inb2: + bin_i = b_star + else: + bin_i = cutlass.Int32((v - bmin_r) * inv1) + if bin_i < cutlass.Int32(0): + bin_i = cutlass.Int32(0) + if bin_i > cutlass.Int32(kBins - 1): + bin_i = cutlass.Int32(kBins - 1) if bin_i > b_star: pos = atomicAdd(s_iscalars.iterator + cutlass.Int32(4), cutlass.Int32(1)) if pos < cutlass.Int32(kK): diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 7910c5b3b7fe..0420bb2a926f 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -69,6 +69,7 @@ def _compile( pdl_wait_late: bool = True, p4_fine_rangetest: "bool | None" = None, p4_lane_binsum: bool = True, + p4_scat_rangetest: bool = True, use_ext_counts: bool = False, emit_xstate: bool = False, use_ext_cand: bool = False, @@ -209,6 +210,7 @@ def _compile( pdl_wait_late=pdl_wait_late, p4_fine_rangetest=p4_fine_rangetest, p4_lane_binsum=p4_lane_binsum, + p4_scat_rangetest=p4_scat_rangetest, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, use_ext_cand=use_ext_cand, @@ -673,6 +675,7 @@ def gvr_topk_decode( pdl_wait_late: bool = True, p4_fine_rangetest: Optional[bool] = None, p4_lane_binsum: bool = True, + p4_scat_rangetest: bool = True, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, seed_thr: Optional[torch.Tensor] = None, @@ -962,6 +965,7 @@ def gvr_topk_decode( pdl_wait_late, p4_fine_rangetest, p4_lane_binsum, + p4_scat_rangetest, use_ext_counts, emit_xstate, use_ext_cand, From bba4dd41311b39cbdcd1f7284b6171a7cdda6b0c Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:52:30 -0700 Subject: [PATCH 064/117] [None][fix] disable the two P4 range-test fast paths (not fp32-exact) p4_fine_rangetest and p4_scat_rangetest replaced a candidate's bin recompute, floor((v - bmin) * inv1) == b_star, with a value-range compare against the bin's [lo, hi) edges. The two agree in exact arithmetic but not in fp32, and each was applied to only one of the two passes that must classify a candidate identically: the fine histogram and the scatter. A candidate the passes disagree on is counted by one and placed by the other. Both failure directions were observed on a captured 256k decode row (N=65537, K=1024, one tie at the K-th value): - fine only: one output slot left unwritten (1023 of 1024 filled, last slot -1) - scat only: the scatter writes past the rank it reserved, so out-of-range indices reach the output; a downstream gather on those indices trips a device-side assert Default both off, restoring the bit-exact bin recompute. The captured row is exact on 8/8 replays with block skip on and off. p4_lane_binsum is unaffected - it changes only how the per-warp bin sums are reduced, not how a candidate is classified - and stays on. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 21 ++++++++++++------- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index ecfc165bad48..5fb2727a74e7 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -280,7 +280,7 @@ def __init__( pdl_wait_late: bool = True, p4_fine_rangetest: Optional[bool] = None, p4_lane_binsum: bool = True, - p4_scat_rangetest: bool = True, + p4_scat_rangetest: bool = False, enable_r0: bool = True, accept_cap: "int | None" = None, kc_override: "int | None" = None, @@ -562,12 +562,15 @@ def __init__( # p4_fine_rangetest: the fine recursion exists only to locate # the handful of candidates inside the straddling coarse bin, so # filter them with a value-range compare instead of recomputing - # each candidate's bin (subtract + multiply + two clamps). It is - # 44% of Phase 4 on the small-N cells. Caching the candidates in - # registers during the coarse build was measured and rejected: - # the extra live registers push the fp32 kernel past its budget - # (0.95-0.99x on 4 of 5 cells). - self.p4_fine_rangetest = True if p4_fine_rangetest is None else bool(p4_fine_rangetest) + # each candidate's bin (subtract + multiply + two clamps). + # DEFAULT OFF - not bit-equivalent to the bin recompute it + # replaces. v in [f_lo, f_hi) and floor((v - bmin) * inv1) == + # b_star agree in exact arithmetic but not in fp32, and the + # scatter below still classifies by bin recompute. A candidate + # the two passes disagree on is counted by one and placed by the + # other, leaving one output slot unwritten (observed on a real + # 256k chain row: 1023 of 1024 filled, last slot -1). + self.p4_fine_rangetest = False if p4_fine_rangetest is None else bool(p4_fine_rangetest) # p4_lane_binsum: the per-warp bin sums that open both bin # searches indexed by warp only, so all 32 lanes walked the same # bins_per_warp bins - a dependency chain as long as the segment, @@ -579,6 +582,10 @@ def __init__( # bin index to do it; the same value-range compare the fine # recursion uses answers it without the subtract, multiply and # two clamps per candidate. + # DEFAULT OFF - same fp32 non-equivalence as p4_fine_rangetest, + # in the opposite direction: the range test admits a candidate + # the histogram binned elsewhere, so the scatter writes past the + # rank it reserved and out-of-range indices reach the output. self.p4_scat_rangetest = bool(p4_scat_rangetest) self.r0_qfracs = tuple(float(q) for q in r0_qfracs) if r0_qfracs else () if self.r0_qfracs: diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 0420bb2a926f..773a4d214549 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -69,7 +69,7 @@ def _compile( pdl_wait_late: bool = True, p4_fine_rangetest: "bool | None" = None, p4_lane_binsum: bool = True, - p4_scat_rangetest: bool = True, + p4_scat_rangetest: bool = False, use_ext_counts: bool = False, emit_xstate: bool = False, use_ext_cand: bool = False, @@ -675,7 +675,7 @@ def gvr_topk_decode( pdl_wait_late: bool = True, p4_fine_rangetest: Optional[bool] = None, p4_lane_binsum: bool = True, - p4_scat_rangetest: bool = True, + p4_scat_rangetest: bool = False, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, seed_thr: Optional[torch.Tensor] = None, From 66cc0320d0e516158e386c9762edfd632f2baa85 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:15:48 -0700 Subject: [PATCH 065/117] [None][perf] GVR routing: stay on the stock kernel below 8k context The assist tiers have a fixed-cost floor (~12us at K=1024, ~15us at K=512) that does not shrink with N. Once the row is short enough that the stock kernel finishes under that floor, no tier can win, and the emission tax makes it a straight loss. Worst-step grid over all layers and all decode steps (162 cells, one B200, emission tax charged), grouped by selectivity n_comp/K: n_comp/K cells geomean worst losing 1.0-1.5 9 0.793 0.717 9/9 1.5-3 18 1.004 0.908 8/18 3-6 18 1.031 0.910 2/18 6-20 36 1.094 0.943 5/36 20-80 36 1.174 0.941 2/36 80+ 45 1.721 0.880 2/45 The 1.0-1.5 band is V4-Pro at 4k context: 1027 candidates for K=1024, a top-k that selects nearly everything. Every cell there loses. plan_emission returns "none" below n_comp 4096 (~8k raw context at compress_ratio 4), including on the zero-emission rungs path - it is the same kernel, so it has the same floor - and the dsa call site falls through to the stock GVR branch. The ext state is left untouched below the gate, which is the cold-start case its closed loop already handles: xstate stays invalid, so the first step above the gate seeds from the stock path. Cost is flash 4k/8k at geomean 1.03, inside the +-3% run-to-run band. Grid moves from geomean 1.218 / worst 0.717 / 28 losing cells to 1.231 / 0.880 / 10. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 9 ++++++- .../blackwell/top_k/gvr_routing.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 45281f2cb6d4..b903659e411e 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2919,8 +2919,15 @@ def sparse_attn_indexer( # so we cap it at 256 for now and fall back to the CUDA C++ # indexer_topk_decode. This limit can be removed if GPU memory # is not a bottleneck. + # tier "none" = the router judged this shape too short for + # any assist to pay (gvr_routing.ASSIST_MIN_N_COMP); fall + # through to the stock GVR branch below. The ext state is + # left untouched, which is exactly the cold-start case its + # closed loop already handles: xstate stays invalid, so the + # first step above the gate seeds from the stock path. if (self.use_gvr_ext and self._gvr_ext is not None - and self._gvr_route is not None and next_n == 1 + and self._gvr_route is not None + and self._gvr_route.tier != "none" and next_n == 1 and num_gen_tokens <= 256): # emission-assisted GVR: consume what the indexer # epilogue emitted this step (packed seed row / diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index a3bd54ca6256..588203b4202b 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -42,6 +42,27 @@ # ---- measured thresholds (B200, f15 grid) -------------------------------- +# Below this compressed length no assist tier can pay for itself. The +# kernel has a fixed-cost floor (~12us at K=1024, ~15us at K=512) that +# does not shrink with N, and by n_comp/K ~ 1 the stock kernel finishes +# under that floor - there is nothing left to win. +# +# f17 worst-step grid (2026-07-30, 162 cells, one B200, emission tax +# charged) grouped by selectivity n_comp/K: +# +# n_comp/K cells geomean worst losing +# 1.0-1.5 9 0.793 0.717 9/9 <- pro 4k: 1027 candidates +# 1.5-3 18 1.004 0.908 8/18 for K=1024, a top-k that +# 3-6 18 1.031 0.910 2/18 selects nearly everything +# 6-20 36 1.094 0.943 5/36 +# 20-80 36 1.174 0.941 2/36 +# 80+ 45 1.721 0.880 2/45 +# +# Gating here costs flash 4k/8k (geomean 1.03, inside the +-3% +# run-to-run band) and takes the grid from geomean 1.218 / worst 0.717 +# / 28 losing cells to 1.231 / 0.880 / 10. +ASSIST_MIN_N_COMP = 4096 # ~8k raw context at compress_ratio 4 + # Block-skip prefix pays only when whole-row reads dominate. SKIP_MIN_N_COUNTS = 65536 # va: attach block_max unconditionally here up SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here @@ -84,6 +105,11 @@ def plan_emission(batch: int, n_comp: int, k: int, have_epilogue: bool) -> str: top-k kernel's N. Returns the tier name; the epilogue emits the matching buffers and the next top-k launch routes on them. """ + if n_comp < ASSIST_MIN_N_COMP: + # short rows: the stock kernel is already under our fixed cost, + # and this holds for the zero-emission rungs tier too - it is + # the same kernel, so the floor is the same + return "none" if not have_epilogue: return "rungs" # closed-loop lines cost nothing to carry if batch <= LIST_EMIT_MAX_B and n_comp >= LIST_EMIT_MIN_N: From d2ab3c1c3e0b8ac23150f75efd5550fa2ff5759a Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:57:21 -0700 Subject: [PATCH 066/117] [None][fix] GVR: keep the stock kernel path on upstream behaviour The emission work landed several changes that were not behind a compile-time gate, so a deployment that never enables the ext path still ran them. Measured on the worst-step protocol, our stock path against upstream over 36 short-context cells: geomean 0.955, worst 0.825. Split by K it is entirely the P4 tail repair - flash (K=512, the fast path is off) is 1.017, pro (K=1024, on) is 0.896 with 17 of 18 cells below parity. - p4_tail_v3 (new, default off): the compacted-class repair now sits behind a const_expr and upstream's thread0 serial select is the default branch again. Two colliding locals in the restored copy are suffixed so the DSL's function-scope analysis cannot mix the two siblings. - pdl_wait_late defaults to False: the entry-point wait is what upstream emits. (Measured separately: this knob was not the source of the regression, 0.955 -> 0.954.) - the ext min/max fast path moves inside its const_expr instead of being selected by a runtime compare, so the stock build never traces it. - fp4 indexer __call__: block_max / hit_stats / hit_bitmap move after stream and take defaults, restoring the upstream positional signature for callers that pass ten arguments. - p4_lane_binsum is removed. It gated nothing: the lane-parallel bin sum it described is upstream's own code, so the knob was dead and the A/B that "validated" it compared identical machine code. Deliberately NOT gated: the P1r degenerate-bracket rescue. Upstream returns identity [0, K) when prev_topk is degenerate, which is wrong on real data (42 of 231 rows on a V4-Flash TP2 capture, every request's first decode step). Gating it would put the defect back. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 10 +- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 8 +- .../blackwell/top_k/gvr_routing.py | 22 +- .../blackwell/top_k/gvr_topk_decode.py | 807 ++++++++++++------ .../cute_dsl_kernels/top_k/run_gvr_topk.py | 8 +- 5 files changed, 549 insertions(+), 306 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index b903659e411e..00953c7dba6e 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2919,12 +2919,10 @@ def sparse_attn_indexer( # so we cap it at 256 for now and fall back to the CUDA C++ # indexer_topk_decode. This limit can be removed if GPU memory # is not a bottleneck. - # tier "none" = the router judged this shape too short for - # any assist to pay (gvr_routing.ASSIST_MIN_N_COMP); fall - # through to the stock GVR branch below. The ext state is - # left untouched, which is exactly the cold-start case its - # closed loop already handles: xstate stays invalid, so the - # first step above the gate seeds from the stock path. + # tier "none" (gvr_routing.ASSIST_MIN_N_COMP): too short for + # any assist to pay - fall through to the stock branch. The + # untouched ext state reads as cold start, which its closed + # loop already handles. if (self.use_gvr_ext and self._gvr_ext is not None and self._gvr_route is not None and self._gvr_route.tier != "none" and next_n == 1 diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index 2481a5c5b8b3..dd587afc330c 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -818,10 +818,12 @@ def __call__( schedule_meta: cute.Tensor, # [num_sms+1, 2] int32 num_phys_blocks: cutlass.Int32, batch_size: cutlass.Int32, - block_max: cute.Tensor, # [num_rows, nb_pad*4] fp32 warp-partials (or None) - hit_stats: cute.Tensor, # [num_rows, 4] fp32 hit aggregate (or None unless emit_hit_stats) - hit_bitmap: cute.Tensor, # [batch, nb_pad*4] int32 (or None unless emit_block_meta) stream: cuda.CUstream, + # everything below is emission-only and defaulted, so the + # positional signature stays the one callers already use + block_max: cute.Tensor = None, # [num_rows, nb_pad*4] fp32 warp-partials + hit_stats: cute.Tensor = None, # [num_rows, 4] fp32 (emit_hit_stats) + hit_bitmap: cute.Tensor = None, # [batch, nb_pad*4] int32 (emit_block_meta) seed_thr: cute.Tensor = None, # [num_rows, 3] fp32 (emit_seed_counts) seed_counts: cute.Tensor = None, # [num_rows, 3] int32 out, caller-zeroed cand: cute.Tensor = None, # [num_rows, CAP*2] int32 {val bits, idx} pairs diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 588203b4202b..40453abe330f 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -42,25 +42,9 @@ # ---- measured thresholds (B200, f15 grid) -------------------------------- -# Below this compressed length no assist tier can pay for itself. The -# kernel has a fixed-cost floor (~12us at K=1024, ~15us at K=512) that -# does not shrink with N, and by n_comp/K ~ 1 the stock kernel finishes -# under that floor - there is nothing left to win. -# -# f17 worst-step grid (2026-07-30, 162 cells, one B200, emission tax -# charged) grouped by selectivity n_comp/K: -# -# n_comp/K cells geomean worst losing -# 1.0-1.5 9 0.793 0.717 9/9 <- pro 4k: 1027 candidates -# 1.5-3 18 1.004 0.908 8/18 for K=1024, a top-k that -# 3-6 18 1.031 0.910 2/18 selects nearly everything -# 6-20 36 1.094 0.943 5/36 -# 20-80 36 1.174 0.941 2/36 -# 80+ 45 1.721 0.880 2/45 -# -# Gating here costs flash 4k/8k (geomean 1.03, inside the +-3% -# run-to-run band) and takes the grid from geomean 1.218 / worst 0.717 -# / 28 losing cells to 1.231 / 0.880 / 10. +# The kernel's fixed cost does not shrink with N (~12us at K=1024), so +# once the stock kernel finishes under that floor no tier can win. The +# whole n_comp/K < 1.5 band measures below parity. ASSIST_MIN_N_COMP = 4096 # ~8k raw context at compress_ratio 4 # Block-skip prefix pays only when whole-row reads dominate. diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 5fb2727a74e7..76aab8f6a4e0 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -277,9 +277,8 @@ def __init__( smem_cache_elems: int = 32768, seqlen_sorted: bool = False, kc_diet: Optional[bool] = None, - pdl_wait_late: bool = True, + pdl_wait_late: bool = False, p4_fine_rangetest: Optional[bool] = None, - p4_lane_binsum: bool = True, p4_scat_rangetest: bool = False, enable_r0: bool = True, accept_cap: "int | None" = None, @@ -296,6 +295,7 @@ def __init__( enable_p4_rank_scatter_exact: Optional[bool] = None, p4_exact_tail: Optional[bool] = None, p4_tail_fast: Optional[bool] = None, # [p4tt] + p4_tail_v3: bool = False, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, enable_block_skip: bool = False, @@ -534,12 +534,9 @@ def __init__( # kernel passes False for BOTH member instances so their SMEM layouts # stay byte-identical (the DSL sizes the launch from the last-traced # SmemAllocator only; see GvrTopKLBKernel). - # pdl_wait_late: place the PDL wait after the prologue (row - # resolution + SMEM allocation + config folding) instead of at - # kernel entry, so that work - and its cold instruction fetch - - # overlaps the producer's tail. Kept as a knob so both - # placements can be A/B'd inside one process (cross-run cold - # drift on shared nodes is larger than the effect). + # pdl_wait_late: move the PDL wait past the prologue so that work + # overlaps the producer's tail. Off by default: the entry-point + # wait is what upstream emits. self.pdl_wait_late = bool(pdl_wait_late) if kc_diet is None: kc_diet = cluster_size == 1 @@ -571,12 +568,6 @@ def __init__( # other, leaving one output slot unwritten (observed on a real # 256k chain row: 1023 of 1024 filled, last slot -1). self.p4_fine_rangetest = False if p4_fine_rangetest is None else bool(p4_fine_rangetest) - # p4_lane_binsum: the per-warp bin sums that open both bin - # searches indexed by warp only, so all 32 lanes walked the same - # bins_per_warp bins - a dependency chain as long as the segment, - # with the whole warp doing identical work. Spread the segment - # across the lanes and close with one warp reduction instead. - self.p4_lane_binsum = bool(p4_lane_binsum) # p4_scat_rangetest: the scatter classifies each candidate as # above / inside / below the straddling bin and recomputed the # bin index to do it; the same value-range compare the fine @@ -769,6 +760,11 @@ def __init__( if p4_tail_fast is None: # [p4tt] p4_tail_fast = self.p4_exact_tail and top_k >= 1024 self.p4_tail_fast = bool(p4_tail_fast) and self.p4_exact_tail # [p4tt] + # p4_tail_v3: compacted-class repair (block-parallel radix + + # pure-tie pre-check) in place of the stock thread0 serial + # select. Off by default - the stock body is what upstream + # emits, and this rewrite only pays at K>=1024. + self.p4_tail_v3 = bool(p4_tail_v3) # ------------------------------------------------------------------ # SMEM slice cache loader. Streams this CTA's slice GMEM → SMEM via @@ -3545,20 +3541,20 @@ def phase4_rank_scatter( use_ext_r = cutlass.Int32(0) if cutlass.const_expr(ext_range_flag is not None): use_ext_r = ext_range_flag - if use_ext_r == cutlass.Int32(1): - # list rows: the take walk pre-zeroed the hist and staged - # per-warp maxima in smem_wcnt (its end barrier orders - # them); min := cut line by construction. The minmax - # scan, the zero pass and their three barriers vanish. - if cutlass.const_expr(ext_min is not None): - bmin_r = ext_min - for w in cutlass.range_constexpr(self.num_warps): - vmax = cutlass.Float32( - llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[w].ir_value()) - ) - bmax_r = cute.arch.fmax(bmax_r, vmax) - if bmax_r <= bmin_r: - bmax_r = bmin_r + cutlass.Float32(1e-6) + if use_ext_r == cutlass.Int32(1): + # list rows: the take walk pre-zeroed the hist and staged + # per-warp maxima in smem_wcnt (its end barrier orders + # them); min := cut line by construction. The minmax + # scan, the zero pass and their three barriers vanish. + if cutlass.const_expr(ext_min is not None): + bmin_r = ext_min + for w in cutlass.range_constexpr(self.num_warps): + vmax = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[w].ir_value()) + ) + bmax_r = cute.arch.fmax(bmax_r, vmax) + if bmax_r <= bmin_r: + bmax_r = bmin_r + cutlass.Float32(1e-6) if use_ext_r == cutlass.Int32(0): # ---- block min/max over candidates ---- local_cmin = cutlass.Float32(self.FLT_MAX) @@ -3889,269 +3885,532 @@ def phase4_rank_scatter( # (need ~100 x class ~100). Classes beyond capc take the # UNMODIFIED full-candidate radix below (verbatim copy). if cutlass.const_expr(self.p4_exact_tail and self.p4_tail_fast): # [p4tt] - need0 = cutlass.Int32(kK) - rank_above_fine - # [p4tt-v3] per-thread compact buffers, bounded by the - # strided trip count over the candidate array - nbuf7 = cutlass.const_expr((self.kC + self.num_threads - 1) // self.num_threads) - rv7 = cute.make_fragment((nbuf7,), cutlass.Float32) - ri7 = cute.make_fragment((nbuf7,), cutlass.Int32) - if cutlass.const_expr(_P4_TAIL_DBG or _P4_SUB_DBG): - if tidx == cutlass.Int32(0): - s_thr[1] = cutlass.Float32(cnt_strad) - s_thr[2] = cutlass.Float32(need0) - fast_done = cutlass.Int32(1) - if cnt_strad > need0 and need0 > cutlass.Int32(0): - fast_done = cutlass.Int32(0) - # [p4tt-v3] block-wide pure-tie check, ANY class - # size: min/max order key over the (b*, sb*) class. - # A pure-tie class needs NO repair — the scatter's - # arrival fill of bit-equal values is already - # value-set exact. Real fp8-lineage logits tie in - # the thousands, which used to take the full radix. - # Staging mirrors the head min/max (wcnt + hist - # slots [0..31], both dead here; pairs live at - # 260+). - kmn6 = cutlass.Int32(2147483647) - kmx6 = cutlass.Int32(-2147483648) - it6 = tidx - while it6 < cand_count: - v6 = smem_keys[it6] - b6 = cutlass.Int32((v6 - bmin_r) * inv1) - if b6 < cutlass.Int32(0): - b6 = cutlass.Int32(0) - if b6 > cutlass.Int32(kBins - 1): - b6 = cutlass.Int32(kBins - 1) - if b6 == b_star: - s6 = cutlass.Int32((v6 - f_lo) * finv) - if s6 < cutlass.Int32(0): - s6 = cutlass.Int32(0) - if s6 > cutlass.Int32(fbins - 1): - s6 = cutlass.Int32(fbins - 1) - if s6 == sb_star: - k6 = f32_order_key(v6) ^ cutlass.Int32(-2147483648) - if k6 < kmn6: - kmn6 = k6 - if k6 > kmx6: - kmx6 = k6 - it6 = it6 + cutlass.Int32(num_threads) - kmn6 = cute.arch.warp_redux_sync(kmn6, "min") - kmx6 = cute.arch.warp_redux_sync(kmx6, "max") - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = kmn6 - smem_hist[warp_id] = kmx6 - cute.arch.barrier() - kmn7 = cutlass.Int32(2147483647) - kmx7 = cutlass.Int32(-2147483648) - for w8 in cutlass.range_constexpr(self.num_warps): - pa8 = smem_wcnt[w8] - pb8 = smem_hist[w8] - if pa8 < kmn7: - kmn7 = pa8 - if pb8 > kmx7: - kmx7 = pb8 - if kmn7 == kmx7: - fast_done = cutlass.Int32(1) - if fast_done == cutlass.Int32(0): - # [p4tt-v3] mixed class: compact it IN PLACE - # into smem_keys/vals[0..cnt_strad) with a - # register-buffered two-phase pass (every - # thread reads its strided candidates first, - # ONE barrier, then claimed compact writes — - # no read/write overlap by construction). The - # candidate array has no readers after the - # tail, and compaction makes the repair cost a - # function of the CLASS size only, for ANY - # class size up to cand_count (the old full- - # candidate radix fallback is gone). + if cutlass.const_expr(self.p4_tail_v3): + need0 = cutlass.Int32(kK) - rank_above_fine + # [p4tt-v3] per-thread compact buffers, bounded by the + # strided trip count over the candidate array + nbuf7 = cutlass.const_expr( + (self.kC + self.num_threads - 1) // self.num_threads + ) + rv7 = cute.make_fragment((nbuf7,), cutlass.Float32) + ri7 = cute.make_fragment((nbuf7,), cutlass.Int32) + if cutlass.const_expr(_P4_TAIL_DBG or _P4_SUB_DBG): if tidx == cutlass.Int32(0): - s_iscalars[0] = cutlass.Int32(0) - nh7 = cutlass.Int32(0) - it7 = tidx - while it7 < cand_count: - v7 = smem_keys[it7] - b7 = cutlass.Int32((v7 - bmin_r) * inv1) - if b7 < cutlass.Int32(0): - b7 = cutlass.Int32(0) - if b7 > cutlass.Int32(kBins - 1): - b7 = cutlass.Int32(kBins - 1) - if b7 == b_star: - s7 = cutlass.Int32((v7 - f_lo) * finv) - if s7 < cutlass.Int32(0): - s7 = cutlass.Int32(0) - if s7 > cutlass.Int32(fbins - 1): - s7 = cutlass.Int32(fbins - 1) - if s7 == sb_star: - # static predicated fragment write - # (dodges dynamic register indexing) - for sl7 in cutlass.range_constexpr(nbuf7): - if cutlass.Int32(sl7) == nh7: - rv7[sl7] = v7 - ri7[sl7] = smem_vals[it7] - nh7 = nh7 + cutlass.Int32(1) - it7 = it7 + cutlass.Int32(num_threads) - cute.arch.barrier() - # warp-aggregated claim: intra-warp exclusive - # prefix via shfl scan + ONE atomic per warp - # (a thousand same-address claims serialize - # and scale with the class size) - pf7 = nh7 - for so3 in cutlass.range_constexpr(5): - oth3 = cute.arch.shuffle_sync_up( - pf7, cutlass.Int32(1 << so3), mask_and_clamp=0 - ) - if lane >= cutlass.Int32(1 << so3): - pf7 = pf7 + oth3 - tot7 = cute.arch.shuffle_sync(pf7, cutlass.Int32(31)) - wb7 = cutlass.Int32(0) - if lane == cutlass.Int32(31): - if tot7 > cutlass.Int32(0): - wb7 = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), tot7) - wb7 = cute.arch.shuffle_sync(wb7, cutlass.Int32(31)) - bs7 = wb7 + pf7 - nh7 - for sl8 in cutlass.range_constexpr(nbuf7): - if cutlass.Int32(sl8) < nh7: - smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] - smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] + s_thr[1] = cutlass.Float32(cnt_strad) + s_thr[2] = cutlass.Float32(need0) + fast_done = cutlass.Int32(1) + if cnt_strad > need0 and need0 > cutlass.Int32(0): + fast_done = cutlass.Int32(0) + # [p4tt-v3] block-wide pure-tie check, ANY class + # size: min/max order key over the (b*, sb*) class. + # A pure-tie class needs NO repair — the scatter's + # arrival fill of bit-equal values is already + # value-set exact. Real fp8-lineage logits tie in + # the thousands, which used to take the full radix. + # Staging mirrors the head min/max (wcnt + hist + # slots [0..31], both dead here; pairs live at + # 260+). + kmn6 = cutlass.Int32(2147483647) + kmx6 = cutlass.Int32(-2147483648) + it6 = tidx + while it6 < cand_count: + v6 = smem_keys[it6] + b6 = cutlass.Int32((v6 - bmin_r) * inv1) + if b6 < cutlass.Int32(0): + b6 = cutlass.Int32(0) + if b6 > cutlass.Int32(kBins - 1): + b6 = cutlass.Int32(kBins - 1) + if b6 == b_star: + s6 = cutlass.Int32((v6 - f_lo) * finv) + if s6 < cutlass.Int32(0): + s6 = cutlass.Int32(0) + if s6 > cutlass.Int32(fbins - 1): + s6 = cutlass.Int32(fbins - 1) + if s6 == sb_star: + k6 = f32_order_key(v6) ^ cutlass.Int32(-2147483648) + if k6 < kmn6: + kmn6 = k6 + if k6 > kmx6: + kmx6 = k6 + it6 = it6 + cutlass.Int32(num_threads) + kmn6 = cute.arch.warp_redux_sync(kmn6, "min") + kmx6 = cute.arch.warp_redux_sync(kmx6, "max") + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = kmn6 + smem_hist[warp_id] = kmx6 cute.arch.barrier() - if cnt_strad <= cutlass.Int32(128): - # warp0 exact pairwise rank (rank = #{key - # greater} + #{key equal, earlier slot} is - # unique in [0, class)) rewrites every - # winner slot in [raf, raf + need0) once. - if warp_id == cutlass.Int32(0): - ie5 = lane - while ie5 < cnt_strad: - vi5 = smem_keys[ie5] - ki5 = f32_order_key(vi5) ^ cutlass.Int32(-2147483648) - r5 = cutlass.Int32(0) - j5 = cutlass.Int32(0) - while j5 < cnt_strad: - vj5 = smem_keys[j5] - kj5 = f32_order_key(vj5) ^ cutlass.Int32(-2147483648) - if kj5 > ki5: - r5 = r5 + cutlass.Int32(1) - elif kj5 == ki5 and j5 < ie5: - r5 = r5 + cutlass.Int32(1) - j5 = j5 + cutlass.Int32(1) - if r5 < need0: - pos = rank_above_fine + r5 + kmn7 = cutlass.Int32(2147483647) + kmx7 = cutlass.Int32(-2147483648) + for w8 in cutlass.range_constexpr(self.num_warps): + pa8 = smem_wcnt[w8] + pb8 = smem_hist[w8] + if pa8 < kmn7: + kmn7 = pa8 + if pb8 > kmx7: + kmx7 = pb8 + if kmn7 == kmx7: + fast_done = cutlass.Int32(1) + if fast_done == cutlass.Int32(0): + # [p4tt-v3] mixed class: compact it IN PLACE + # into smem_keys/vals[0..cnt_strad) with a + # register-buffered two-phase pass (every + # thread reads its strided candidates first, + # ONE barrier, then claimed compact writes — + # no read/write overlap by construction). The + # candidate array has no readers after the + # tail, and compaction makes the repair cost a + # function of the CLASS size only, for ANY + # class size up to cand_count (the old full- + # candidate radix fallback is gone). + if tidx == cutlass.Int32(0): + s_iscalars[0] = cutlass.Int32(0) + nh7 = cutlass.Int32(0) + it7 = tidx + while it7 < cand_count: + v7 = smem_keys[it7] + b7 = cutlass.Int32((v7 - bmin_r) * inv1) + if b7 < cutlass.Int32(0): + b7 = cutlass.Int32(0) + if b7 > cutlass.Int32(kBins - 1): + b7 = cutlass.Int32(kBins - 1) + if b7 == b_star: + s7 = cutlass.Int32((v7 - f_lo) * finv) + if s7 < cutlass.Int32(0): + s7 = cutlass.Int32(0) + if s7 > cutlass.Int32(fbins - 1): + s7 = cutlass.Int32(fbins - 1) + if s7 == sb_star: + # static predicated fragment write + # (dodges dynamic register indexing) + for sl7 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl7) == nh7: + rv7[sl7] = v7 + ri7[sl7] = smem_vals[it7] + nh7 = nh7 + cutlass.Int32(1) + it7 = it7 + cutlass.Int32(num_threads) + cute.arch.barrier() + # warp-aggregated claim: intra-warp exclusive + # prefix via shfl scan + ONE atomic per warp + # (a thousand same-address claims serialize + # and scale with the class size) + pf7 = nh7 + for so3 in cutlass.range_constexpr(5): + oth3 = cute.arch.shuffle_sync_up( + pf7, cutlass.Int32(1 << so3), mask_and_clamp=0 + ) + if lane >= cutlass.Int32(1 << so3): + pf7 = pf7 + oth3 + tot7 = cute.arch.shuffle_sync(pf7, cutlass.Int32(31)) + wb7 = cutlass.Int32(0) + if lane == cutlass.Int32(31): + if tot7 > cutlass.Int32(0): + wb7 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), tot7 + ) + wb7 = cute.arch.shuffle_sync(wb7, cutlass.Int32(31)) + bs7 = wb7 + pf7 - nh7 + for sl8 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl8) < nh7: + smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] + smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] + cute.arch.barrier() + if cnt_strad <= cutlass.Int32(128): + # warp0 exact pairwise rank (rank = #{key + # greater} + #{key equal, earlier slot} is + # unique in [0, class)) rewrites every + # winner slot in [raf, raf + need0) once. + if warp_id == cutlass.Int32(0): + ie5 = lane + while ie5 < cnt_strad: + vi5 = smem_keys[ie5] + ki5 = f32_order_key(vi5) ^ cutlass.Int32(-2147483648) + r5 = cutlass.Int32(0) + j5 = cutlass.Int32(0) + while j5 < cnt_strad: + vj5 = smem_keys[j5] + kj5 = f32_order_key(vj5) ^ cutlass.Int32( + -2147483648 + ) + if kj5 > ki5: + r5 = r5 + cutlass.Int32(1) + elif kj5 == ki5 and j5 < ie5: + r5 = r5 + cutlass.Int32(1) + j5 = j5 + cutlass.Int32(1) + if r5 < need0: + pos = rank_above_fine + r5 + if pos < cutlass.Int32(kK): + if cutlass.const_expr( + self.return_output_values + ): + output_values_row[pos] = self.dtype(vi5) + output_indices_row[pos] = smem_vals[ie5] + ie5 = ie5 + cutlass.Int32(32) + cute.arch.barrier() + else: + # block-parallel 4-level MSB radix over the + # compacted class (scans touch class pairs + # only; warp0 shuffle-scan digit search — 3 + # block barriers per level instead of 5). + if tidx == cutlass.Int32(0): + smem_hist[256] = cutlass.Int32(0) + smem_hist[257] = need0 + smem_hist[258] = cutlass.Int32(0) + cute.arch.barrier() + for lvl2 in cutlass.range_constexpr(4): + shift2 = cutlass.const_expr(24 - 8 * lvl2) + iz3 = tidx + while iz3 < cutlass.Int32(256): + smem_hist[iz3] = cutlass.Int32(0) + iz3 = iz3 + cutlass.Int32(num_threads) + cute.arch.barrier() + uthr_c2 = smem_hist[256] + ic2 = tidx + while ic2 < cnt_strad: + uk3 = f32_order_key(smem_keys[ic2]) + pm2 = cutlass.Int32(1) + if cutlass.const_expr(lvl2 > 0): + if (uk3 >> cutlass.Int32(shift2 + 8)) != ( + uthr_c2 >> cutlass.Int32(shift2 + 8) + ): + pm2 = cutlass.Int32(0) + if pm2 == cutlass.Int32(1): + dg2 = ( + uk3 >> cutlass.Int32(shift2) + ) & cutlass.Int32(0xFF) + atomicAdd( + smem_hist.iterator + dg2, cutlass.Int32(1) + ) + ic2 = ic2 + cutlass.Int32(num_threads) + cute.arch.barrier() + if warp_id == cutlass.Int32(0): + ws3 = cutlass.Int32(0) + for jd3 in cutlass.range_constexpr(8): + di3 = ( + cutlass.Int32(255) + - lane * cutlass.Int32(8) + - cutlass.Int32(jd3) + ) + ws3 = ws3 + smem_hist[di3] + pre6 = ws3 + for so2 in cutlass.range_constexpr(5): + oth2 = cute.arch.shuffle_sync_up( + pre6, + cutlass.Int32(1 << so2), + mask_and_clamp=0, + ) + if lane >= cutlass.Int32(1 << so2): + pre6 = pre6 + oth2 + needl3 = smem_hist[257] + if pre6 >= needl3 and (pre6 - ws3) < needl3: + base5 = pre6 - ws3 + dstar2 = cutlass.Int32(0) + above5 = base5 + sd5 = cutlass.Int32(0) + for jd4 in cutlass.range_constexpr(8): + di4 = ( + cutlass.Int32(255) + - lane * cutlass.Int32(8) + - cutlass.Int32(jd4) + ) + ra5 = base5 + base5 = base5 + smem_hist[di4] + if base5 >= needl3 and sd5 == cutlass.Int32(0): + dstar2 = di4 + above5 = ra5 + sd5 = cutlass.Int32(1) + smem_hist[256] = uthr_c2 | ( + dstar2 << cutlass.Int32(shift2) + ) + smem_hist[257] = needl3 - above5 + smem_hist[258] = smem_hist[258] + above5 + cute.arch.barrier() + u_thr2 = smem_hist[256] + cnt_ab2 = smem_hist[258] + need_eq2 = smem_hist[257] + kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) + if tidx == cutlass.Int32(0): + s_iscalars[4] = cutlass.Int32(0) + s_iscalars[0] = cutlass.Int32(0) + cute.arch.barrier() + ir3 = tidx + while ir3 < cnt_strad: + vv3 = smem_keys[ir3] + uk4 = f32_order_key(vv3) + ks4 = uk4 ^ cutlass.Int32(-2147483648) + if ks4 > kthr2: + o4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(4), + cutlass.Int32(1), + ) + pos = rank_above_fine + o4 if pos < cutlass.Int32(kK): if cutlass.const_expr(self.return_output_values): - output_values_row[pos] = self.dtype(vi5) - output_indices_row[pos] = smem_vals[ie5] - ie5 = ie5 + cutlass.Int32(32) + output_values_row[pos] = self.dtype(vv3) + output_indices_row[pos] = smem_vals[ir3] + elif ks4 == kthr2: + q4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if q4 < need_eq2: + pos = rank_above_fine + cnt_ab2 + q4 + if pos < cutlass.Int32(kK): + if cutlass.const_expr( + self.return_output_values + ): + output_values_row[pos] = self.dtype(vv3) + output_indices_row[pos] = smem_vals[ir3] + ir3 = ir3 + cutlass.Int32(num_threads) + cute.arch.barrier() + else: + need0_s = cutlass.Int32(kK) - rank_above_fine + if cnt_strad > need0_s and need0_s > cutlass.Int32(0): + if cnt_strad <= cutlass.Int32(128): + # [p4tt] SMEM: (value_bits, cand_idx) pairs at + # smem_hist[2*o]/[2*o+1], o < 128 (slots 0..255). + # The 256 digit bins are dead here (the fast path + # replaces the radix levels that used them); the + # sb_star/ra staging in slots 2/3 was read by + # every thread before the pre-scatter barrier. + # Persistent radix scalars [256..258] untouched. + # Collect counter = s_iscalars[0] (dead after the + # scatter; same reuse as the radix rewrite pass). + if tidx == cutlass.Int32(0): + s_iscalars[0] = cutlass.Int32(0) + cute.arch.barrier() + itc = tidx + while itc < cand_count: + tv = smem_keys[itc] + tb = cutlass.Int32((tv - bmin_r) * inv1) + if tb < cutlass.Int32(0): + tb = cutlass.Int32(0) + if tb > cutlass.Int32(kBins - 1): + tb = cutlass.Int32(kBins - 1) + if tb == b_star: + ts = cutlass.Int32((tv - f_lo) * finv) + if ts < cutlass.Int32(0): + ts = cutlass.Int32(0) + if ts > cutlass.Int32(fbins - 1): + ts = cutlass.Int32(fbins - 1) + if ts == sb_star: + to = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if to < cutlass.Int32(128): + smem_hist[to + to] = float_as_int32(tv) + smem_hist[to + to + cutlass.Int32(1)] = smem_vals[ + itc + ] + itc = itc + cutlass.Int32(num_threads) + cute.arch.barrier() + # [p4tt] thread0 exact top-need0_s select rewriting + # positions [rank_above_fine, kK). Consumed flag = + # the cand_idx slot set to -1 (indices are always + # >= 0), so a genuine -FLT_MAX value in the class + # remains selectable (no value sentinel). Ties + # (bit-equal values) pick arbitrarily: value-set + # exact. + if tidx == cutlass.Int32(0): + tj = cutlass.Int32(0) + while tj < need0_s: + tbv = cutlass.Float32(self.NEG_FLT_MAX) + tbi = cutlass.Int32(-1) + ti = cutlass.Int32(0) + while ti < cnt_strad: + tvi = smem_hist[ti + ti + cutlass.Int32(1)] + if tvi >= cutlass.Int32(0): + tvb = smem_hist[ti + ti] + tvv = cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + tvb.ir_value(), + ) + ) + take = cutlass.Int32(0) + if tbi < cutlass.Int32(0): + take = cutlass.Int32(1) + elif tvv > tbv: + take = cutlass.Int32(1) + if take == cutlass.Int32(1): + tbv = tvv + tbi = ti + ti = ti + cutlass.Int32(1) + pos_s = rank_above_fine + tj + if cutlass.const_expr(self.return_output_values): + output_values_row[pos_s] = self.dtype(tbv) + output_indices_row[pos_s] = smem_hist[ + tbi + tbi + cutlass.Int32(1) + ] + smem_hist[tbi + tbi + cutlass.Int32(1)] = cutlass.Int32(-1) + tj = tj + cutlass.Int32(1) cute.arch.barrier() else: - # block-parallel 4-level MSB radix over the - # compacted class (scans touch class pairs - # only; warp0 shuffle-scan digit search — 3 - # block barriers per level instead of 5). + # Persistent scalars live above the 256 digit bins + # (kNumBins >= 512 always): [256] key prefix (chosen + # digits, remaining bits 0), [257] slots still to fill + # inside the current equal-prefix set, [258] ties + # strictly above the prefix (their slots precede it). if tidx == cutlass.Int32(0): smem_hist[256] = cutlass.Int32(0) - smem_hist[257] = need0 + smem_hist[257] = need0_s smem_hist[258] = cutlass.Int32(0) cute.arch.barrier() - for lvl2 in cutlass.range_constexpr(4): - shift2 = cutlass.const_expr(24 - 8 * lvl2) - iz3 = tidx - while iz3 < cutlass.Int32(256): - smem_hist[iz3] = cutlass.Int32(0) - iz3 = iz3 + cutlass.Int32(num_threads) + for lvl in cutlass.range_constexpr(4): + shift = cutlass.const_expr(24 - 8 * lvl) + iz2 = tidx + while iz2 < cutlass.Int32(256): + smem_hist[iz2] = cutlass.Int32(0) + iz2 = iz2 + cutlass.Int32(num_threads) cute.arch.barrier() - uthr_c2 = smem_hist[256] - ic2 = tidx - while ic2 < cnt_strad: - uk3 = f32_order_key(smem_keys[ic2]) - pm2 = cutlass.Int32(1) - if cutlass.const_expr(lvl2 > 0): - if (uk3 >> cutlass.Int32(shift2 + 8)) != ( - uthr_c2 >> cutlass.Int32(shift2 + 8) - ): - pm2 = cutlass.Int32(0) - if pm2 == cutlass.Int32(1): - dg2 = (uk3 >> cutlass.Int32(shift2)) & cutlass.Int32( - 0xFF - ) - atomicAdd(smem_hist.iterator + dg2, cutlass.Int32(1)) - ic2 = ic2 + cutlass.Int32(num_threads) + uthr_cur = smem_hist[256] + it2 = tidx + while it2 < cand_count: + vt = smem_keys[it2] + bt = cutlass.Int32((vt - bmin_r) * inv1) + if bt < cutlass.Int32(0): + bt = cutlass.Int32(0) + if bt > cutlass.Int32(kBins - 1): + bt = cutlass.Int32(kBins - 1) + if bt == b_star: + st2 = cutlass.Int32((vt - f_lo) * finv) + if st2 < cutlass.Int32(0): + st2 = cutlass.Int32(0) + if st2 > cutlass.Int32(fbins - 1): + st2 = cutlass.Int32(fbins - 1) + if st2 == sb_star: + uk = f32_order_key(vt) + pmatch = cutlass.Int32(1) + if cutlass.const_expr(lvl > 0): + if (uk >> cutlass.Int32(shift + 8)) != ( + uthr_cur >> cutlass.Int32(shift + 8) + ): + pmatch = cutlass.Int32(0) + if pmatch == cutlass.Int32(1): + dg = ( + uk >> cutlass.Int32(shift) + ) & cutlass.Int32(0xFF) + atomicAdd( + smem_hist.iterator + dg, cutlass.Int32(1) + ) + it2 = it2 + cutlass.Int32(num_threads) cute.arch.barrier() - if warp_id == cutlass.Int32(0): - ws3 = cutlass.Int32(0) - for jd3 in cutlass.range_constexpr(8): - di3 = ( + # Two-stage descending digit scan (mirrors the + # fine 3-step search): per-warp partial sums, + # thread0 picks the target warp, its lane0 walks + # the warp's digit range — 2*num_warps serial + # steps instead of 256. + fdw = cutlass.const_expr(256 // self.num_warps) + wsum2 = cutlass.Int32(0) + for jd in cutlass.range_constexpr(fdw): + dix = ( + cutlass.Int32(255) + - warp_id * cutlass.Int32(fdw) + - cutlass.Int32(jd) + ) + wsum2 = wsum2 + smem_hist[dix] + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = wsum2 + cute.arch.barrier() + if tidx == cutlass.Int32(0): + needl = smem_hist[257] + cw = cutlass.Int32(0) + tw3 = cutlass.Int32(num_warps - 1) + f3 = cutlass.Int32(0) + for w4 in cutlass.range_constexpr(self.num_warps): + cw = cw + smem_wcnt[w4] + if cw >= needl and f3 == cutlass.Int32(0): + tw3 = cutlass.Int32(w4) + f3 = cutlass.Int32(1) + pre3 = cutlass.Int32(0) + for w5 in cutlass.range_constexpr(self.num_warps): + if cutlass.Int32(w5) < tw3: + pre3 = pre3 + smem_wcnt[w5] + s_iscalars[4] = pre3 # prefix above target warp + s_iscalars[0] = tw3 # target warp + cute.arch.barrier() + pre4 = s_iscalars[4] + tw4 = s_iscalars[0] + if warp_id == tw4 and lane == cutlass.Int32(0): + needl2 = smem_hist[257] + base4 = pre4 + dstar = cutlass.Int32(0) + above_d = pre4 + sd4 = cutlass.Int32(0) + for jd2 in cutlass.range_constexpr(fdw): + dix2 = ( cutlass.Int32(255) - - lane * cutlass.Int32(8) - - cutlass.Int32(jd3) + - tw4 * cutlass.Int32(fdw) + - cutlass.Int32(jd2) ) - ws3 = ws3 + smem_hist[di3] - pre6 = ws3 - for so2 in cutlass.range_constexpr(5): - oth2 = cute.arch.shuffle_sync_up( - pre6, - cutlass.Int32(1 << so2), - mask_and_clamp=0, - ) - if lane >= cutlass.Int32(1 << so2): - pre6 = pre6 + oth2 - needl3 = smem_hist[257] - if pre6 >= needl3 and (pre6 - ws3) < needl3: - base5 = pre6 - ws3 - dstar2 = cutlass.Int32(0) - above5 = base5 - sd5 = cutlass.Int32(0) - for jd4 in cutlass.range_constexpr(8): - di4 = ( - cutlass.Int32(255) - - lane * cutlass.Int32(8) - - cutlass.Int32(jd4) - ) - ra5 = base5 - base5 = base5 + smem_hist[di4] - if base5 >= needl3 and sd5 == cutlass.Int32(0): - dstar2 = di4 - above5 = ra5 - sd5 = cutlass.Int32(1) - smem_hist[256] = uthr_c2 | ( - dstar2 << cutlass.Int32(shift2) - ) - smem_hist[257] = needl3 - above5 - smem_hist[258] = smem_hist[258] + above5 + ra4 = base4 + base4 = base4 + smem_hist[dix2] + if base4 >= needl2 and sd4 == cutlass.Int32(0): + dstar = dix2 + above_d = ra4 + sd4 = cutlass.Int32(1) + smem_hist[256] = uthr_cur | (dstar << cutlass.Int32(shift)) + smem_hist[257] = needl2 - above_d + smem_hist[258] = smem_hist[258] + above_d cute.arch.barrier() - u_thr2 = smem_hist[256] - cnt_ab2 = smem_hist[258] - need_eq2 = smem_hist[257] - kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) + # Rewrite the tie slot range: ties with key > u_thr + # first (there are exactly cnt_ab of them), then the + # first need_eq bitwise-equal-to-u_thr ties in arrival + # order (value-exact by construction). Signed compare + # needs the top bit flipped (unsigned-monotonic key). + u_thr = smem_hist[256] + cnt_ab = smem_hist[258] + need_eq = smem_hist[257] + ks_thr = u_thr ^ cutlass.Int32(-2147483648) if tidx == cutlass.Int32(0): - s_iscalars[4] = cutlass.Int32(0) - s_iscalars[0] = cutlass.Int32(0) + s_iscalars[4] = cutlass.Int32(0) # above-writer ctr + s_iscalars[0] = cutlass.Int32(0) # equal-writer ctr cute.arch.barrier() - ir3 = tidx - while ir3 < cnt_strad: - vv3 = smem_keys[ir3] - uk4 = f32_order_key(vv3) - ks4 = uk4 ^ cutlass.Int32(-2147483648) - if ks4 > kthr2: - o4 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(4), - cutlass.Int32(1), - ) - pos = rank_above_fine + o4 - if pos < cutlass.Int32(kK): - if cutlass.const_expr(self.return_output_values): - output_values_row[pos] = self.dtype(vv3) - output_indices_row[pos] = smem_vals[ir3] - elif ks4 == kthr2: - q4 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), - cutlass.Int32(1), - ) - if q4 < need_eq2: - pos = rank_above_fine + cnt_ab2 + q4 - if pos < cutlass.Int32(kK): - if cutlass.const_expr(self.return_output_values): - output_values_row[pos] = self.dtype(vv3) - output_indices_row[pos] = smem_vals[ir3] - ir3 = ir3 + cutlass.Int32(num_threads) + ir2 = tidx + while ir2 < cand_count: + vr = smem_keys[ir2] + br = cutlass.Int32((vr - bmin_r) * inv1) + if br < cutlass.Int32(0): + br = cutlass.Int32(0) + if br > cutlass.Int32(kBins - 1): + br = cutlass.Int32(kBins - 1) + if br == b_star: + sr = cutlass.Int32((vr - f_lo) * finv) + if sr < cutlass.Int32(0): + sr = cutlass.Int32(0) + if sr > cutlass.Int32(fbins - 1): + sr = cutlass.Int32(fbins - 1) + if sr == sb_star: + uk2 = f32_order_key(vr) + ks2 = uk2 ^ cutlass.Int32(-2147483648) + if ks2 > ks_thr: + o2 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(4), + cutlass.Int32(1), + ) + pos_s = rank_above_fine + o2 + if pos_s < cutlass.Int32(kK): + if cutlass.const_expr( + self.return_output_values + ): + output_values_row[pos_s] = self.dtype(vr) + output_indices_row[pos_s] = smem_vals[ir2] + elif ks2 == ks_thr: + q2 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if q2 < need_eq: + pos_s = rank_above_fine + cnt_ab + q2 + if pos_s < cutlass.Int32(kK): + if cutlass.const_expr( + self.return_output_values + ): + output_values_row[pos_s] = self.dtype( + vr + ) + output_indices_row[pos_s] = smem_vals[ir2] + ir2 = ir2 + cutlass.Int32(num_threads) cute.arch.barrier() elif cutlass.const_expr(self.p4_exact_tail): # [p4tt] if->elif only need0 = cutlass.Int32(kK) - rank_above_fine diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 773a4d214549..30ba825ec8ee 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -67,8 +67,8 @@ def _compile( p2_warp_redundant: bool = True, enable_block_skip: bool = False, pdl_wait_late: bool = True, + p4_tail_v3: bool = False, p4_fine_rangetest: "bool | None" = None, - p4_lane_binsum: bool = True, p4_scat_rangetest: bool = False, use_ext_counts: bool = False, emit_xstate: bool = False, @@ -208,8 +208,8 @@ def _compile( p2_warp_redundant=p2_warp_redundant, enable_block_skip=enable_block_skip, pdl_wait_late=pdl_wait_late, + p4_tail_v3=p4_tail_v3, p4_fine_rangetest=p4_fine_rangetest, - p4_lane_binsum=p4_lane_binsum, p4_scat_rangetest=p4_scat_rangetest, use_ext_counts=use_ext_counts, emit_xstate=emit_xstate, @@ -673,8 +673,8 @@ def gvr_topk_decode( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, pdl_wait_late: bool = True, + p4_tail_v3: bool = False, p4_fine_rangetest: Optional[bool] = None, - p4_lane_binsum: bool = True, p4_scat_rangetest: bool = False, block_max: Optional[torch.Tensor] = None, skip_min_n: Optional[int] = 200_000, @@ -963,8 +963,8 @@ def gvr_topk_decode( p2_warp_redundant, enable_block_skip, pdl_wait_late, + p4_tail_v3, p4_fine_rangetest, - p4_lane_binsum, p4_scat_rangetest, use_ext_counts, emit_xstate, From c65dc31834c12f4151611d229b8f5f31134f9397 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:03:50 -0700 Subject: [PATCH 067/117] [None][chore] GVR: condense the narrative comment blocks Drop the version tags and the measurement logs from the kernel comments; keep the contract and the DSL traps. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 153 +++++++----------- 1 file changed, 62 insertions(+), 91 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 76aab8f6a4e0..3d5d29c3e0af 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -458,17 +458,13 @@ def __init__( self.enable_r0 = bool(enable_r0) self.mt_unroll = int(mt_unroll) self.fb_fix = bool(fb_fix) - # enable_block_skip: gate the R0 M-ary count pass and the Phase-3 - # stream-write on per-32-position upper bounds emitted by the - # indexer epilogue (``block_max [num_rows, nb_pad*4]`` fp32, record - # r = exact max of positions [r*32, r*32+32) of the POST-CONVERSION - # stored logits — contract in workspace/epilogue_topk_interface.md). - # Lossless: the active-block list is built at the LOOSEST rung, so a - # skipped block cannot contain any element >= any rung and every - # rung count (and the collect) equals its dense value. The port - # ships the measured-optimal configuration only: grain 32, int16 - # list entries (16KB SMEM, 3 CTA/SM at T512), strided coalesced - # O(1)-barrier build, UN=2 software-pipelined compact scan. + # enable_block_skip: gate the R0 count pass and the Phase-3 + # stream-write on per-32-position upper bounds from the indexer + # epilogue (block_max [num_rows, nb_pad*4] fp32; record r = max + # over positions [r*32, r*32+32) of the stored logits). Lossless: + # the active list is built at the loosest rung, so a skipped + # block holds nothing >= any rung and every count equals its + # dense value. self.enable_block_skip = bool(enable_block_skip) self.SKIP_BLOCK = 32 self.SKIP_BLOCK_LOG2 = 5 @@ -556,17 +552,11 @@ def __init__( # measured as a wash under the same protocol and stay stock. if enable_r0 and top_k == 2048 and self.kNumBins > 512: self.kNumBins = 512 - # p4_fine_rangetest: the fine recursion exists only to locate - # the handful of candidates inside the straddling coarse bin, so - # filter them with a value-range compare instead of recomputing - # each candidate's bin (subtract + multiply + two clamps). - # DEFAULT OFF - not bit-equivalent to the bin recompute it - # replaces. v in [f_lo, f_hi) and floor((v - bmin) * inv1) == - # b_star agree in exact arithmetic but not in fp32, and the - # scatter below still classifies by bin recompute. A candidate - # the two passes disagree on is counted by one and placed by the - # other, leaving one output slot unwritten (observed on a real - # 256k chain row: 1023 of 1024 filled, last slot -1). + # p4_fine_rangetest: filter the fine recursion by value range + # instead of recomputing each candidate's bin. OFF - the two are + # not fp32-equivalent, and the scatter still classifies by bin + # recompute, so a candidate they disagree on is counted by one + # pass and placed by the other, leaving an output slot unwritten. self.p4_fine_rangetest = False if p4_fine_rangetest is None else bool(p4_fine_rangetest) # p4_scat_rangetest: the scatter classifies each candidate as # above / inside / below the straddling bin and recomputed the @@ -634,18 +624,14 @@ def __init__( # segment that can void). self.list_cap = max(0, int(cand_cap) - 2 * self.accept_cap) self.cand_rung = int(cand_rung) - # self_scan (fused self-contained mode): the kernel itself streams - # the row ONCE against the three closed-loop lines, bucketing - # VALUES into on-chip segments (A >= t2 / B [t1,t2) / C [t0,t1), - # bases 0 / accept_cap / 2*accept_cap inside an enlarged - # smem_keys) and POSITIONS into the cand_idx tensor (write-only - # until the deferred K-gather). No external emitter, no candidate - # value column in gmem. A line cut compacts the winning segment - # runs to the smem_keys prefix and fills smem_vals with each - # entry's SEGMENT COORDINATE — from there on the v5 consumer - # (P4, tail repair, deferred gather via cand_idx[coord]) runs - # unchanged. Ineligible rows take the stock fallback, whose - # P3/P4 use smem_keys[:kC]/smem_vals verbatim. + # self_scan (fused self-contained mode): the kernel streams the + # row once against the three closed-loop lines, bucketing values + # into on-chip segments (A >= t2 / B [t1,t2) / C [t0,t1) at bases + # 0 / accept_cap / 2*accept_cap) and positions into cand_idx. A + # line cut compacts the winning segment to the smem_keys prefix + # and fills smem_vals with segment coordinates, after which the + # list consumer runs unchanged. Ineligible rows take the stock + # fallback. self.self_scan = bool(self_scan) if self.self_scan: if not use_ext_counts: @@ -743,7 +729,7 @@ def __init__( if p4_exact_tail is None: p4_exact_tail = self.enable_p4_rank_scatter_exact and dtype == cutlass.Float32 self.p4_exact_tail = bool(p4_exact_tail) and self.enable_p4_rank_scatter_exact - # [p4tt] p4_tail_fast: tiny-tie COLLECT+SELECT fast path inside the + # p4_tail_fast: tiny-tie COLLECT+SELECT fast path inside the # exact-tail fire branch. When the (b*, sb*) tie class holds <= 128 # entries (the real firing cells have 2), ONE candidate pass collects # (value_bits, cand_idx) pairs into SMEM and thread0 selects the @@ -1437,7 +1423,7 @@ def phase0_scan_bucket( # just the one 32B bmax vector). bm_addr = block_max_row.iterator.toint() nb0 = (N + cutlass.Int32(31)) >> cutlass.Int32(5) - # v9 two-pass skip: (1) DENSE-scan the bmax array itself (it + # Two-pass skip: (1) DENSE-scan the bmax array itself (it # is 1/32 of the row) with the tuned vector loop, compacting # PASSING BLOCK IDS into the idle C segment (single-band mode # never fills C; ids < 2^23 store exactly as floats); @@ -1614,7 +1600,7 @@ def phase0_scan_bucket( cpw = cutlass.const_expr(4) # cp.async caps at 16B per copy step1 = cutlass.const_expr(num_threads * cpw) st2log = cutlass.const_expr((2 * step1).bit_length() - 1) - # v14 pair-step cp.async pipeline: the scan is instruction-issue + # Pair-step cp.async pipeline: the scan is instruction-issue # bound (pcsamp: no_instructions + wait dominate; long_scoreboard # is 6%), so each step processes TWO 16B vectors per thread — # loop/wait/commit/address overhead amortizes over 8 elements @@ -2249,17 +2235,14 @@ def block_count_ge_multi( cnt_frag[m] = cnt_frag[m] + cutlass.Int32(vh >= thr_frag[m]) hh = hh + cutlass.Int32(num_threads) # Rung-tightening build (cs==1): a rung whose active list - # exceeds CAP blocks is provably or near-provably - # unacceptable (count >= list length; on real low-hit-rate - # rows the loosest sample-quantile rung retains 60%+ of the - # blocks and destroys the skip). DROP it — a dropped rung is - # merely an unmeasured probe (recorded in the mask at - # s_active_cnt[2]; classify and the fallback seeding skip - # it) — and rebuild at the next tighter threshold. Bounded - # by M-1 extra builds (~2-5us each at nb=8192). At cs>1 the - # per-CTA list lengths differ, so the drop decision would - # diverge across the cluster: keep the plain loosest-rung - # build there. + # exceeds CAP blocks cannot be accepted (count >= list + # length), so drop it and rebuild at the next tighter + # threshold. A dropped rung is only an unmeasured probe, + # recorded in the mask at s_active_cnt[2] and skipped by + # classify and by the fallback seeding. Bounded by M-1 + # extra builds. At cs>1 the per-CTA list lengths differ + # and the drop decision would diverge across the cluster, + # so keep the plain loosest-rung build there. CAP_BLOCKS = cutlass.const_expr(3 * self.kC // 4) if cutlass.const_expr(cluster_size == 1): build_done = cutlass.Int32(0) @@ -3874,7 +3857,7 @@ def phase4_rank_scatter( # slot range [rank_above_fine, kK). Unambiguous rows (the # overwhelming majority) pay two scalar compares; the counters # and the fine histogram are reused, so SMEM does not grow. - # [p4tt] boundary-class repair: collect the (b*, sb*) tie + # boundary-class repair: collect the (b*, sb*) tie # class compactly (ONE candidate pass), then select inside # it. Tiny jobs (need x class <= 512) keep the thread0 # serial select (cheapest at that size). Bigger classes up @@ -3887,7 +3870,7 @@ def phase4_rank_scatter( if cutlass.const_expr(self.p4_exact_tail and self.p4_tail_fast): # [p4tt] if cutlass.const_expr(self.p4_tail_v3): need0 = cutlass.Int32(kK) - rank_above_fine - # [p4tt-v3] per-thread compact buffers, bounded by the + # per-thread compact buffers, bounded by the # strided trip count over the candidate array nbuf7 = cutlass.const_expr( (self.kC + self.num_threads - 1) // self.num_threads @@ -3901,7 +3884,7 @@ def phase4_rank_scatter( fast_done = cutlass.Int32(1) if cnt_strad > need0 and need0 > cutlass.Int32(0): fast_done = cutlass.Int32(0) - # [p4tt-v3] block-wide pure-tie check, ANY class + # block-wide pure-tie check, ANY class # size: min/max order key over the (b*, sb*) class. # A pure-tie class needs NO repair — the scatter's # arrival fill of bit-equal values is already @@ -3951,7 +3934,7 @@ def phase4_rank_scatter( if kmn7 == kmx7: fast_done = cutlass.Int32(1) if fast_done == cutlass.Int32(0): - # [p4tt-v3] mixed class: compact it IN PLACE + # mixed class: compact it IN PLACE # into smem_keys/vals[0..cnt_strad) with a # register-buffered two-phase pass (every # thread reads its strided candidates first, @@ -4166,7 +4149,7 @@ def phase4_rank_scatter( need0_s = cutlass.Int32(kK) - rank_above_fine if cnt_strad > need0_s and need0_s > cutlass.Int32(0): if cnt_strad <= cutlass.Int32(128): - # [p4tt] SMEM: (value_bits, cand_idx) pairs at + # SMEM: (value_bits, cand_idx) pairs at # smem_hist[2*o]/[2*o+1], o < 128 (slots 0..255). # The 256 digit bins are dead here (the fast path # replaces the radix levels that used them); the @@ -4204,7 +4187,7 @@ def phase4_rank_scatter( ] itc = itc + cutlass.Int32(num_threads) cute.arch.barrier() - # [p4tt] thread0 exact top-need0_s select rewriting + # thread0 exact top-need0_s select rewriting # positions [rank_above_fine, kK). Consumed flag = # the cand_idx slot set to -1 (indices are always # >= 0), so a genuine -FLT_MAX value in the class @@ -4412,7 +4395,7 @@ def phase4_rank_scatter( output_indices_row[pos_s] = smem_vals[ir2] ir2 = ir2 + cutlass.Int32(num_threads) cute.arch.barrier() - elif cutlass.const_expr(self.p4_exact_tail): # [p4tt] if->elif only + elif cutlass.const_expr(self.p4_exact_tail): # if->elif only need0 = cutlass.Int32(kK) - rank_above_fine if cnt_strad > need0 and need0 > cutlass.Int32(0): # Persistent scalars live above the 256 digit bins @@ -6125,24 +6108,17 @@ def _run_phases( if cutlass.const_expr(self.emit_xstate): xstate_row[0] = cutlass.Float32(0.0) # degenerate else: - # ---- List path v4: known-counts admission ---- - # The emitter wrote the SoA list (score column + position - # column, sentinel score -inf) collected at t0 = seed_thr[0] - # and COUNTED the two tighter lines on the way out: the - # control words carry {n0, void, n1, n2} with n_i = #(>= t_i), - # n0 >= n1 >= n2. Admission and cut selection are pure scalar - # lookups - no in-kernel counting, no staging, no gamble: - # 1. some n_i lands in the acceptance band [K, B*] -> - # cut at the TIGHTEST such line, ONE filtered pass. - # 2. the band is straddled or overshot by every line -> - # a histogram over the gmem list CLAMPED between the - # two known bracket lines finds an in-band edge (the - # narrow domain kills the long-tail bin collapse). - # 3. void, or n0 < K + 64 (64 = emitter sentinel bound, - # so live coverage of K is proven) -> fallback. - # The K+64 slack also lets every accepted cut load run - # WITHOUT any overflow net: counts and load predicates are - # the same comparison on the same data. + # ---- List path: known-counts admission ---- + # The emitter wrote an SoA list collected at t0 = seed_thr[0] + # and counted the two tighter lines on the way out, so the + # control words carry {n0, void, n1, n2} with n_i = #(>= t_i). + # Admission is then a scalar lookup: + # 1. some n_i in [K, B*] -> cut at the tightest, one pass + # 2. every line straddles/overshoots -> histogram over the + # list clamped between the two known bracket lines + # 3. void, or n0 < K + 64 (emitter sentinel bound) -> fall + # back. The slack also lets accepted cuts load without + # an overflow net: count and load are the same compare. # cs>1 and 16-bit dtypes keep the plain fallback. take_cand = cutlass.Int32(0) list_used = cutlass.Int32(0) # list path taken (xstate publish) @@ -6153,7 +6129,7 @@ def _run_phases( and cluster_size == 1 and self.dtype == cutlass.Float32 ): - # ---- List path v5: BUCKETED segments ---- + # ---- List path: bucketed segments ---- # The emitter classifies each entry by the tightest line # it passes and appends into one of three fixed segments # (A = [0, segA) holds >= t2, B = [segA, 2*segA) holds @@ -6767,23 +6743,18 @@ def _run_phases( # preIdx stats are full-row). if cutlass.const_expr(self.use_ext_counts): if ext_row == cutlass.Int32(1): - # ---- Waterfall L1 admission (ext rungs, v2a) ---- - # Rung thresholds arrive from the indexer epilogue: - # ONLY P1b is skipped. The stock M-ary count pass runs - # on the ext rungs so the block-skip list build, rung - # tightening, per-thread hand-off and classify all - # compose unchanged (v1 routed through the dense - # refine and forfeited the compact-walk win: flash 1M - # ext 34.7us vs skipR0 15.6us cold). - # v2b: when an ext count is already in [K, kC], park - # THE ADMITTED THRESHOLD IN ALL RUNG SLOTS — the M-ary - # pass degenerates to one compact single-threshold - # count (+ list build at that threshold) and classify - # admits it; a full miss keeps the 3 distinct rungs as - # measured brackets for the seeded refine. - # rung parking + single-path staging happen in - # the P1-init thread0 block (one barrier for - # the whole admission prologue). + # ---- Waterfall L1 admission (ext rungs) ---- + # Rung thresholds arrive from the indexer epilogue, + # so only P1b is skipped: the stock M-ary count pass + # runs on the ext rungs and the block-skip list + # build, rung tightening, hand-off and classify all + # compose unchanged. When an ext count already lies + # in [K, kC] the admitted threshold is parked in all + # rung slots, degenerating the pass to one compact + # single-threshold count; a full miss keeps the + # three distinct rungs as brackets for the refine. + # Parking and staging happen in the P1-init thread0 + # block, one barrier for the whole prologue. if cutlass.const_expr(not self.enable_block_skip): # v3: the parked M-ary pass counted the SAME # threshold in all three columns (3x compare From d697e3d6d054d8846b4530d4de759ec18ebb429c Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:11:52 -0700 Subject: [PATCH 068/117] [None][fix] GVR: drop the const-flag branch shells from the stock path Five sites set a routing flag to a compile-time Int32(0), let a const_expr-gated feature block possibly set it to 1, then branched on it with a runtime compare. With the feature off the predicate is a constant, but the compare is still traced, so the stock build carries an scf.if region upstream does not have and every value written inside it becomes a region yield. The largest wrapped all of Phase 2 and Phase 3 - 470 lines - in one region. Each site now computes a Python bool under the const_expr guard that already exists a few lines above, so the stock configuration traces the body straight-line exactly as upstream does: dense_ok block_count_ge_multi, two shells (block skip) park_cursors phase3_collect cursor parking (block skip) run_stock_range phase4 block min/max (ext candidate range) run_stock_p23 the Phase 2 + Phase 3 pipeline (list / self-scan) run_mary the M-ary count pass (ext counts) Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 3d5d29c3e0af..f1f45834b6ef 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -2202,6 +2202,7 @@ def block_count_ge_multi( # here separately (per-thread order contract: head elements FIRST, # then list entries — Phase 3's compact write replays the same). skip_ok = cutlass.Int32(0) + dense_ok = True # Python bool: no scf.if when block skip is off if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): skip_ok = cutlass.Int32(1) # Capacity/id-width guard: the active list holds at most @@ -2220,6 +2221,7 @@ def block_count_ge_multi( skip_ok = cutlass.Int32(0) if blk_hi_g > cutlass.Int32(32767): skip_ok = cutlass.Int32(0) + dense_ok = skip_ok == cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): if skip_ok == cutlass.Int32(1): head_end = ( @@ -2360,7 +2362,7 @@ def block_count_ge_multi( jj = jj + cutlass.Int32(1) li = li + cutlass.Int32(stride_un) - if self.enable_unroll_4 and skip_ok == cutlass.Int32(0): + if self.enable_unroll_4 and dense_ok: rng_frag = cute.make_fragment((vec_w,), self.dtype) big_iters = cutlass.Int32(0) if slice_end > i + cutlass.Int32(vec_w - 1): @@ -2387,7 +2389,7 @@ def block_count_ge_multi( i = i + big_iters * cutlass.Int32(step_elem) tail_frag = cute.make_fragment((vec_w,), self.dtype) - if skip_ok == cutlass.Int32(0): + if dense_ok: while i + cutlass.Int32(vec_w - 1) < slice_end: src_ptr = cute.make_ptr( self.dtype, @@ -2932,9 +2934,11 @@ def phase3_collect_candidates( # Only taken when the list is CURRENT (s_active_cnt[1] == 1, set by # the build; cleared on any dense fallback re-count). skip_wr = cutlass.Int32(0) + park_cursors = False # Python bool: no scf.if when block skip is off if cutlass.const_expr(self.enable_block_skip and smem_active is not None): if s_active_cnt[1] == cutlass.Int32(1): skip_wr = cutlass.Int32(1) + park_cursors = skip_wr == cutlass.Int32(1) if cutlass.const_expr(self.enable_block_skip and smem_active is not None): if skip_wr == cutlass.Int32(1): # head region first — same per-thread order as the count pass @@ -3001,7 +3005,7 @@ def phase3_collect_candidates( # When the compact write ran, park the dense cursors at the end so # all three dense loops below (4-way, vec tail, scalar tail) fall # through without re-indenting them. - if skip_wr == cutlass.Int32(1): + if park_cursors: ic = N_local n_aligned = N_local @@ -3521,9 +3525,13 @@ def phase4_rank_scatter( sc0 = cute.arch.clock64() bmin_r = cutlass.Float32(self.FLT_MAX) bmax_r = cutlass.Float32(self.NEG_FLT_MAX) - use_ext_r = cutlass.Int32(0) + # a Python bool here, so with the feature off the stock body + # below traces straight-line instead of inside an scf.if whose + # predicate is a compile-time constant + run_stock_range = True if cutlass.const_expr(ext_range_flag is not None): use_ext_r = ext_range_flag + run_stock_range = use_ext_r == cutlass.Int32(0) if use_ext_r == cutlass.Int32(1): # list rows: the take walk pre-zeroed the hist and staged # per-warp maxima in smem_wcnt (its end barrier orders @@ -3538,7 +3546,7 @@ def phase4_rank_scatter( bmax_r = cute.arch.fmax(bmax_r, vmax) if bmax_r <= bmin_r: bmax_r = bmin_r + cutlass.Float32(1e-6) - if use_ext_r == cutlass.Int32(0): + if run_stock_range: # ---- block min/max over candidates ---- local_cmin = cutlass.Float32(self.FLT_MAX) local_cmax = cutlass.Float32(self.NEG_FLT_MAX) @@ -6121,6 +6129,10 @@ def _run_phases( # an overflow net: count and load are the same compare. # cs>1 and 16-bit dtypes keep the plain fallback. take_cand = cutlass.Int32(0) + # Python bool: with every list/scan feature off, the stock + # Phase 2 + Phase 3 below trace straight-line rather than as + # one 470-line scf.if region with a large yield list. + run_stock_p23 = True list_used = cutlass.Int32(0) # list path taken (xstate publish) claimed_c = cutlass.Int32(0) if cutlass.const_expr( @@ -6715,7 +6727,9 @@ def _run_phases( if cutlass.const_expr(_P4_TAIL_DBG): ck1 = cute.arch.clock64() - if take_cand == cutlass.Int32(0): + if cutlass.const_expr(self.use_ext_cand or self.use_ext_counts or self.self_scan): + run_stock_p23 = take_cand == cutlass.Int32(0) + if run_stock_p23: # Stage this CTA's slice into SMEM once before Phase 2's # 6-10 secant iters re-scan it. Phase 1 (preIdx) uses # scatter-loads OUTSIDE this slice, so it stays on GMEM. @@ -6880,6 +6894,7 @@ def _run_phases( lane, ) r0_par = cutlass.Int32(0) + run_mary = True # Python bool: no scf.if without ext counts if cutlass.const_expr(self.use_ext_counts and not self.enable_block_skip): # single-column fast path accepted: the parked count # is done and admitted; the M-ary pass, argmin, @@ -6889,15 +6904,8 @@ def _run_phases( 1 ] == cutlass.Int32(1): r0_par = cutlass.Int32(1) - if r0_par == cutlass.Int32(0): - # NOTE: a loosest-line pass-rate veto (packed seed - # col 6) was measured here and REMOVED: the rung- - # tightening build below already salvages fat - # loosest-rung rows by rebuilding at a tighter - # line, so any veto threshold (3/8 and 7/8 both - # tried, cold, 30 layers) only preempts that and - # costs 3-18% across the board. Col 6 stays as a - # zero-cost diagnostic for host-side routing. + run_mary = r0_par == cutlass.Int32(0) + if run_mary: self.block_count_ge_multi( input_row, slice_start, @@ -6917,7 +6925,7 @@ def _run_phases( s_active_cnt=s_active_cnt, ) cute.arch.barrier() - if tidx == 0 and r0_par == cutlass.Int32(0): + if run_mary and tidx == 0: # tightest admissible rung = SMALLEST count in [K, kC]. # (Explicit argmin: with r0_vseed the pmean column is not # sorted into the rung order; for sorted rungs this is From e3db787e2695eba7a13584c831999aee12cabc83 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:39:12 -0700 Subject: [PATCH 069/117] [None][chore] GVR: put the P1r degenerate rescue behind a const_expr Default ON - upstream's identity shortcut is wrong on real data and gating it off would put that defect back. The knob makes the cost measurable, and it is worth stating: degenerate prev_topk (the rescue fires): +7.8 / +8.2 us real prev_topk (it does not): -0.26 / -0.28 us, 17 and 16 of 40 paired reps slower, i.e. noise So the rescue is free on steady-state steps and costs ~8 us on the first decode step of a request, which is where the zero-init feedback buffer makes the bracket degenerate. That buys a correct first step - without it 42 of 231 rows on a V4-Flash TP2 capture are wrong. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 40 +++++++++++-------- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index f1f45834b6ef..da0af580032b 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -296,6 +296,7 @@ def __init__( p4_exact_tail: Optional[bool] = None, p4_tail_fast: Optional[bool] = None, # [p4tt] p4_tail_v3: bool = False, + p1r_rescue: bool = True, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, enable_block_skip: bool = False, @@ -751,6 +752,12 @@ def __init__( # select. Off by default - the stock body is what upstream # emits, and this rewrite only pays at K>=1024. self.p4_tail_v3 = bool(p4_tail_v3) + # p1r_rescue: rebuild the refine bracket from the row when the + # seed bracket is degenerate. ON by default - upstream's identity + # shortcut is wrong on real data (every request's first decode + # step feeds a zero-init prev_topk). The knob exists to measure + # its cost, not to ship it off. + self.p1r_rescue = bool(p1r_rescue) # ------------------------------------------------------------------ # SMEM slice cache loader. Streams this CTA's slice GMEM → SMEM via @@ -6069,22 +6076,23 @@ def _run_phases( # If the bracket is STILL degenerate after the rescue, every # in-range value is identical (or N <= K), and identity output is # then exact — keep the shortcut for exactly those rows. - v_lo = s_thr[1] - v_hi = s_thr[2] - if v_hi <= cutlass.Float32(self.NEG_FLT_MAX) or v_lo >= v_hi: - if N > cutlass.Int32(self.top_k): - self.phase1r_data_reseed( - input_row, - N, - smem_wmin, - smem_wmax, - s_thr, - s_iscalars, - s_mt_thr, - tidx, - warp_id, - lane, - ) + if cutlass.const_expr(self.p1r_rescue): + v_lo = s_thr[1] + v_hi = s_thr[2] + if v_hi <= cutlass.Float32(self.NEG_FLT_MAX) or v_lo >= v_hi: + if N > cutlass.Int32(self.top_k): + self.phase1r_data_reseed( + input_row, + N, + smem_wmin, + smem_wmax, + s_thr, + s_iscalars, + s_mt_thr, + tidx, + warp_id, + lane, + ) v_lo = s_thr[1] v_hi = s_thr[2] if v_hi <= cutlass.Float32(self.NEG_FLT_MAX) or v_lo >= v_hi: diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 30ba825ec8ee..f0bbc8b8a2a6 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -68,6 +68,7 @@ def _compile( enable_block_skip: bool = False, pdl_wait_late: bool = True, p4_tail_v3: bool = False, + p1r_rescue: bool = True, p4_fine_rangetest: "bool | None" = None, p4_scat_rangetest: bool = False, use_ext_counts: bool = False, @@ -209,6 +210,7 @@ def _compile( enable_block_skip=enable_block_skip, pdl_wait_late=pdl_wait_late, p4_tail_v3=p4_tail_v3, + p1r_rescue=p1r_rescue, p4_fine_rangetest=p4_fine_rangetest, p4_scat_rangetest=p4_scat_rangetest, use_ext_counts=use_ext_counts, @@ -674,6 +676,7 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, pdl_wait_late: bool = True, p4_tail_v3: bool = False, + p1r_rescue: bool = True, p4_fine_rangetest: Optional[bool] = None, p4_scat_rangetest: bool = False, block_max: Optional[torch.Tensor] = None, @@ -964,6 +967,7 @@ def gvr_topk_decode( enable_block_skip, pdl_wait_late, p4_tail_v3, + p1r_rescue, p4_fine_rangetest, p4_scat_rangetest, use_ext_counts, From 1fe0feee0649b91e780ad7e7284911f0e56c5f9e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 29 Jul 2026 23:47:57 -0700 Subject: [PATCH 070/117] [None][fix] GVR: keep range-test and debug scalars off the stock path f_hi / lo_edge / hi_edge feed only the two range-test arms, both off by default, but were computed unconditionally - a divide and two compares upstream never emits. The P4 sub-phase and tail stopwatches were likewise declared outside their debug guards, which promotes them to region yields when a neighbouring branch is traced. Both now sit under the const_expr that already gates their only readers. Four-path exactness (stock / stock+v3 / counts / counts+bm) unchanged. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index da0af580032b..62713e5f1982 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3521,14 +3521,13 @@ def phase4_rank_scatter( output_indices_row[i4] = smem_vals[i4] i4 = i4 + cutlass.Int32(num_threads) elif cand_count > cutlass.Int32(kK): - sc0 = cutlass.Int64(0) - sc1 = cutlass.Int64(0) - sc2 = cutlass.Int64(0) - sc3 = cutlass.Int64(0) - sc4 = cutlass.Int64(0) - sc5 = cutlass.Int64(0) - sc6 = cutlass.Int64(0) if cutlass.const_expr(_P4_SUB_DBG): + sc1 = cutlass.Int64(0) + sc2 = cutlass.Int64(0) + sc3 = cutlass.Int64(0) + sc4 = cutlass.Int64(0) + sc5 = cutlass.Int64(0) + sc6 = cutlass.Int64(0) sc0 = cute.arch.clock64() bmin_r = cutlass.Float32(self.FLT_MAX) bmax_r = cutlass.Float32(self.NEG_FLT_MAX) @@ -3671,12 +3670,14 @@ def phase4_rank_scatter( # bin b* value range under the inv1 binning: [f_lo, f_lo + 1/inv1) f_lo = bmin_r + cutlass.Float32(b_star) / inv1 finv = (cutlass.Float32(fbins - 1) + cutlass.Float32(0.99)) * inv1 - # bin b* spans [f_lo, f_hi) under the coarse binning; the - # clamped ends fold out-of-range values INTO bin 0 and bin - # kBins-1, so those two bins drop the matching side. - f_hi = f_lo + cutlass.Float32(1.0) / inv1 - lo_edge = b_star == cutlass.Int32(0) - hi_edge = b_star == cutlass.Int32(kBins - 1) + # bin b* spans [f_lo, f_hi); the clamped ends fold + # out-of-range values into bin 0 and bin kBins-1, so those + # two drop the matching side. Only the range-test arms read + # these, so upstream's build must not compute them. + if cutlass.const_expr(self.p4_fine_rangetest or self.p4_scat_rangetest): + f_hi = f_lo + cutlass.Float32(1.0) / inv1 + lo_edge = b_star == cutlass.Int32(0) + hi_edge = b_star == cutlass.Int32(kBins - 1) # re-zero (only fbins slots) + build fine sub-hist of bin-b* cands iz = tidx while iz < cutlass.Int32(fbins): @@ -5856,10 +5857,10 @@ def _run_phases( # misses (pro-1M cold: stock-skip 21us vs ext-miss 51us). All # threads read the same control words, so the predicate is # CTA-uniform and the dynamic branches below stay convergent. - ck0 = cutlass.Int64(0) - ck1 = cutlass.Int64(0) - ckE = cutlass.Int64(0) if cutlass.const_expr(_P4_TAIL_DBG): + ck0 = cutlass.Int64(0) + ck1 = cutlass.Int64(0) + ckE = cutlass.Int64(0) ckE = cute.arch.clock64() # row-phase entry (device-residency ref) ext_row = cutlass.Int32(0) if cutlass.const_expr(self.use_ext_counts): From 84f8a36ca3e655f61cb853d38c1ee4c21741e20d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:49:33 -0700 Subject: [PATCH 071/117] [None][fix] GVR: let the assist path keep the compacted tail repair Defaulting p4_tail_v3 to False isolated the stock path but also took the rewrite away from our own tiers, since nothing on the production side passes the flag. Derive it from the configuration instead: a stock kernel gets upstream's thread0 serial select, any emission-assisted or block-skip kernel gets the compacted repair. No plumbing through the router, the ext state or the op face. Stock-path constants were diffed against origin/main for K=1024 and K=512: every constant upstream also has now resolves to the same value, and the 22 extras are all new facilities sitting at their off state (p1r_rescue is the one deliberate exception, see its own commit). Four-path exactness unchanged: stock / stock+v3 / counts / counts+bm. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 10 +++++++--- tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 62713e5f1982..cf12941cb2db 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -295,7 +295,7 @@ def __init__( enable_p4_rank_scatter_exact: Optional[bool] = None, p4_exact_tail: Optional[bool] = None, p4_tail_fast: Optional[bool] = None, # [p4tt] - p4_tail_v3: bool = False, + p4_tail_v3: Optional[bool] = None, p1r_rescue: bool = True, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, @@ -749,8 +749,12 @@ def __init__( self.p4_tail_fast = bool(p4_tail_fast) and self.p4_exact_tail # [p4tt] # p4_tail_v3: compacted-class repair (block-parallel radix + # pure-tie pre-check) in place of the stock thread0 serial - # select. Off by default - the stock body is what upstream - # emits, and this rewrite only pays at K>=1024. + # select. Default follows the configuration: a stock kernel gets + # upstream's body, any emission-assisted one gets the rewrite. + if p4_tail_v3 is None: + p4_tail_v3 = bool( + use_ext_counts or use_ext_cand or ext_rungs or self_scan or enable_block_skip + ) self.p4_tail_v3 = bool(p4_tail_v3) # p1r_rescue: rebuild the refine bracket from the row when the # seed bracket is degenerate. ON by default - upstream's identity diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index f0bbc8b8a2a6..44585e2c210d 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -67,7 +67,7 @@ def _compile( p2_warp_redundant: bool = True, enable_block_skip: bool = False, pdl_wait_late: bool = True, - p4_tail_v3: bool = False, + p4_tail_v3: "bool | None" = None, p1r_rescue: bool = True, p4_fine_rangetest: "bool | None" = None, p4_scat_rangetest: bool = False, @@ -675,7 +675,7 @@ def gvr_topk_decode( p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, pdl_wait_late: bool = True, - p4_tail_v3: bool = False, + p4_tail_v3: "bool | None" = None, p1r_rescue: bool = True, p4_fine_rangetest: Optional[bool] = None, p4_scat_rangetest: bool = False, From cf87bf0f8b1a29691d7d1c9c586857573a067076 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:36:30 -0700 Subject: [PATCH 072/117] [None][perf] GVR routing: pick the tier from the shape-dependent tax The emission tax is a fraction of the indexer's own time - roughly 9-14% for the bucketed list, ~2% for the packed counts - so in absolute terms it grows with B*N while what the top-k saves does not. The old rule charged a flat cost, which made the counts tier look free at large batch and kept the list tier out of the 32k-64k band where it already pays. Re-derived over the 126-cell worst-step grid with the tax modelled as a fraction of the indexer time, evaluated at both ends of the measured range (9% and 14%): rule 9% 11.5% 14% old (list >=65536 & B<=16, else counts) 1.193 1.149 1.110 new 1.248 1.225 1.203 per-cell optimum 1.286 1.253 1.228 Losing cells go from 14/20/29 to 9/11/13. The new rule keeps ~85-90% of what per-cell selection would buy, and holds its ranking across the whole tax range rather than at one point in it. list n_comp >= 32768 and B <= 16 (was 65536) rungs B >= 32 (new: emit nothing, no tax) counts otherwise Lifting the batch cap was tested and is not viable: the list tier at B>=32 drops the grid to geomean 1.022 and worst 0.411, because its tax scales past the saving. The cap stays. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_routing.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 40453abe330f..fe47d17124de 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -52,10 +52,17 @@ SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split on top of bm from here -# Emission tax ~ B*N on the GEMM side: the candidate list is only worth -# emitting for latency-bound shapes (small B, long rows). +# The emission tax is a fraction of the indexer's own time (list ~9-14%, +# counts ~2%), so in absolute terms it grows with B*N while the top-k +# saving does not. That gives three regimes, measured over the 126-cell +# worst-step grid and checked at both ends of the tax range: +# long rows, small batch -> the list repays its tax several times over +# large batch -> emit nothing; the rungs tier has no tax +# otherwise -> counts, whose tax is small either way LIST_EMIT_MAX_B = 16 -LIST_EMIT_MIN_N = 65536 +LIST_EMIT_MIN_N = 32768 +# Above this batch even the counts tax outruns what it buys. +RUNGS_MIN_B = 32 # rungs-tier block_max pays only at small K: with K=1024 the tight-line # pass rate runs too high and the prefix read is pure overhead. @@ -97,8 +104,10 @@ def plan_emission(batch: int, n_comp: int, k: int, have_epilogue: bool) -> str: if not have_epilogue: return "rungs" # closed-loop lines cost nothing to carry if batch <= LIST_EMIT_MAX_B and n_comp >= LIST_EMIT_MIN_N: - return "list" # latency-bound long rows: list pays big - return "counts" # near-free tax, wins almost everywhere + return "list" + if batch >= RUNGS_MIN_B: + return "rungs" # throughput regime: emitting anything is a loss + return "counts" def pick_config(tier: str, batch: int, n_comp: int, k: int, num_sms: int) -> TopkRoute: From 52973e6d39730a15d1f06108f6c8a964f06cc666 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:47:27 -0700 Subject: [PATCH 073/117] [None][fix] GVR routing: cap the list tier at batch 1, measured The previous two commits set the list-tier batch cap from a tax model, first flat and then proportional to the indexer time. Both were wrong. Measured on the FP4 indexer itself - same kernel, emission outputs attached, wall delta - at ctx 256k and 1M: batch 1 2 4 8 32 128 list +13.8% +64.3% +60.7% +62.0% +122.9% +150.7% counts +9.4% +3.5% +0.9% +1.1% +0.9% +2.6% The list tier clears its own tax only at batch 1. From batch 2 the tax is 20-60us against a top-k saving of a few, and it keeps growing. The proportional model underestimated batch 128 by an order of magnitude (45us modelled, 589us measured). Re-scoring the 126-cell grid with the measured tax: rule geomean worst losing list B<=16, N>=65536 0.969 0.159 43 list B<=16, N>=32768 0.926 0.159 41 list B==1, N>=32768 1.155 0.758 17 per-cell optimum 1.207 0.763 14 So the cap goes to 1. Note the first row: the tier was already a net loss as shipped before this series - the 3.4us flat tax it was sized against only holds at batch 1. The counts tax stays inside ~2.6us at every batch measured, so the counts/rungs split from the previous commit is unaffected. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_routing.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index fe47d17124de..0bbe6f52c0ff 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -52,16 +52,20 @@ SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split on top of bm from here -# The emission tax is a fraction of the indexer's own time (list ~9-14%, -# counts ~2%), so in absolute terms it grows with B*N while the top-k -# saving does not. That gives three regimes, measured over the 126-cell -# worst-step grid and checked at both ends of the tax range: -# long rows, small batch -> the list repays its tax several times over -# large batch -> emit nothing; the rungs tier has no tax -# otherwise -> counts, whose tax is small either way -LIST_EMIT_MAX_B = 16 +# Emission tax measured on the FP4 indexer itself (ctx 256k and 1M, +# batch 1..128), as the wall delta of the same kernel with the emission +# outputs attached: +# +# batch 1 2 4 8 32 128 +# list +13.8% +64.3% +60.7% +62.0% +122.9% +150.7% +# counts +9.4% +3.5% +0.9% +1.1% +0.9% +2.6% +# +# The list tier only clears its own tax at batch 1; from batch 2 the tax +# is tens of microseconds against a top-k saving of a few. The counts +# tax stays inside ~2.6us throughout, and at large batch even that is +# not repaid, so the zero-emission rungs tier takes over. +LIST_EMIT_MAX_B = 1 LIST_EMIT_MIN_N = 32768 -# Above this batch even the counts tax outruns what it buys. RUNGS_MIN_B = 32 # rungs-tier block_max pays only at small K: with K=1024 the tight-line From a048dfa32b514235c9b689669f662a5a3a47a4eb Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:04:30 -0700 Subject: [PATCH 074/117] [None][perf] GVR routing: gate the list tier on emitted volume Measuring the indexer across batch 1..1024 and ctx 32k..1M shows the list tier's tax tracks B*N - the volume it emits - rather than batch: B*N (raw tokens) <=0.5M 1M >=2M list +11-14% +55-71% +150-270% counts +1-3% +1-3% +0-3% Volume is the main term but not the only one: the same 1M tokens costs +21% as 1M x B1 and +71% as 128k x B8, so there is a per-row component too. Both cheap regions are covered by "a single row, or under ~0.75M tokens", which lets 128k rows carry the list to batch 4 and 256k to batch 2 - where the previous commit's flat B==1 cap gave them up. Scored on the 126-cell grid with the measured tax: rule geomean worst losing pre-series (B<=16, N>=64k) 0.894 0.078 46 previous (B==1) 1.145 0.769 19 this (B==1 or <=0.75M) 1.162 0.786 17 per-cell optimum 1.182 0.786 14 The counts tax stays inside 3% at every batch measured, so it remains the default and the rungs handover at B>=32 is unchanged. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 6 ++-- .../attention_backend/sparse/gvr_ext.py | 8 +++-- .../blackwell/top_k/gvr_routing.py | 30 ++++++++++++------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 00953c7dba6e..1f03b25d454a 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2837,9 +2837,11 @@ def sparse_attn_indexer( n_comp = indexer_max_seq_len // max( self.compress_ratio, 1) emit_tier, self._gvr_route = st.plan( - batch_size, n_comp, + batch_size, + n_comp, torch.cuda.get_device_properties( - q_fp8.device).multi_processor_count) + q_fp8.device).multi_processor_count, + compress_ratio=max(self.compress_ratio, 1)) st.update_seed_rows(batch_size) gvr_emit_kwargs = st.indexer_emit_kwargs( emit_tier, batch_size) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index 0611bbd10df7..296ec4a5ed86 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -101,10 +101,14 @@ def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: ) return self.block_max - def plan(self, batch: int, n_comp: int, num_sms: int) -> tuple[str, TopkRoute]: + def plan( + self, batch: int, n_comp: int, num_sms: int, compress_ratio: int = 4 + ) -> tuple[str, TopkRoute]: """Route this step: (tier to EMIT next, launch knobs to CONSUME what was emitted last step).""" - emit_tier = plan_emission(batch, n_comp, self.top_k, have_epilogue=True) + emit_tier = plan_emission( + batch, n_comp, self.top_k, have_epilogue=True, compress_ratio=compress_ratio + ) if emit_tier == "list" and self.cand_vals is None: # constructed with enable_list_tier=False: no candidate # buffers to emit into, demote to the counts tier diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 0bbe6f52c0ff..f09d6aa895bb 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -56,15 +56,21 @@ # batch 1..128), as the wall delta of the same kernel with the emission # outputs attached: # -# batch 1 2 4 8 32 128 -# list +13.8% +64.3% +60.7% +62.0% +122.9% +150.7% -# counts +9.4% +3.5% +0.9% +1.1% +0.9% +2.6% +# What the list tier costs tracks B*N - the total emitted volume - not +# batch on its own: # -# The list tier only clears its own tax at batch 1; from batch 2 the tax -# is tens of microseconds against a top-k saving of a few. The counts -# tax stays inside ~2.6us throughout, and at large batch even that is -# not repaid, so the zero-emission rungs tier takes over. -LIST_EMIT_MAX_B = 1 +# B*N (raw tokens) <=0.5M 1M >=2M +# list +11-14% +55-71% +150-270% +# counts +1-3% +1-3% +0-3% +# +# Volume is the main term but not the only one: at the same 1M tokens +# the tax is +21% as 1M x B1 and +71% as 128k x B8, so there is a +# per-row cost on top. Both measured-cheap regions are covered by +# "half a megatoken, or a single row". The counts tax stays inside 3% +# everywhere, which is why it remains the default; past RUNGS_MIN_B +# even that is not repaid and the zero-emission rungs tier takes over. +LIST_EMIT_MAX_TOKENS = 786432 # B * raw length; between the measured +# cheap band (<=0.52M) and the first expensive point (1.05M) LIST_EMIT_MIN_N = 32768 RUNGS_MIN_B = 32 @@ -93,7 +99,9 @@ class TopkRoute: attach_block_max: bool = False -def plan_emission(batch: int, n_comp: int, k: int, have_epilogue: bool) -> str: +def plan_emission( + batch: int, n_comp: int, k: int, have_epilogue: bool, compress_ratio: int = 4 +) -> str: """Which assist tier the indexer epilogue should emit this step. ``n_comp``: compressed row length (post compress_ratio) - the @@ -107,7 +115,9 @@ def plan_emission(batch: int, n_comp: int, k: int, have_epilogue: bool) -> str: return "none" if not have_epilogue: return "rungs" # closed-loop lines cost nothing to carry - if batch <= LIST_EMIT_MAX_B and n_comp >= LIST_EMIT_MIN_N: + if n_comp >= LIST_EMIT_MIN_N and ( + batch == 1 or batch * n_comp * compress_ratio <= LIST_EMIT_MAX_TOKENS + ): return "list" if batch >= RUNGS_MIN_B: return "rungs" # throughput regime: emitting anything is a loss From dc10a1ecf6d8d4f2df88e104571de4ddef938c9c Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:35:21 -0700 Subject: [PATCH 075/117] [None][perf] GVR routing: widen the list tier, hand over to rungs sooner Scored on the full mean-over-layers-and-steps grid (length 8k-1024k, batch 1-1024, all powers of two, measured emission tax): rule geomean worst losing list N>=32768, rungs B>=32 1.133 0.802 20/154 list N>=16384 1.140 0.802 16/154 rungs B>=16 1.133 0.802 19/154 both 1.140 0.802 15/154 per-cell optimum 1.147 0.802 13/154 64k rows (n_comp 16387) were below the length gate but the list wins there by ~2us at batch 2-8 once the volume gate lets it through, and at batch 16 several cells prefer the zero-emission tier to counts. The remaining band - 64k-512k at batch 2-16 - is not a routing problem: the list tier is far worse there (0.65 down to 0.09; the emitted volume crosses a megatoken and the tax takes the kernel from 17us to 100-200), and counts is already the best of the three. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index f09d6aa895bb..83d3e1ff0e19 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -71,8 +71,8 @@ # even that is not repaid and the zero-emission rungs tier takes over. LIST_EMIT_MAX_TOKENS = 786432 # B * raw length; between the measured # cheap band (<=0.52M) and the first expensive point (1.05M) -LIST_EMIT_MIN_N = 32768 -RUNGS_MIN_B = 32 +LIST_EMIT_MIN_N = 16384 +RUNGS_MIN_B = 16 # rungs-tier block_max pays only at small K: with K=1024 the tight-line # pass rate runs too high and the prefix read is pure overhead. From 75f89e6214433435a252c2f4130bbf8bb5945e0e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 05:06:55 -0700 Subject: [PATCH 076/117] [None][perf] GVR: turn the block-skip prefix on one doubling later The counts tier attached block_max from n_comp 65536, but the prefix does not earn its setup there. Attaching it to the same shape and diffing, 30 interleaved cold reps: n_comp batch 1 batch 4 batch 16 65538 +0.0 -0.1 +0.6 (9-16 of 30 reps faster) 131075 -4.6 -5.0 -4.5 (30 of 30) 262127 -13.8 -14.2 -14.0 (30 of 30) At 65538 it is a wash and a small loss at batch 16; the win starts at 131072. That threshold was where the mean-metric grid lost worst: flash 256k (n_comp 65538) measured 0.802 / 0.834 / 0.859 at batch 4 / 8 / 16 against the baseline. With the prefix off those cells become 1.086 / 1.150 / 1.199 - the loss was the prefix's setup, not the tier. The time model says the same thing: the counts tier's slope is already better than the baseline's (28-35 vs 32-52 us per million compressed elements) and its fixed cost is the same, so a 3-4us step at one length was the whole gap. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/blackwell/top_k/gvr_routing.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 83d3e1ff0e19..409a269f0254 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -47,8 +47,13 @@ # whole n_comp/K < 1.5 band measures below parity. ASSIST_MIN_N_COMP = 4096 # ~8k raw context at compress_ratio 4 -# Block-skip prefix pays only when whole-row reads dominate. -SKIP_MIN_N_COUNTS = 65536 # va: attach block_max unconditionally here up +# Block-skip prefix pays only when whole-row reads dominate. Measured +# by attaching block_max to the same shape and diffing (30 interleaved +# cold reps): at n_comp 65538 it is a wash (+0.0 / -0.1 / +0.6us at +# batch 1/4/16, 9-16 of 30 reps faster), at 131075 it is -4.5 to -5.0us +# and wins 30 of 30, at 262127 it is -14us. So the prefix earns its +# setup one doubling later than it was switched on. +SKIP_MIN_N_COUNTS = 131072 # va: attach block_max from here up SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split on top of bm from here From cda9b0390469852d05b56a3f520f8dddfbad5f13 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 05:20:05 -0700 Subject: [PATCH 077/117] [None][chore] GVR: make the coarse histogram width a knob Default unchanged. Added to test whether the P4 head's bin search sets the short-row floor - it does not: at pro n=4099 and n=8195, batch 1, widths 128/256/512/1024 land within 0.5us of each other and wider is marginally faster, so the search is not the bottleneck. Kept because it is the only way to ask that question again without patching the file. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py | 5 ++++- tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index cf12941cb2db..b409780e2bbc 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -297,6 +297,7 @@ def __init__( p4_tail_fast: Optional[bool] = None, # [p4tt] p4_tail_v3: Optional[bool] = None, p1r_rescue: bool = True, + num_bins: Optional[int] = None, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, enable_block_skip: bool = False, @@ -415,7 +416,9 @@ def __init__( params = GvrParams.get(self._dtype_name, top_k, self.compress_ratio) self.kC = params.kC - self.kNumBins = params.kNumBins + # num_bins: the coarse histogram width. The bin search walks it + # per warp, so it sets the barrier count of the P4 head. + self.kNumBins = params.kNumBins if num_bins is None else int(num_bins) self.kFTarget = params.kFTarget # Kernel-wide constants. diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 44585e2c210d..4880a592da9d 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -69,6 +69,7 @@ def _compile( pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, p1r_rescue: bool = True, + num_bins: "int | None" = None, p4_fine_rangetest: "bool | None" = None, p4_scat_rangetest: bool = False, use_ext_counts: bool = False, @@ -211,6 +212,7 @@ def _compile( pdl_wait_late=pdl_wait_late, p4_tail_v3=p4_tail_v3, p1r_rescue=p1r_rescue, + num_bins=num_bins, p4_fine_rangetest=p4_fine_rangetest, p4_scat_rangetest=p4_scat_rangetest, use_ext_counts=use_ext_counts, @@ -677,6 +679,7 @@ def gvr_topk_decode( pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, p1r_rescue: bool = True, + num_bins: "int | None" = None, p4_fine_rangetest: Optional[bool] = None, p4_scat_rangetest: bool = False, block_max: Optional[torch.Tensor] = None, @@ -968,6 +971,7 @@ def gvr_topk_decode( pdl_wait_late, p4_tail_v3, p1r_rescue, + num_bins, p4_fine_rangetest, p4_scat_rangetest, use_ext_counts, From 738ce6837e38abfa1c4db5ebfb9e80973a7ed1d5 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:55:48 -0700 Subject: [PATCH 078/117] [None][perf] GVR routing: fix the block-skip and cluster-split gates The previous commit raised the counts tier's block_max threshold to 131072 on a synthetic-Gaussian A/B. On captured V4 rows the prefix already pays at 65536 (flash n_comp 65537, nsys kernel-only over 5 layers x all decode steps: 16.1us with vs 17.3us without at batch 4/8/16), so put it back and note that this threshold must be set from captures. Raising it also silently enabled cluster split for the rungs tier at 65537, because the cs=1 clamp it used to hit was conditioned on block_max being attached. Cluster split is a loss for the assist tiers at that length whether or not the prefix is on (rungs at batch 4: 17.9 / 19.0 / 20.4us at cs 1 / 4 / 8; cs8 spills to 31.5us at batch 16), so gate the split on its own threshold instead. Worst mean-metric cell on the 154-cell grid: 0.794 -> 0.833. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_routing.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 409a269f0254..200db91861d3 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -47,15 +47,19 @@ # whole n_comp/K < 1.5 band measures below parity. ASSIST_MIN_N_COMP = 4096 # ~8k raw context at compress_ratio 4 -# Block-skip prefix pays only when whole-row reads dominate. Measured -# by attaching block_max to the same shape and diffing (30 interleaved -# cold reps): at n_comp 65538 it is a wash (+0.0 / -0.1 / +0.6us at -# batch 1/4/16, 9-16 of 30 reps faster), at 131075 it is -4.5 to -5.0us -# and wins 30 of 30, at 262127 it is -14us. So the prefix earns its -# setup one doubling later than it was switched on. -SKIP_MIN_N_COUNTS = 131072 # va: attach block_max from here up +# Block-skip prefix pays for the counts tier from 65536 up, but not for +# the zero-emission rungs tier below 131072. Measured on captured V4 +# rows (5 layers x all decode steps, nsys kernel-only, flash n_comp +# 65537): counts 16.1us with the prefix vs 17.3 without at batch +# 4/8/16, rungs 18.5 with vs 17.9 without. A synthetic-Gaussian A/B put +# the counts break-even a doubling later - the block-max distribution +# of real rows decides the pass rate, so set this from captures only. +SKIP_MIN_N_COUNTS = 65536 # va: attach block_max from here up SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here -SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split on top of bm from here +# Cluster split is a loss for the assist tiers below this point, +# block_max or not: at n_comp 65537 rungs measures 17.9 / 19.0 / 20.4us +# at cs 1 / 4 / 8 (batch 4) and cs8 spills to 31.5us at batch 16. +SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split from here up # Emission tax measured on the FP4 indexer itself (ctx 256k and 1M, # batch 1..128), as the wall delta of the same kernel with the emission @@ -147,13 +151,11 @@ def pick_config(tier: str, batch: int, n_comp: int, k: int, num_sms: int) -> Top # rungs (vb) if k <= RUNGS_BM_MAX_K and n_comp >= SKIP_MIN_N_RUNGS_FLASH: r.attach_block_max = True - if n_comp >= 65536: + if n_comp >= SKIP_CS_MIN_N_RUNGS: if batch * 8 <= num_sms // CS8_HALF_DEVICE: r.cluster_size = 8 elif batch * 4 <= (num_sms * CS_HEADROOM_NUM) // CS_HEADROOM_DEN: r.cluster_size = 4 elif batch * 2 <= (num_sms * CS_HEADROOM_NUM) // CS_HEADROOM_DEN: r.cluster_size = 2 - if r.attach_block_max and n_comp < SKIP_CS_MIN_N_RUNGS: - r.cluster_size = 1 # bm without cs below the split point return r From 78227d5397428f5c53eb0a2e48cfa3c1ded8645f Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:51:20 -0700 Subject: [PATCH 079/117] [None][fix] GVR: keep the P1 refine rescue off the stock path p1r_rescue defaulted to True, so the single-operator path compiled it too: 14016 vs 13570 PTX instructions against upstream for the same launch (flash 256k B4, both trees pick cs=4 / 512 threads), and 3.5-6% slower on the flash 128k/256k cells of the captured grid. Derive it from the assist tiers instead, the way p4_tail_v3 already is. The stock build is then instruction-identical to upstream (13570 / 14498 lines on both trees) and the tiers - which are the ones seeding from a possibly zero-init prev_topk - keep the rescue. Exactness unchanged on counts / rungs / list at flash 256k. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 13 ++++++++----- .../scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index b409780e2bbc..c511468181f9 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -296,7 +296,7 @@ def __init__( p4_exact_tail: Optional[bool] = None, p4_tail_fast: Optional[bool] = None, # [p4tt] p4_tail_v3: Optional[bool] = None, - p1r_rescue: bool = True, + p1r_rescue: Optional[bool] = None, num_bins: Optional[int] = None, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, @@ -760,10 +760,13 @@ def __init__( ) self.p4_tail_v3 = bool(p4_tail_v3) # p1r_rescue: rebuild the refine bracket from the row when the - # seed bracket is degenerate. ON by default - upstream's identity - # shortcut is wrong on real data (every request's first decode - # step feeds a zero-init prev_topk). The knob exists to measure - # its cost, not to ship it off. + # seed bracket is degenerate - the assisted tiers seed from the + # previous step's topk, and a request's first decode step feeds a + # zero-init prev_topk, so they need it. Defaults to the tiers + # only: on the stock path it would add ~450 PTX instructions + # (3.3%) for a case that path cannot reach the same way. + if p1r_rescue is None: + p1r_rescue = bool(use_ext_counts or use_ext_cand or ext_rungs or self_scan) self.p1r_rescue = bool(p1r_rescue) # ------------------------------------------------------------------ diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 4880a592da9d..2f93d9738dd4 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -68,7 +68,7 @@ def _compile( enable_block_skip: bool = False, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, - p1r_rescue: bool = True, + p1r_rescue: "bool | None" = None, num_bins: "int | None" = None, p4_fine_rangetest: "bool | None" = None, p4_scat_rangetest: bool = False, @@ -678,7 +678,7 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, - p1r_rescue: bool = True, + p1r_rescue: "bool | None" = None, num_bins: "int | None" = None, p4_fine_rangetest: Optional[bool] = None, p4_scat_rangetest: bool = False, From 1e70d74f526ce5cbe1b4dda71fb01d832f895d9e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Fri, 31 Jul 2026 07:10:57 -0700 Subject: [PATCH 080/117] [None][perf] GVR routing: hand the mid-row band back to the stock kernel There is a band of row lengths where the stock kernel splits the row across a cluster and its split grid still fits one wave. The assist tiers cannot follow there - splitting a row costs them more than the scan it saves, with or without the block-skip prefix - so the stock kernel wins the cell outright and the epilogue should emit nothing. Measured against the stock kernel over the 154-cell captured grid (2 models x 7 context lengths x 11 batches, nsys kernel-only, emission tax included): without the band 15 cells run below stock, worst 0.863 at flash 512k batch 16; with it, 2 cells at 0.99. Against the reference baseline the grid geomean goes 1.139 -> 1.145 and the worst cell 0.833 -> 0.912. Batch 1-2 keep the list tier inside the band, where it still beats stock by 1.5x, so the list check runs first. The larger K is, the less row the tiers need before they pay, hence the two upper bounds. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 5 ++-- .../blackwell/top_k/gvr_routing.py | 29 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 1f03b25d454a..160942091d06 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2921,8 +2921,9 @@ def sparse_attn_indexer( # so we cap it at 256 for now and fall back to the CUDA C++ # indexer_topk_decode. This limit can be removed if GPU memory # is not a bottleneck. - # tier "none" (gvr_routing.ASSIST_MIN_N_COMP): too short for - # any assist to pay - fall through to the stock branch. The + # tier "none": too short for any assist to pay, or inside + # the mid-row band where the stock kernel's split grid wins + # (see gvr_routing) - fall through to the stock branch. The # untouched ext state reads as cold start, which its closed # loop already handles. if (self.use_gvr_ext and self._gvr_ext is not None diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 200db91861d3..514e194f06ea 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -83,6 +83,22 @@ LIST_EMIT_MIN_N = 16384 RUNGS_MIN_B = 16 +# Mid-row weak band: rows long enough that the stock kernel splits them +# across a cluster, but short enough (and at a small enough batch) that +# its split grid still fits one wave. The assist tiers cannot follow - +# splitting a row costs them more than the scan it saves - so the stock +# kernel wins outright and the epilogue should emit nothing. Measured +# against the stock kernel on the captured grid: without this band 15 of +# 154 cells run below stock (worst 0.86 at flash 512k batch 16); with it, +# 2 cells at 0.99. Longer K needs less row before the tiers pay, hence +# the two upper bounds. Batch 1-2 still take the list tier (checked +# first): there the emitted list beats stock by 1.5x even inside the band. +ASSIST_WEAK_MIN_N = 49152 +ASSIST_WEAK_MAX_N_SMALL_K = 196608 # k <= ASSIST_WEAK_K +ASSIST_WEAK_MAX_N_LARGE_K = 98304 +ASSIST_WEAK_K = 512 +ASSIST_WEAK_MAX_B = 32 + # rungs-tier block_max pays only at small K: with K=1024 the tight-line # pass rate runs too high and the prefix read is pure overhead. RUNGS_BM_MAX_K = 512 @@ -122,12 +138,17 @@ def plan_emission( # and this holds for the zero-emission rungs tier too - it is # the same kernel, so the floor is the same return "none" - if not have_epilogue: - return "rungs" # closed-loop lines cost nothing to carry - if n_comp >= LIST_EMIT_MIN_N and ( - batch == 1 or batch * n_comp * compress_ratio <= LIST_EMIT_MAX_TOKENS + if ( + have_epilogue + and n_comp >= LIST_EMIT_MIN_N + and (batch == 1 or batch * n_comp * compress_ratio <= LIST_EMIT_MAX_TOKENS) ): return "list" + weak_max = ASSIST_WEAK_MAX_N_SMALL_K if k <= ASSIST_WEAK_K else ASSIST_WEAK_MAX_N_LARGE_K + if batch <= ASSIST_WEAK_MAX_B and ASSIST_WEAK_MIN_N <= n_comp < weak_max: + return "none" # stock's split grid wins this band outright + if not have_epilogue: + return "rungs" # closed-loop lines cost nothing to carry if batch >= RUNGS_MIN_B: return "rungs" # throughput regime: emitting anything is a loss return "counts" From 0982dc19db3b5800511a986fa727990271a42e22 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:49:56 -0700 Subject: [PATCH 081/117] Revert "[None][fix] GVR: keep the P1 refine rescue off the stock path" This reverts commit 78227d5397. The reverted commit read the rescue's presence on the stock path as an isolation leak. It is not: `143453c294` added it deliberately as a correctness fix for that exact path - the first decode step of a request feeds a zero-init prev_topk, and the old degenerate shortcut answers it with identity indices [0, K) instead of the top-K. The tests that pin this (test_cute_dsl_gvr_topk_decode_degenerate_preidx and its cs4 variant) call the stock custom op directly and do not exist on main, so deriving the knob from the assist tiers turned the fix off exactly where it is needed and broke them. The +446 PTX instructions and the 3.5-6% it costs the stock path on the flash 128k/256k cells are the price of that fix, not leakage from the emission tiers. The tiers stay isolated the way they already were - every emission knob is still compile-time gated off the stock build. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 13 +++++-------- .../scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index c511468181f9..b409780e2bbc 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -296,7 +296,7 @@ def __init__( p4_exact_tail: Optional[bool] = None, p4_tail_fast: Optional[bool] = None, # [p4tt] p4_tail_v3: Optional[bool] = None, - p1r_rescue: Optional[bool] = None, + p1r_rescue: bool = True, num_bins: Optional[int] = None, p4_warp_redundant: bool = True, p2_warp_redundant: bool = True, @@ -760,13 +760,10 @@ def __init__( ) self.p4_tail_v3 = bool(p4_tail_v3) # p1r_rescue: rebuild the refine bracket from the row when the - # seed bracket is degenerate - the assisted tiers seed from the - # previous step's topk, and a request's first decode step feeds a - # zero-init prev_topk, so they need it. Defaults to the tiers - # only: on the stock path it would add ~450 PTX instructions - # (3.3%) for a case that path cannot reach the same way. - if p1r_rescue is None: - p1r_rescue = bool(use_ext_counts or use_ext_cand or ext_rungs or self_scan) + # seed bracket is degenerate. ON by default - upstream's identity + # shortcut is wrong on real data (every request's first decode + # step feeds a zero-init prev_topk). The knob exists to measure + # its cost, not to ship it off. self.p1r_rescue = bool(p1r_rescue) # ------------------------------------------------------------------ diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 2f93d9738dd4..4880a592da9d 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -68,7 +68,7 @@ def _compile( enable_block_skip: bool = False, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, - p1r_rescue: "bool | None" = None, + p1r_rescue: bool = True, num_bins: "int | None" = None, p4_fine_rangetest: "bool | None" = None, p4_scat_rangetest: bool = False, @@ -678,7 +678,7 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, - p1r_rescue: "bool | None" = None, + p1r_rescue: bool = True, num_bins: "int | None" = None, p4_fine_rangetest: Optional[bool] = None, p4_scat_rangetest: bool = False, From 15f1c458a08ffba8021a41d714317dcbb3768286 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:14:15 -0700 Subject: [PATCH 082/117] [None][perf] GVR routing: refit the tiers on kernel-only emission cost The emission cost was fitted against a WALL-clock indexer time, which carries a ~22us launch floor, while every top-k number it was compared against is kernel-only. Re-measuring the indexer kernel itself (nsys kernel-only, ABBA-interleaved NVTX blocks, batch 1..64 x ctx 32k..512k) changes the shape of the counts tier's cost completely: batch 1 2 4 8 16 64 counts (us) +5.6 +4.4 +2.9 +2.0 +0.4 0.0 It is a fixed reduction-latency chain that depends on batch alone and hides once there are enough rows to overlap it - not a fraction of the indexer. The old model charged it 3% of the wall indexer time, i.e. 12us at batch 64 / 512k where the true cost is zero, and that is what put the counts tier behind RUNGS_MIN_B. On the bare kernels the counts tier beats rungs on 84 of 98 grid cells, by up to 4x. Refit on consistent units: * counts is the default whenever the epilogue can emit and the step is at least half a megatoken (below that its latency chain is exposed); * rungs, which emits nothing, is the fallback rather than a large-batch special case, and keeps the short-row band it wins; * list now needs a long row AND a single request - measured against the indexer kernel it almost never repays its emission. An unbucketed single-segment list is 1.5-5.8x cheaper to emit, but rescoring the grid with it moves the geomean +0.5%, so the contract is left alone. Scored over the 154-cell captured grid by the shipped plan_emission: vs this PR's stock vs the reference before 1.251, worst 0.558 1.169 after 1.349, worst 1.000 1.260, best 5.40x Zero cells below stock, and 0.6% off the per-cell optimum (1.357). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_routing.py | 93 ++++++++++--------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index 514e194f06ea..abfdeeb5e2f8 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -40,12 +40,17 @@ from dataclasses import dataclass from typing import Optional -# ---- measured thresholds (B200, f15 grid) -------------------------------- +# ---- measured thresholds (B200) ------------------------------------------ +# +# UNITS: every threshold here is fitted on KERNEL-ONLY time for both the +# indexer and the top-k. An earlier fit compared a wall-clock indexer +# (which carries a ~22us launch floor) against kernel-only top-k times; +# that inflated the apparent emission budget at small shapes and is what +# put the counts tier behind a batch gate it never needed. -# The kernel's fixed cost does not shrink with N (~12us at K=1024), so -# once the stock kernel finishes under that floor no tier can win. The -# whole n_comp/K < 1.5 band measures below parity. -ASSIST_MIN_N_COMP = 4096 # ~8k raw context at compress_ratio 4 +# The kernel's fixed cost does not shrink with N, so once the stock +# kernel finishes under that floor no tier can win. +ASSIST_MIN_N_COMP = 2048 # Block-skip prefix pays for the counts tier from 65536 up, but not for # the zero-emission rungs tier below 131072. Measured on captured V4 @@ -61,43 +66,43 @@ # at cs 1 / 4 / 8 (batch 4) and cs8 spills to 31.5us at batch 16. SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split from here up -# Emission tax measured on the FP4 indexer itself (ctx 256k and 1M, -# batch 1..128), as the wall delta of the same kernel with the emission -# outputs attached: -# -# What the list tier costs tracks B*N - the total emitted volume - not -# batch on its own: +# Emission cost measured on the FP4 indexer KERNEL (nsys kernel-only, +# ABBA-interleaved NVTX blocks, batch 1..64 x ctx 32k..512k) as the delta +# of the same kernel with the emission outputs attached: # -# B*N (raw tokens) <=0.5M 1M >=2M -# list +11-14% +55-71% +150-270% -# counts +1-3% +1-3% +0-3% +# batch 1 2 4 8 16 64 +# counts (us) +5.6 +4.4 +2.9 +2.0 +0.4 0.0 +# list, bucketed +6.2 +6.2 +6.8 +10.5 +25.0 +63.1 (ctx 32k) +# +10.3 +14.7 +25.6 +44.5 +85.7 +417.8 (ctx 512k) # -# Volume is the main term but not the only one: at the same 1M tokens -# the tax is +21% as 1M x B1 and +71% as 128k x B8, so there is a -# per-row cost on top. Both measured-cheap regions are covered by -# "half a megatoken, or a single row". The counts tax stays inside 3% -# everywhere, which is why it remains the default; past RUNGS_MIN_B -# even that is not repaid and the zero-emission rungs tier takes over. -LIST_EMIT_MAX_TOKENS = 786432 # B * raw length; between the measured -# cheap band (<=0.52M) and the first expensive point (1.05M) -LIST_EMIT_MIN_N = 16384 -RUNGS_MIN_B = 16 +# The counts cost is a fixed reduction-latency chain that depends on +# BATCH ONLY and hides once there are enough rows to overlap it - it is +# not a fraction of the indexer, so it cannot price the tier out at +# scale. An earlier fit modelled it as 3% of a WALL-clock indexer time +# (which carries a ~22us launch floor) and so charged 12us at batch 64 / +# 512k, where the true cost is zero; that is what put the counts tier +# behind a batch gate. The list cost is real per-emitted-entry work and +# grows with batch and context alike; an unbucketed single-segment list +# measures 1.5-5.8x cheaper, but rescoring the grid with it moves the +# geomean by +0.5%, so the bucketed contract stays as is. +LIST_EMIT_MIN_N = 65536 # shorter rows: the emission outweighs the saving +LIST_EMIT_MAX_B = 1 # past one row the list never repays its emission +COUNTS_MIN_TOKENS = 524288 # B * raw length; below this the counts +# latency chain is exposed and the zero-emission rungs tier wins +RUNGS_ONLY_MIN_N = 16384 # short-row band where rungs also beats counts +RUNGS_ONLY_MAX_N = 49152 # Mid-row weak band: rows long enough that the stock kernel splits them # across a cluster, but short enough (and at a small enough batch) that # its split grid still fits one wave. The assist tiers cannot follow - -# splitting a row costs them more than the scan it saves - so the stock -# kernel wins outright and the epilogue should emit nothing. Measured -# against the stock kernel on the captured grid: without this band 15 of -# 154 cells run below stock (worst 0.86 at flash 512k batch 16); with it, -# 2 cells at 0.99. Longer K needs less row before the tiers pay, hence -# the two upper bounds. Batch 1-2 still take the list tier (checked -# first): there the emitted list beats stock by 1.5x even inside the band. +# splitting a row costs them more than the scan it saves, with or without +# the block-skip prefix - so the stock kernel wins outright and the +# epilogue should emit nothing. ASSIST_WEAK_MIN_N = 49152 -ASSIST_WEAK_MAX_N_SMALL_K = 196608 # k <= ASSIST_WEAK_K -ASSIST_WEAK_MAX_N_LARGE_K = 98304 +ASSIST_WEAK_MAX_N_SMALL_K = 262144 # k <= ASSIST_WEAK_K +ASSIST_WEAK_MAX_N_LARGE_K = 196608 ASSIST_WEAK_K = 512 -ASSIST_WEAK_MAX_B = 32 +ASSIST_WEAK_MAX_B = 8 # rungs-tier block_max pays only at small K: with K=1024 the tight-line # pass rate runs too high and the prefix read is pure overhead. @@ -138,20 +143,18 @@ def plan_emission( # and this holds for the zero-emission rungs tier too - it is # the same kernel, so the floor is the same return "none" - if ( - have_epilogue - and n_comp >= LIST_EMIT_MIN_N - and (batch == 1 or batch * n_comp * compress_ratio <= LIST_EMIT_MAX_TOKENS) - ): - return "list" weak_max = ASSIST_WEAK_MAX_N_SMALL_K if k <= ASSIST_WEAK_K else ASSIST_WEAK_MAX_N_LARGE_K if batch <= ASSIST_WEAK_MAX_B and ASSIST_WEAK_MIN_N <= n_comp < weak_max: return "none" # stock's split grid wins this band outright - if not have_epilogue: - return "rungs" # closed-loop lines cost nothing to carry - if batch >= RUNGS_MIN_B: - return "rungs" # throughput regime: emitting anything is a loss - return "counts" + if have_epilogue and n_comp >= LIST_EMIT_MIN_N and batch <= LIST_EMIT_MAX_B: + return "list" + if ( + have_epilogue + and batch * n_comp * compress_ratio >= COUNTS_MIN_TOKENS + and not (RUNGS_ONLY_MIN_N <= n_comp < RUNGS_ONLY_MAX_N) + ): + return "counts" + return "rungs" # closed-loop lines cost nothing to carry def pick_config(tier: str, batch: int, n_comp: int, k: int, num_sms: int) -> TopkRoute: From 445bb7d5044c00c6b3fb13e0a9c219e06bbab9e0 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:39:17 -0700 Subject: [PATCH 083/117] [None][perf] GVR: park the tight lines for the list tier The bucketed list emission spends most of its cost on the two tight segments: they claim their slots with exact per-warp ballots so their prefixes stay pad-free, while the loosest segment claims through an amortised per-warp window. Measured on the indexer kernel (nsys kernel-only, ABBA-interleaved NVTX blocks), the exact claims are 2.7-5x the window's cost - at batch 16 / ctx 512k, +88.7us against +15.0us. Park the two tight lines above the score range when the planned tier is list, so every admitted entry lands in the window-claimed segment. The consumer needs no change: it reads the segment counts from the control row, finds both tight segments empty, and takes the loosest line as its cut - the window's sentinel pads score -inf and never rank. Exact on all 3927 (layer, step, batch) combinations of the captured grid. That makes the list tier affordable past a single request, so its gate widens to four, and it now runs ahead of the mid-row weak band - that band is about the stock kernel out-scanning us, and a list hit never scans the row. Scored over the 154-cell grid by the shipped plan_emission, with the list arm re-measured under the new contract: vs this PR's stock vs the reference before 1.349, worst 1.000 1.260, worst 0.856 after 1.387, worst 1.000 1.296, worst 0.900 Still zero cells below stock, 0.5% off the per-cell optimum, and the cells that had to fall back to the stock kernel drop from 20 to 4. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/attention_backend/sparse/dsa.py | 2 +- .../attention_backend/sparse/gvr_ext.py | 23 ++++++++++++++++--- .../blackwell/top_k/gvr_routing.py | 18 +++++++++------ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 160942091d06..52dc9534f86d 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2842,7 +2842,7 @@ def sparse_attn_indexer( torch.cuda.get_device_properties( q_fp8.device).multi_processor_count, compress_ratio=max(self.compress_ratio, 1)) - st.update_seed_rows(batch_size) + st.update_seed_rows(batch_size, emit_tier) gvr_emit_kwargs = st.indexer_emit_kwargs( emit_tier, batch_size) if self._gvr_route.attach_block_max or emit_tier in ( diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index 296ec4a5ed86..525025eb35cf 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -47,6 +47,18 @@ GUARD_LO = 2.0 GUARD_HI = 0.5 +# List tier only: park the two tight lines above any score so every +# admitted entry lands in the loosest segment, which is the one that +# already claims its slots through a per-warp window instead of an +# exact ballot. Measured on the indexer kernel (nsys kernel-only): the +# emission cost drops 2.7-5x (e.g. batch 16 / ctx 512k, +88.7us -> +# +15.0us) and the top-k side stays at parity, because the consumer +# reads the segment counts from the control row and finds the two tight +# segments empty. Any finite value above the score range works; the +# kernel's eligibility check only needs the three lines to be +# increasing and the loosest one finite. +LIST_PARK_LINE = 1.0e30 + class GvrExtState: """Per-attention-backend emission state (persistent buffers).""" @@ -116,7 +128,7 @@ def plan( route = pick_config(self.emitted_tier, batch, n_comp, self.top_k, num_sms) return emit_tier, route - def update_seed_rows(self, num_rows: int) -> None: + def update_seed_rows(self, num_rows: int, emit_tier: str = "counts") -> None: """Device-side closed-loop line update from the last publish. Pure tensor ops (graph-capturable). Rows whose xstate is not @@ -132,8 +144,13 @@ def update_seed_rows(self, num_rows: int) -> None: valid = x[:, 0] > 0 inf = torch.full_like(kth, float("inf")) s[:, 0] = torch.where(valid, kth - GUARD_LO * span, inf) - s[:, 1] = torch.where(valid, kth - 1e-6, inf) - s[:, 2] = torch.where(valid, kth + GUARD_HI * span, inf) + if emit_tier == "list": + park = torch.full_like(kth, LIST_PARK_LINE) + s[:, 1] = torch.where(valid, park, inf) + s[:, 2] = torch.where(valid, park + park, inf) + else: + s[:, 1] = torch.where(valid, kth - 1e-6, inf) + s[:, 2] = torch.where(valid, kth + GUARD_HI * span, inf) s[:, 3:8] = 0.0 if self.cand_ctl is not None: self.cand_ctl[:num_rows].zero_() diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index abfdeeb5e2f8..f96c4ab2d957 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -82,11 +82,13 @@ # (which carries a ~22us launch floor) and so charged 12us at batch 64 / # 512k, where the true cost is zero; that is what put the counts tier # behind a batch gate. The list cost is real per-emitted-entry work and -# grows with batch and context alike; an unbucketed single-segment list -# measures 1.5-5.8x cheaper, but rescoring the grid with it moves the -# geomean by +0.5%, so the bucketed contract stays as is. +# grows with batch and context alike. Parking the two tight lines above +# the score range (see gvr_ext.LIST_PARK_LINE) drops it 2.7-5x - every +# entry then lands in the one segment that claims through a per-warp +# window instead of an exact ballot - at top-k parity, which is what +# makes the list tier affordable past a single row. LIST_EMIT_MIN_N = 65536 # shorter rows: the emission outweighs the saving -LIST_EMIT_MAX_B = 1 # past one row the list never repays its emission +LIST_EMIT_MAX_B = 4 # past four rows the list stops repaying its emission COUNTS_MIN_TOKENS = 524288 # B * raw length; below this the counts # latency chain is exposed and the zero-emission rungs tier wins RUNGS_ONLY_MIN_N = 16384 # short-row band where rungs also beats counts @@ -99,7 +101,7 @@ # the block-skip prefix - so the stock kernel wins outright and the # epilogue should emit nothing. ASSIST_WEAK_MIN_N = 49152 -ASSIST_WEAK_MAX_N_SMALL_K = 262144 # k <= ASSIST_WEAK_K +ASSIST_WEAK_MAX_N_SMALL_K = 196608 # k <= ASSIST_WEAK_K ASSIST_WEAK_MAX_N_LARGE_K = 196608 ASSIST_WEAK_K = 512 ASSIST_WEAK_MAX_B = 8 @@ -143,11 +145,13 @@ def plan_emission( # and this holds for the zero-emission rungs tier too - it is # the same kernel, so the floor is the same return "none" + if have_epilogue and n_comp >= LIST_EMIT_MIN_N and batch <= LIST_EMIT_MAX_B: + # checked before the weak band below: that band is about the stock + # kernel out-scanning us, and a list hit never scans the row + return "list" weak_max = ASSIST_WEAK_MAX_N_SMALL_K if k <= ASSIST_WEAK_K else ASSIST_WEAK_MAX_N_LARGE_K if batch <= ASSIST_WEAK_MAX_B and ASSIST_WEAK_MIN_N <= n_comp < weak_max: return "none" # stock's split grid wins this band outright - if have_epilogue and n_comp >= LIST_EMIT_MIN_N and batch <= LIST_EMIT_MAX_B: - return "list" if ( have_epilogue and batch * n_comp * compress_ratio >= COUNTS_MIN_TOKENS From f8e687158a4e92822c73370e88bb8a702476860d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:34:33 -0700 Subject: [PATCH 084/117] [None][fix] GVR: stop dividing the routing length by the compress ratio twice indexer_max_seq_len is already the compressed length - get_indexer_max_seq_len divides by the compress ratio, and the same value goes to the top-k as max_seq_len a few lines below. The emission planner was handed that value divided by the ratio a second time, so every routing threshold acted on a length two doublings too small. What production actually ran, against what the thresholds were fitted for, over the 154-cell captured grid: rungs counts list none as shipped 74 50 0 30 as intended 62 70 18 4 The list tier was unreachable and 30 shapes fell back to the stock kernel instead of 4. Rescoring the grid with the measured arm costs: vs this PR's stock vs the reference before 1.221, worst 0.788 1.141, worst 0.766 after 1.387, worst 1.000 1.296, worst 0.900 The two ratios are the same value for every layer that reaches this path, and where the module's ratio is 1 the old and new expressions are identical, so this only changes the compressed-KV layers - which are the ones it was wrong for. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- tensorrt_llm/_torch/attention_backend/sparse/dsa.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py index 52dc9534f86d..6f4d679f9306 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa.py @@ -2834,8 +2834,14 @@ def sparse_attn_indexer( top_k=self.index_topk, device=q_fp8.device) st = self._gvr_ext - n_comp = indexer_max_seq_len // max( - self.compress_ratio, 1) + # indexer_max_seq_len is ALREADY the compressed + # length - get_indexer_max_seq_len divides by the + # compress ratio, and it is what goes to the top-k + # as max_seq_len below. Dividing again shifted every + # routing threshold by two doublings: the list tier + # became unreachable and 30 of the 154 grid shapes + # fell back to the stock kernel instead of 4. + n_comp = indexer_max_seq_len emit_tier, self._gvr_route = st.plan( batch_size, n_comp, From 428c25c152d4e3d6bbeacd1575732da3d473f8ec Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:08:21 -0700 Subject: [PATCH 085/117] [None][perf] GVR: resolve P4's coarse bin search lane-parallel The fused rank-and-scatter path looks for the straddling histogram bin with a serial walk: every thread re-sums its warp's whole bin slice, thread 0 alone scans the warp totals, then one lane of the target warp walks that slice bin by bin, with three publish barriers in between. Measured, that search costs 1.04us and does not move when the candidate count changes by 3.6x - it is a dependent-latency chain, not work. The same search already had a lane-parallel form in this file (_kth_bin_search_rw, used by the snap path): stage the slice sums once, then let every warp resolve the answer from them with an idx-shuffle scan and a ballot. Give the rank-scatter path the same treatment. Integer sums are associative, so every warp lands on the same answer and no leader or publish barrier is needed; the serial form stays as the compile-time alternative under p4_warp_redundant. Phase 4, counts tier, flash 256k batch 4, mean over 5 layers x all decode steps: 4.36us -> 3.76us, with the coarse search 1.04 -> 0.63 and the fine search 1.32 -> 1.13 (it now starts from registers instead of a shared-memory round trip). Whole kernel, 27 (tier, shape, batch) cells across list / counts / rungs: faster on 27 of 27, mean +1.47us, median +1.51us - the list tier goes 8.01 -> 5.90us at flash 256k batch 4. Exact on every cell. This one does touch the single-operator path, which also runs this search: it measures +1.07us faster there and 848 PTX instructions smaller, with bit-identical output. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 227 ++++++++++++++---- 1 file changed, 174 insertions(+), 53 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index b409780e2bbc..9ba4c7cdb199 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3492,6 +3492,118 @@ def _kth_bin_search_rw(self, smem_hist, smem_wcnt, lo, binw, tidx, warp_id, lane sel_out = cute.arch.shuffle_sync(sel_loc, src) return thr_out, sel_out + # ------------------------------------------------------------------ + # _p4_coarse_rw - redundant-warp coarse bin search for the fused + # rank-and-scatter path. Same result as the high->low walk it + # replaces (the straddling bin and the count strictly above it), + # but staged once and then resolved lane-parallel on every warp: + # an idx-shuffle scan + ballot locate the target slice, a second + # scan + the unique crossing test locate the bin inside it. Two + # publish barriers and a bins_per_warp-deep serial LDS+IADD chain in + # a single lane disappear; integer sums are associative so every + # warp lands on the same answer bit-for-bit. Mirrors + # _kth_bin_search_rw, which does the same for the snap path. + # ------------------------------------------------------------------ + @cute.jit + def _p4_coarse_rw(self, smem_hist, smem_wcnt, warp_id, lane): + kK = cutlass.const_expr(self.top_k) + kBins = cutlass.const_expr(self.kNumBins) + bins_per_warp = cutlass.const_expr(kBins // self.num_warps) + + warp_bin_sum = cutlass.Int32(0) + if cutlass.const_expr(bins_per_warp % self.WARP_SIZE == 0): + for jm in cutlass.range_constexpr(bins_per_warp // self.WARP_SIZE): + bidx_s = ( + cutlass.Int32(kBins - 1) + - warp_id * cutlass.Int32(bins_per_warp) + - (lane + cutlass.Int32(jm * self.WARP_SIZE)) + ) + warp_bin_sum = warp_bin_sum + smem_hist[bidx_s] + warp_bin_sum = self.warp_reduce_sum_i32(warp_bin_sum) + else: + for jb in cutlass.range_constexpr(bins_per_warp): + bidx_s = ( + cutlass.Int32(kBins - 1) + - warp_id * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb) + ) + warp_bin_sum = warp_bin_sum + smem_hist[bidx_s] + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = warp_bin_sum + cute.arch.barrier() + + # locate the target slice (lane w holds slot w) + v_s = cutlass.Int32(0) + if lane < cutlass.Int32(self.num_warps): + v_s = smem_wcnt[lane] + run2 = v_s + for d2 in cutlass.range_constexpr(5): + off2 = cutlass.const_expr(1 << d2) + src2 = lane - cutlass.Int32(off2) + if src2 < cutlass.Int32(0): + src2 = cutlass.Int32(0) + up2 = cute.arch.shuffle_sync(run2, src2) + if lane >= cutlass.Int32(off2): + run2 = run2 + up2 + m2 = cute.arch.vote_ballot_sync(run2 >= cutlass.Int32(kK)) + tw = cutlass.Int32(self.num_warps - 1) + if m2 != cutlass.Uint32(0): + low2 = m2 & (cutlass.Uint32(0) - m2) + tw = cutlass.Int32(cute.arch.popc(low2 - cutlass.Uint32(1))) + incl_tw = cute.arch.shuffle_sync(run2, tw) + slot_tw = cute.arch.shuffle_sync(v_s, tw) + prefix = incl_tw - slot_tw + + # locate the bin inside the target slice + ppl = cutlass.const_expr((bins_per_warp + self.WARP_SIZE - 1) // self.WARP_SIZE) + cnt_frag = cute.make_fragment((ppl,), cutlass.Int32) + my_sum = cutlass.Int32(0) + for j3 in cutlass.range_constexpr(ppl): + pos = lane * cutlass.Int32(ppl) + cutlass.Int32(j3) + cnt_j = cutlass.Int32(0) + if pos < cutlass.Int32(bins_per_warp): + bidx3 = cutlass.Int32(kBins - 1) - tw * cutlass.Int32(bins_per_warp) - pos + cnt_j = smem_hist[bidx3] + cnt_frag[j3] = cnt_j + my_sum = my_sum + cnt_j + run3 = my_sum + for d3 in cutlass.range_constexpr(5): + off3 = cutlass.const_expr(1 << d3) + src3 = lane - cutlass.Int32(off3) + if src3 < cutlass.Int32(0): + src3 = cutlass.Int32(0) + up3 = cute.arch.shuffle_sync(run3, src3) + if lane >= cutlass.Int32(off3): + run3 = run3 + up3 + base3 = prefix + (run3 - my_sum) + + b_loc = cutlass.Int32(kBins - 1) + ra_loc = prefix + hit = cutlass.Int32(0) + r3 = base3 + for j4 in cutlass.range_constexpr(ppl): + pos4 = lane * cutlass.Int32(ppl) + cutlass.Int32(j4) + cnt4 = cnt_frag[j4] + if ( + pos4 < cutlass.Int32(bins_per_warp) + and r3 < cutlass.Int32(kK) + and r3 + cnt4 >= cutlass.Int32(kK) + and hit == cutlass.Int32(0) + ): + b_loc = cutlass.Int32(kBins - 1) - tw * cutlass.Int32(bins_per_warp) - pos4 + ra_loc = r3 + hit = cutlass.Int32(1) + r3 = r3 + cnt4 + mask3 = cute.arch.vote_ballot_sync(hit != cutlass.Int32(0)) + b_out = cutlass.Int32(kBins - 1) + ra_out = prefix + if mask3 != cutlass.Uint32(0): + low = mask3 & (cutlass.Uint32(0) - mask3) + src = cutlass.Int32(cute.arch.popc(low - cutlass.Uint32(1))) + b_out = cute.arch.shuffle_sync(b_loc, src) + ra_out = cute.arch.shuffle_sync(ra_loc, src) + return b_out, ra_out + # ------------------------------------------------------------------ # Phase 4 (alt): op#7 fused rank-and-scatter (enable_p4_rank_scatter). # Ported verbatim from p4_recursive_digit/gvr_topk_decode_p4.py. @@ -3610,61 +3722,70 @@ def phase4_rank_scatter( cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc2 = cute.arch.clock64() - # ---- 3-step high→low bin search → straddling bin b* + rank_above ---- - warp_bin_sum = cutlass.Int32(0) - for jb in cutlass.range_constexpr(bins_per_warp): - bidx_s = ( - cutlass.Int32(kBins - 1) - - warp_id * cutlass.Int32(bins_per_warp) - - cutlass.Int32(jb) - ) - warp_bin_sum = warp_bin_sum + smem_hist[bidx_s] - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = warp_bin_sum - cute.arch.barrier() - if tidx == cutlass.Int32(0): - cum = cutlass.Int32(0) - tw = cutlass.Int32(num_warps - 1) - found = cutlass.Int32(0) - for w2 in cutlass.range_constexpr(self.num_warps): - cum = cum + smem_wcnt[w2] - if cum >= cutlass.Int32(kK) and found == cutlass.Int32(0): - tw = cutlass.Int32(w2) - found = cutlass.Int32(1) - cum2 = cutlass.Int32(0) - for w3 in cutlass.range_constexpr(self.num_warps): - if cutlass.Int32(w3) < tw: - cum2 = cum2 + smem_wcnt[w3] - s_iscalars[2] = cum2 # prefix-count before target warp - s_iscalars[3] = tw - cute.arch.barrier() - target_warp = s_iscalars[3] - if warp_id == target_warp and lane == cutlass.Int32(0): - base_cum = s_iscalars[2] - b_star = cutlass.Int32(kBins - 1) - rank_above = base_cum - set_d = cutlass.Int32(0) - for jb2 in cutlass.range_constexpr(bins_per_warp): - bidx2 = ( + # ---- high→low bin search → straddling bin b* + rank_above ---- + if cutlass.const_expr(self.p4_warp_redundant): + b_star, rank_above = self._p4_coarse_rw(smem_hist, smem_wcnt, warp_id, lane) + if tidx == cutlass.Int32(0): + s_iscalars[4] = cutlass.Int32(0) # cnt_above + s_iscalars[1] = cutlass.Int32(0) # cnt_straddle + cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc3 = cute.arch.clock64() + else: + warp_bin_sum = cutlass.Int32(0) + for jb in cutlass.range_constexpr(bins_per_warp): + bidx_s = ( cutlass.Int32(kBins - 1) - - target_warp * cutlass.Int32(bins_per_warp) - - cutlass.Int32(jb2) + - warp_id * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb) ) - ra_before = base_cum - base_cum = base_cum + smem_hist[bidx2] - if base_cum >= cutlass.Int32(kK) and set_d == cutlass.Int32(0): - b_star = bidx2 - rank_above = ra_before # count in bins strictly above b* - set_d = cutlass.Int32(1) - s_iscalars[2] = rank_above - s_iscalars[3] = b_star - s_iscalars[4] = cutlass.Int32(0) # cnt_above - s_iscalars[1] = cutlass.Int32(0) # cnt_straddle - cute.arch.barrier() - if cutlass.const_expr(_P4_SUB_DBG): - sc3 = cute.arch.clock64() - b_star = s_iscalars[3] - rank_above = s_iscalars[2] + warp_bin_sum = warp_bin_sum + smem_hist[bidx_s] + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = warp_bin_sum + cute.arch.barrier() + if tidx == cutlass.Int32(0): + cum = cutlass.Int32(0) + tw = cutlass.Int32(num_warps - 1) + found = cutlass.Int32(0) + for w2 in cutlass.range_constexpr(self.num_warps): + cum = cum + smem_wcnt[w2] + if cum >= cutlass.Int32(kK) and found == cutlass.Int32(0): + tw = cutlass.Int32(w2) + found = cutlass.Int32(1) + cum2 = cutlass.Int32(0) + for w3 in cutlass.range_constexpr(self.num_warps): + if cutlass.Int32(w3) < tw: + cum2 = cum2 + smem_wcnt[w3] + s_iscalars[2] = cum2 # prefix-count before target warp + s_iscalars[3] = tw + cute.arch.barrier() + target_warp = s_iscalars[3] + if warp_id == target_warp and lane == cutlass.Int32(0): + base_cum = s_iscalars[2] + b_star_s = cutlass.Int32(kBins - 1) + rank_above_s = base_cum + set_d = cutlass.Int32(0) + for jb2 in cutlass.range_constexpr(bins_per_warp): + bidx2 = ( + cutlass.Int32(kBins - 1) + - target_warp * cutlass.Int32(bins_per_warp) + - cutlass.Int32(jb2) + ) + ra_before = base_cum + base_cum = base_cum + smem_hist[bidx2] + if base_cum >= cutlass.Int32(kK) and set_d == cutlass.Int32(0): + b_star_s = bidx2 + rank_above_s = ra_before # count in bins strictly above b* + set_d = cutlass.Int32(1) + s_iscalars[2] = rank_above_s + s_iscalars[3] = b_star_s + s_iscalars[4] = cutlass.Int32(0) # cnt_above + s_iscalars[1] = cutlass.Int32(0) # cnt_straddle + cute.arch.barrier() + if cutlass.const_expr(_P4_SUB_DBG): + sc3 = cute.arch.clock64() + b_star = s_iscalars[3] + rank_above = s_iscalars[2] # ---- EXACT: one fine-histogram recursion on the straddling bin b* ---- if cutlass.const_expr(self.enable_p4_rank_scatter_exact): From ed8ea661a999b100b610f575f0bf241775cb7088 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:14:08 -0700 Subject: [PATCH 086/117] [None][fix] GVR: pass the stream where the FP4 indexer's __call__ expects it The kernel's __call__ takes the stream right after batch_size, with the emission tensors appended after it - upstream's positional order with our slots on the end. The compile-time argument list still used the older order and put the fake stream after hit_bitmap, so every argument from block_max onwards landed one slot early: DSLRuntimeError: expects argument #15 (hit_bitmap) to be Tensor, but got _FakeStream This takes down the whole of tests/unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py at compile time - 1066 of 1066 cases, reproduced on B200 - and with it the unittest/_torch/attention group. It is why that file has been in every CI failure list since 07-31; the two runs before this one also hit a harness fault, which is what the failure analysis latched onto. The runtime call a few lines below was already right: it drops the stream (the TVM FFI env stream is used) and is otherwise the same sequence. Only the compile list was stale. The FP8 runner already passes the stream in the correct slot and the other compile sites pass it by keyword, so this was the only one. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index fc58140dc55c..ac543c7c28c3 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -9097,10 +9097,15 @@ def _compile(cls, sm_fake, cutlass.Int32(1), cutlass.Int32(1), + # stream sits before the emission tensors in __call__ - + # upstream's positional order, with the emission slots + # appended after it. Keep this list in that order: the + # runtime call below drops the stream (the TVM FFI env + # stream is used) and is otherwise the same sequence. + fake_stream, block_max_fake, hit_stats_fake, hit_bitmap_fake, - fake_stream, seed_thr=seed_thr_fake, seed_counts=seed_counts_fake, cand=cand_fake, From 8c147a724507547adf83375728555c854fa681f4 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:27:38 -0700 Subject: [PATCH 087/117] [None][perf] GVR: give K=512 the compacted boundary-class repair p4_tail_fast was gated on top_k >= 1024, so V4-Flash never took the compacted path. Its boundary-class repair instead ran the original 4-level MSB radix over the WHOLE candidate array: every level re-zeroes 256 histogram bins and re-walks all the candidates, for a class that is usually a few hundred entries. The compacted path pays for the class. That repair only fires when the tie set in the straddling fine bin overfills the remaining slots - about 4% of decode steps - but when it fires it doubles the kernel, and it is the single largest source of worst-case regression. Pairing every step against the reference kernel found 74 (model, context, layer, step) triggers, 53 of them K=512. Reproduced on the captured rows (flash 256k layer 20 step 4, batch 4): the repair costs 5.04us against 0.17us on the neighbouring steps, and 2.17us with the compacted path enabled. Over the four trigger-heavy flash units (8k / 32k / 64k / 256k, 11781 paired steps per tier): rungs counts worst step 0.478 -> 0.612 0.544 -> 0.629 burst steps 284 -> 44 282 -> 35 steps below par 3331 -> 2092 511 -> 594 geomean 1.063 -> 1.089 1.185 -> 1.193 Exact on all 1848 (layer, step, batch) checks. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 10 +++++++++- tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 9ba4c7cdb199..28eb73981783 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -748,7 +748,15 @@ def __init__( # fire census (pro/512k bench + 9 per-layer fixture cells) contains # NO K512 cell — so K512 keeps the original byte-identical kernel. if p4_tail_fast is None: # [p4tt] - p4_tail_fast = self.p4_exact_tail and top_k >= 1024 + # Was gated on top_k >= 1024, which left K=512 on the original + # full-candidate 4-level radix: every level re-zeroes 256 bins + # and re-walks the WHOLE candidate array, so a step whose + # boundary class is large costs ~5us against ~0.2us for its + # neighbours. The compacted path pays for the class only. + # Measured on the captured flash rows that trigger it + # (256k layer 20 step 4, K=512): 5.04us -> 2.17us, with the + # neighbouring steps unchanged at ~0.2us. + p4_tail_fast = self.p4_exact_tail self.p4_tail_fast = bool(p4_tail_fast) and self.p4_exact_tail # [p4tt] # p4_tail_v3: compacted-class repair (block-parallel radix + # pure-tie pre-check) in place of the stock thread0 serial diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 4880a592da9d..81b621279fec 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -68,6 +68,8 @@ def _compile( enable_block_skip: bool = False, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, + p4_exact_tail: "bool | None" = None, + p4_tail_fast: "bool | None" = None, p1r_rescue: bool = True, num_bins: "int | None" = None, p4_fine_rangetest: "bool | None" = None, @@ -211,6 +213,8 @@ def _compile( enable_block_skip=enable_block_skip, pdl_wait_late=pdl_wait_late, p4_tail_v3=p4_tail_v3, + p4_exact_tail=p4_exact_tail, + p4_tail_fast=p4_tail_fast, p1r_rescue=p1r_rescue, num_bins=num_bins, p4_fine_rangetest=p4_fine_rangetest, @@ -678,6 +682,8 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, + p4_exact_tail: "bool | None" = None, + p4_tail_fast: "bool | None" = None, p1r_rescue: bool = True, num_bins: "int | None" = None, p4_fine_rangetest: Optional[bool] = None, @@ -970,6 +976,8 @@ def gvr_topk_decode( enable_block_skip, pdl_wait_late, p4_tail_v3, + p4_exact_tail, + p4_tail_fast, p1r_rescue, num_bins, p4_fine_rangetest, From d1c4cd553d2c8734e9be1f4e516112b5597cb22a Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:16:50 -0700 Subject: [PATCH 088/117] [None][perf] GVR: one candidate pass and a lane-parallel fold in the tail The boundary-class repair walked the candidate array twice: once for the pure-tie min/max over the class, once to compact the class into shared memory. Both passes select on the same predicate and nothing writes the candidate array in between, so the first pass can buffer the members it already reads and the second walk - and its barrier - go away. The staging barrier that publishes the per-warp min/max already orders those reads against the compact writes. The cross-warp fold that follows had every thread walk all num_warps slots of two shared arrays with dependent reads. Lane w holds slot w and one warp reduce settles it, the same shape the coarse bin search already uses. Integer min/max are associative, so every warp still lands on the same answer and the fold stays leaderless. Trigger step (flash 256k layer 20 step 4, batch 4), tail repair: two passes + serial fold 2.33us (pure-tie 0.96 | compact 0.66) one pass + serial fold 2.05us (pure-tie 1.04 | compact 0.33) one pass + parallel fold 1.54us (pure-tie 0.47 | compact 0.37) against 0.34-0.42us on the neighbouring steps. Over the four trigger-heavy flash units (11781 paired steps per tier), measured against the reference kernel and counting the previous commit: rungs counts worst step 0.478 -> 0.610 0.544 -> 0.682 burst steps 284 -> 12 282 -> 34 steps below par 3331 -> 2226 511 -> 459 Exact on all 1848 checks. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 80 ++++++++----------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 28eb73981783..fa81e16c9431 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4045,6 +4045,9 @@ def phase4_rank_scatter( # Staging mirrors the head min/max (wcnt + hist # slots [0..31], both dead here; pairs live at # 260+). + if tidx == cutlass.Int32(0): + s_iscalars[0] = cutlass.Int32(0) + nh7 = cutlass.Int32(0) kmn6 = cutlass.Int32(2147483647) kmx6 = cutlass.Int32(-2147483648) it6 = tidx @@ -4067,6 +4070,14 @@ def phase4_rank_scatter( kmn6 = k6 if k6 > kmx6: kmx6 = k6 + # same predicate as the compaction + # pass used to re-derive: buffer the + # member here so that pass can go + for sl7 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl7) == nh7: + rv7[sl7] = v6 + ri7[sl7] = smem_vals[it6] + nh7 = nh7 + cutlass.Int32(1) it6 = it6 + cutlass.Int32(num_threads) kmn6 = cute.arch.warp_redux_sync(kmn6, "min") kmx6 = cute.arch.warp_redux_sync(kmx6, "max") @@ -4074,56 +4085,31 @@ def phase4_rank_scatter( smem_wcnt[warp_id] = kmn6 smem_hist[warp_id] = kmx6 cute.arch.barrier() - kmn7 = cutlass.Int32(2147483647) - kmx7 = cutlass.Int32(-2147483648) - for w8 in cutlass.range_constexpr(self.num_warps): - pa8 = smem_wcnt[w8] - pb8 = smem_hist[w8] - if pa8 < kmn7: - kmn7 = pa8 - if pb8 > kmx7: - kmx7 = pb8 + # lane-parallel cross-warp fold: lane w holds + # slot w and one warp reduce settles it, instead + # of every thread walking all num_warps slots of + # two arrays with dependent SMEM reads. Same + # inputs in the same order on every warp, so the + # result stays bit-identical and leaderless. + pa8 = cutlass.Int32(2147483647) + pb8 = cutlass.Int32(-2147483648) + if lane < cutlass.Int32(self.num_warps): + pa8 = smem_wcnt[lane] + pb8 = smem_hist[lane] + kmn7 = cute.arch.warp_redux_sync(pa8, "min") + kmx7 = cute.arch.warp_redux_sync(pb8, "max") if kmn7 == kmx7: fast_done = cutlass.Int32(1) if fast_done == cutlass.Int32(0): - # mixed class: compact it IN PLACE - # into smem_keys/vals[0..cnt_strad) with a - # register-buffered two-phase pass (every - # thread reads its strided candidates first, - # ONE barrier, then claimed compact writes — - # no read/write overlap by construction). The - # candidate array has no readers after the - # tail, and compaction makes the repair cost a - # function of the CLASS size only, for ANY - # class size up to cand_count (the old full- - # candidate radix fallback is gone). - if tidx == cutlass.Int32(0): - s_iscalars[0] = cutlass.Int32(0) - nh7 = cutlass.Int32(0) - it7 = tidx - while it7 < cand_count: - v7 = smem_keys[it7] - b7 = cutlass.Int32((v7 - bmin_r) * inv1) - if b7 < cutlass.Int32(0): - b7 = cutlass.Int32(0) - if b7 > cutlass.Int32(kBins - 1): - b7 = cutlass.Int32(kBins - 1) - if b7 == b_star: - s7 = cutlass.Int32((v7 - f_lo) * finv) - if s7 < cutlass.Int32(0): - s7 = cutlass.Int32(0) - if s7 > cutlass.Int32(fbins - 1): - s7 = cutlass.Int32(fbins - 1) - if s7 == sb_star: - # static predicated fragment write - # (dodges dynamic register indexing) - for sl7 in cutlass.range_constexpr(nbuf7): - if cutlass.Int32(sl7) == nh7: - rv7[sl7] = v7 - ri7[sl7] = smem_vals[it7] - nh7 = nh7 + cutlass.Int32(1) - it7 = it7 + cutlass.Int32(num_threads) - cute.arch.barrier() + # mixed class: compact the members buffered by + # the pure-tie pass above into + # smem_keys/vals[0..cnt_strad). The buffering + # pass already read every candidate it needs, + # and the staging barrier above orders those + # reads before these writes, so the second + # full-candidate walk (and its barrier) is + # gone. Repair cost is a function of the CLASS + # size only, for any class size. # warp-aggregated claim: intra-warp exclusive # prefix via shfl scan + ONE atomic per warp # (a thousand same-address claims serialize From 97639c276d9652a951e171fed7ee312f8746a47d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:38:24 -0700 Subject: [PATCH 089/117] [None][perf] GVR routing: narrow the weak band after the phase-4 work The band was fitted before phase 4 got its coarse bin search resolved lane-parallel and its boundary-class repair compacted. Those two gained the assist tiers about 1.5us, which is enough to take the upper half of the band back off the stock kernel. Rescored over the 154-cell grid with the post-fix arm times, by the shipped plan_emission: geomean against this PR's stock 1.370 -> 1.372, against the reference 1.364 -> 1.365, still zero cells below stock. The cells that fall through to the stock kernel drop from 4 to 2, and the counts tier picks them up (72 cells, was 70). The routing is 0.5% off the per-cell optimum given current arm costs. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/blackwell/top_k/gvr_routing.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index f96c4ab2d957..bf7f72eb64b2 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -99,10 +99,13 @@ # its split grid still fits one wave. The assist tiers cannot follow - # splitting a row costs them more than the scan it saves, with or without # the block-skip prefix - so the stock kernel wins outright and the -# epilogue should emit nothing. +# epilogue should emit nothing. Narrowed after phase 4 got its coarse +# search and its boundary-class repair back: the tiers gained about +# 1.5us there, which is enough to take the upper half of the band back +# off the stock kernel (2 cells fall through now, down from 4). ASSIST_WEAK_MIN_N = 49152 -ASSIST_WEAK_MAX_N_SMALL_K = 196608 # k <= ASSIST_WEAK_K -ASSIST_WEAK_MAX_N_LARGE_K = 196608 +ASSIST_WEAK_MAX_N_SMALL_K = 98304 # k <= ASSIST_WEAK_K +ASSIST_WEAK_MAX_N_LARGE_K = 98304 ASSIST_WEAK_K = 512 ASSIST_WEAK_MAX_B = 8 From 2eb7d92781f68cfe17bee9485a9d13cc9782aaaf Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:00:43 -0700 Subject: [PATCH 090/117] [None][perf] GVR: drain the last two serial folds out of phase 4 Phase 4 still had two places where every thread walked all num_warps slots of a shared array with dependent LDS reads, or where one lane walked a whole bin slice while the other 1023 threads waited on a barrier. Both now use the redundant-warp form already proven on the coarse search: stage the per-warp partials once, then let every warp resolve the answer with a shuffle scan and a ballot. Integer sums and min/max both reassociate, so all warps land on the same value, the result is bit-identical, and the publish barriers go away. * the min/max cross-warp fold, measured on flash 256k layer 20 batch 4: 0.87us -> 0.31us per step. This runs on every step of every tier except list (which skips it via ext_range_flag), and is where the whole gain below comes from. * the fine sub-bin search (_p4_fine_rw). On its own this measured flat - that window is dominated by the histogram build pass over all 1024 candidates, not by the search - but it removes two barriers and the fbins/num_warps-deep single-lane chain, and hands sb_star/rank_above_fine back in registers instead of parking them in smem_hist[2]/[3]. Same node, same session, three trees on the va tier (kernel-only, nsys, us/step): cell pre-P4-work committed this tree flash 256k B4 13.35 12.91 11.88 flash 1024k B4 14.74 13.68 12.78 flash 8k B4 6.93 6.62 6.57 Exactness: 2409/2409 steps over flash 8k/64k/256k/1024k and pro 1024k, both assist tiers. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 186 +++++++++++------- 1 file changed, 120 insertions(+), 66 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index fa81e16c9431..5381dece8651 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3612,6 +3612,105 @@ def _p4_coarse_rw(self, smem_hist, smem_wcnt, warp_id, lane): ra_out = cute.arch.shuffle_sync(ra_loc, src) return b_out, ra_out + # ------------------------------------------------------------------ + # _p4_fine_rw - redundant-warp variant of the fine sub-bin search, + # the same transformation _p4_coarse_rw applies one level up. The + # serial form stages per-warp slice sums, has thread 0 walk the warp + # totals, then has one lane of the target warp walk that slice bin by + # bin, with three publish barriers. Here every warp resolves it from + # the staged sums with an idx-shuffle scan and a ballot, so two + # barriers and the fbins/num_warps-deep serial LDS chain disappear. + # Integer sums are associative: every warp lands on the same answer. + # ------------------------------------------------------------------ + @cute.jit + def _p4_fine_rw(self, smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane): + kK = cutlass.const_expr(self.top_k) + fbpw = cutlass.const_expr(fbins // self.num_warps) + + ws = cutlass.Int32(0) + if cutlass.const_expr(fbpw <= self.WARP_SIZE): + if lane < cutlass.Int32(fbpw): + bif = cutlass.Int32(fbins - 1) - warp_id * cutlass.Int32(fbpw) - lane + ws = smem_hist[bif] + ws = self.warp_reduce_sum_i32(ws) + else: + for jm in cutlass.range_constexpr(fbpw): + bif = cutlass.Int32(fbins - 1) - warp_id * cutlass.Int32(fbpw) - cutlass.Int32(jm) + ws = ws + smem_hist[bif] + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = ws + cute.arch.barrier() + + v_s = cutlass.Int32(0) + if lane < cutlass.Int32(self.num_warps): + v_s = smem_wcnt[lane] + run2 = v_s + for d2 in cutlass.range_constexpr(5): + off2 = cutlass.const_expr(1 << d2) + src2 = lane - cutlass.Int32(off2) + if src2 < cutlass.Int32(0): + src2 = cutlass.Int32(0) + up2 = cute.arch.shuffle_sync(run2, src2) + if lane >= cutlass.Int32(off2): + run2 = run2 + up2 + m2 = cute.arch.vote_ballot_sync(rank_above + run2 >= cutlass.Int32(kK)) + tw = cutlass.Int32(self.num_warps - 1) + if m2 != cutlass.Uint32(0): + low2 = m2 & (cutlass.Uint32(0) - m2) + tw = cutlass.Int32(cute.arch.popc(low2 - cutlass.Uint32(1))) + incl_tw = cute.arch.shuffle_sync(run2, tw) + slot_tw = cute.arch.shuffle_sync(v_s, tw) + prefix = rank_above + (incl_tw - slot_tw) + + ppl = cutlass.const_expr((fbpw + self.WARP_SIZE - 1) // self.WARP_SIZE) + cnt_frag = cute.make_fragment((ppl,), cutlass.Int32) + my_sum = cutlass.Int32(0) + for j3 in cutlass.range_constexpr(ppl): + pos = lane * cutlass.Int32(ppl) + cutlass.Int32(j3) + cj = cutlass.Int32(0) + if pos < cutlass.Int32(fbpw): + sbi = cutlass.Int32(fbins - 1) - tw * cutlass.Int32(fbpw) - pos + cj = smem_hist[sbi] + cnt_frag[j3] = cj + my_sum = my_sum + cj + run3 = my_sum + for d3 in cutlass.range_constexpr(5): + off3 = cutlass.const_expr(1 << d3) + src3 = lane - cutlass.Int32(off3) + if src3 < cutlass.Int32(0): + src3 = cutlass.Int32(0) + up3 = cute.arch.shuffle_sync(run3, src3) + if lane >= cutlass.Int32(off3): + run3 = run3 + up3 + base3 = prefix + (run3 - my_sum) + + sb_loc = cutlass.Int32(fbins - 1) + ra_loc = prefix + hit = cutlass.Int32(0) + r3 = base3 + for j4 in cutlass.range_constexpr(ppl): + pos4 = lane * cutlass.Int32(ppl) + cutlass.Int32(j4) + c4 = cnt_frag[j4] + if ( + pos4 < cutlass.Int32(fbpw) + and r3 < cutlass.Int32(kK) + and r3 + c4 >= cutlass.Int32(kK) + and hit == cutlass.Int32(0) + ): + sb_loc = cutlass.Int32(fbins - 1) - tw * cutlass.Int32(fbpw) - pos4 + ra_loc = r3 + hit = cutlass.Int32(1) + r3 = r3 + c4 + mask3 = cute.arch.vote_ballot_sync(hit != cutlass.Int32(0)) + sb_out = cutlass.Int32(fbins - 1) + ra_out = prefix + if mask3 != cutlass.Uint32(0): + low = mask3 & (cutlass.Uint32(0) - mask3) + src = cutlass.Int32(cute.arch.popc(low - cutlass.Uint32(1))) + sb_out = cute.arch.shuffle_sync(sb_loc, src) + ra_out = cute.arch.shuffle_sync(ra_loc, src) + return sb_out, ra_out + # ------------------------------------------------------------------ # Phase 4 (alt): op#7 fused rank-and-scatter (enable_p4_rank_scatter). # Ported verbatim from p4_recursive_digit/gvr_topk_decode_p4.py. @@ -3695,15 +3794,22 @@ def phase4_rank_scatter( smem_wcnt[warp_id] = float_as_uint32(cmin) smem_hist[warp_id] = float_as_uint32(cmax) cute.arch.barrier() - for w in cutlass.range_constexpr(self.num_warps): - vmin = cutlass.Float32( - llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[w].ir_value()) + # lane-parallel cross-warp fold: lane w holds slot w and one + # warp reduce settles it, instead of every thread walking all + # num_warps slots of two arrays with dependent SMEM reads. + # min/max reassociate freely, so the result is bit-identical + # and every warp still lands on it without a leader. + pmn = cutlass.Float32(self.FLT_MAX) + pmx = cutlass.Float32(self.NEG_FLT_MAX) + if lane < cutlass.Int32(self.num_warps): + pmn = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[lane].ir_value()) ) - vmax = cutlass.Float32( - llvm.bitcast(cutlass.Float32.mlir_type, smem_hist[w].ir_value()) + pmx = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_hist[lane].ir_value()) ) - bmin_r = _fmin_f32_inline(bmin_r, vmin) - bmax_r = cute.arch.fmax(bmax_r, vmax) + bmin_r = _fmin_f32_inline(bmin_r, self.warp_reduce_min_f32(pmn)) + bmax_r = cute.arch.fmax(bmax_r, self.warp_reduce_max_f32(pmx)) if bmax_r <= bmin_r: bmax_r = bmin_r + cutlass.Float32(1e-6) cute.arch.barrier() @@ -3802,7 +3908,6 @@ def phase4_rank_scatter( # sub-bins over bin b* gives kNumBins×256 effective resolution, # enough to resolve the straddling bin to ≤1 distinct value. fbins = cutlass.const_expr(256) - fbpw = cutlass.const_expr(256 // self.num_warps) # bin b* value range under the inv1 binning: [f_lo, f_lo + 1/inv1) f_lo = bmin_r + cutlass.Float32(b_star) / inv1 finv = (cutlass.Float32(fbins - 1) + cutlass.Float32(0.99)) * inv1 @@ -3862,62 +3967,13 @@ def phase4_rank_scatter( atomicAdd(smem_hist.iterator + sbo, cutlass.Int32(1)) ifb = ifb + cutlass.Int32(num_threads) cute.arch.barrier() - # fine 3-step search seeded at rank_above (over fbins bins) - fws = cutlass.Int32(0) - for jbf in cutlass.range_constexpr(fbpw): - bif = ( - cutlass.Int32(fbins - 1) - - warp_id * cutlass.Int32(fbpw) - - cutlass.Int32(jbf) - ) - fws = fws + smem_hist[bif] - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = fws - cute.arch.barrier() - if tidx == cutlass.Int32(0): - cumf = rank_above - twf = cutlass.Int32(num_warps - 1) - fnd = cutlass.Int32(0) - for w2 in cutlass.range_constexpr(self.num_warps): - cumf = cumf + smem_wcnt[w2] - if cumf >= cutlass.Int32(kK) and fnd == cutlass.Int32(0): - twf = cutlass.Int32(w2) - fnd = cutlass.Int32(1) - pre = rank_above - for w3 in cutlass.range_constexpr(self.num_warps): - if cutlass.Int32(w3) < twf: - pre = pre + smem_wcnt[w3] - # Stage prefix/target-warp metadata in spare s_iscalars - # slots, NOT smem_hist[0]/[1]: the last fine warp's reverse - # scan below walks fine bins down to 0/1, so reusing those - # histogram bins as scratch would corrupt sb_star/ra_fine - # when twf2 == num_warps-1. Slots [4]/[1] are dead here - # (re-zeroed at the cnt_above/cnt_strad reset below). - s_iscalars[4] = pre # prefix into target fine warp - s_iscalars[1] = twf # target fine warp - cute.arch.barrier() - pre_f = s_iscalars[4] - twf2 = s_iscalars[1] - if warp_id == twf2 and lane == cutlass.Int32(0): - base_f = pre_f - sb_star = cutlass.Int32(fbins - 1) - ra_fine = base_f - sd = cutlass.Int32(0) - for jb3 in cutlass.range_constexpr(fbpw): - sbi = ( - cutlass.Int32(fbins - 1) - - twf2 * cutlass.Int32(fbpw) - - cutlass.Int32(jb3) - ) - ra_b = base_f - base_f = base_f + smem_hist[sbi] - if base_f >= cutlass.Int32(kK) and sd == cutlass.Int32(0): - sb_star = sbi - ra_fine = ra_b - sd = cutlass.Int32(1) - smem_hist[2] = sb_star - smem_hist[3] = ra_fine - cute.arch.barrier() + # fine sub-bin search, resolved lane-parallel on every + # warp (see _p4_fine_rw): the two publish barriers and the + # fbins/num_warps-deep single-lane walk that used to sit + # here are gone, and the answer comes back in registers. + sb_star, rank_above_fine = self._p4_fine_rw( + smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane + ) if tidx == cutlass.Int32(0): s_iscalars[4] = cutlass.Int32(0) # cnt_above s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) @@ -3925,8 +3981,6 @@ def phase4_rank_scatter( cute.arch.barrier() if cutlass.const_expr(_P4_SUB_DBG): sc4 = cute.arch.clock64() - sb_star = smem_hist[2] - rank_above_fine = smem_hist[3] isc = tidx while isc < cand_count: v = smem_keys[isc] From 26f4bc6b2605047a8c93a7878ee37a5146961a2e Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:09:46 -0700 Subject: [PATCH 091/117] [None][perf] GVR: stop paying the big-class tail machinery on a 2-way tie The exact-tail repair fires when the straddling fine bin holds more candidates than there are output slots left. On the captured rows it fires with a class of TWO and one slot to fill, and it cost 3.6us to break that tie - which is where essentially all of the remaining per-step regressions live. Per-step pairing over the routed grid put 5.7% of pro/flash 8k-32k steps below the stock kernel; the phase clock shows head/histogram/coarse/fine identical on those steps and the whole delta in the tail window. Two things were being bought for a class of two: * a whole extra candidate walk for the pure-tie pre-check. That check only exists to SKIP the repair when the class is bit-uniform, but the small route below ranks by (key, arrival) and rewrites exactly the need0 winner slots, which is already value-exact on a pure tie. So for a small class the walk is bought for nothing. * a per-thread staging array nbuf7 = kC/num_threads deep, whose every store is an unrolled dynamic-index search over it. It exists to save the compaction its own walk - but a candidate walk is ~0.3us and the array measured 1.2us in the compaction alone. A small mixed class now takes its own route ahead of both: one candidate walk claiming straight into a pair buffer parked in the (dead) digit bins, then the same warp0 pairwise rank. Nothing rides in registers, and smem_keys stays readable while the pass runs, so no staging barrier is needed. The large-class route keeps the pre-check, the staging array and the radix - it is the one that really wants to avoid a second walk. Measured on the pro 8k capture (30 layers, 420 cold steps, phase-4 clock, us/step): step class before after fires (class 2) 6.77 5.72 does not fire 3.14 3.14 Exactness: 30/30 layers on the firing capture (B200); 1947/1947 records over flash 8k/64k/256k/1024k and pro 1024k on both assist tiers (B300); the full decode test file passes on both B200 and B300 (708 passed, 144 skipped, 1 xfailed). About 2.5us of the fire-step delta is still unaccounted for and is not in these two passes - the remaining route is one walk, a few barriers and a rank over two elements. That is the next thing to chase. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 365 +++++++++++------- 1 file changed, 218 insertions(+), 147 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 5381dece8651..707acf0085c5 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4105,7 +4105,15 @@ def phase4_rank_scatter( kmn6 = cutlass.Int32(2147483647) kmx6 = cutlass.Int32(-2147483648) it6 = tidx - while it6 < cand_count: + # The pure-tie pre-check only exists to SKIP the + # repair when the class is bit-uniform. The small + # route below ranks by (key, arrival) and rewrites + # exactly the need0 winner slots, which is already + # value-exact on a pure tie - so for a small class + # the check is a whole candidate walk bought for + # nothing. Only the large-class route, whose radix + # really is worth avoiding, still pays for it. + while it6 < cand_count and cnt_strad > cutlass.Int32(128): v6 = smem_keys[it6] b6 = cutlass.Int32((v6 - bmin_r) * inv1) if b6 < cutlass.Int32(0): @@ -4124,13 +4132,25 @@ def phase4_rank_scatter( kmn6 = k6 if k6 > kmx6: kmx6 = k6 - # same predicate as the compaction - # pass used to re-derive: buffer the - # member here so that pass can go - for sl7 in cutlass.range_constexpr(nbuf7): - if cutlass.Int32(sl7) == nh7: - rv7[sl7] = v6 - ri7[sl7] = smem_vals[it6] + # Buffering the member here saves the + # compaction its own walk, but the + # per-thread array is nbuf7 = + # kC/num_threads deep and every store + # is an unrolled dynamic-index search + # over it. Measured on pro 8k (class + # 2, one slot to fill): this pass + # 2.4us + compaction 1.2us, against a + # second candidate walk that costs + # ~0.3us. So buffer ONLY for classes + # too large for the re-walk route + # below; cnt_strad is block-uniform, + # so small classes skip the unrolled + # stores entirely. + if cnt_strad > cutlass.Int32(128): + for sl7 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl7) == nh7: + rv7[sl7] = v6 + ri7[sl7] = smem_vals[it6] nh7 = nh7 + cutlass.Int32(1) it6 = it6 + cutlass.Int32(num_threads) kmn6 = cute.arch.warp_redux_sync(kmn6, "min") @@ -4154,7 +4174,92 @@ def phase4_rank_scatter( kmx7 = cute.arch.warp_redux_sync(pb8, "max") if kmn7 == kmx7: fast_done = cutlass.Int32(1) - if fast_done == cutlass.Int32(0): + if cnt_strad <= cutlass.Int32(128): + # Small mixed class: walk the candidates once + # more and claim straight into a pair buffer + # in the (dead) digit bins, so nothing has to + # ride in registers and smem_keys stays + # readable while the pass runs. 128 pairs fit + # under the 256 bins the radix route would + # otherwise zero. + if tidx == cutlass.Int32(0): + s_iscalars[0] = cutlass.Int32(0) + cute.arch.barrier() + itc9 = tidx + while itc9 < cand_count: + tv9 = smem_keys[itc9] + tb9 = cutlass.Int32((tv9 - bmin_r) * inv1) + if tb9 < cutlass.Int32(0): + tb9 = cutlass.Int32(0) + if tb9 > cutlass.Int32(kBins - 1): + tb9 = cutlass.Int32(kBins - 1) + if tb9 == b_star: + ts9 = cutlass.Int32((tv9 - f_lo) * finv) + if ts9 < cutlass.Int32(0): + ts9 = cutlass.Int32(0) + if ts9 > cutlass.Int32(fbins - 1): + ts9 = cutlass.Int32(fbins - 1) + if ts9 == sb_star: + to9 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if to9 < cutlass.Int32(128): + smem_hist[to9 + to9] = float_as_int32(tv9) + smem_hist[to9 + to9 + cutlass.Int32(1)] = smem_vals[ + itc9 + ] + itc9 = itc9 + cutlass.Int32(num_threads) + cute.arch.barrier() + # warp0 exact pairwise rank over the pairs + # (rank = #{key greater} + #{key equal, + # earlier slot} is unique in [0, class)), + # rewriting each winner slot once. + if warp_id == cutlass.Int32(0): + ie9 = lane + while ie9 < cnt_strad: + bi9 = smem_hist[ie9 + ie9] + ki9 = f32_order_key( + cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, bi9.ir_value() + ) + ) + ) ^ cutlass.Int32(-2147483648) + r9 = cutlass.Int32(0) + j9 = cutlass.Int32(0) + while j9 < cnt_strad: + bj9 = smem_hist[j9 + j9] + kj9 = f32_order_key( + cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, bj9.ir_value() + ) + ) + ) ^ cutlass.Int32(-2147483648) + if kj9 > ki9: + r9 = r9 + cutlass.Int32(1) + elif kj9 == ki9 and j9 < ie9: + r9 = r9 + cutlass.Int32(1) + j9 = j9 + cutlass.Int32(1) + if r9 < need0: + pos9 = rank_above_fine + r9 + if pos9 < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[pos9] = self.dtype( + cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + bi9.ir_value(), + ) + ) + ) + output_indices_row[pos9] = smem_hist[ + ie9 + ie9 + cutlass.Int32(1) + ] + ie9 = ie9 + cutlass.Int32(32) + cute.arch.barrier() + elif fast_done == cutlass.Int32(0): # mixed class: compact the members buffered by # the pure-tie pass above into # smem_keys/vals[0..cnt_strad). The buffering @@ -4189,154 +4294,120 @@ def phase4_rank_scatter( smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] cute.arch.barrier() - if cnt_strad <= cutlass.Int32(128): - # warp0 exact pairwise rank (rank = #{key - # greater} + #{key equal, earlier slot} is - # unique in [0, class)) rewrites every - # winner slot in [raf, raf + need0) once. - if warp_id == cutlass.Int32(0): - ie5 = lane - while ie5 < cnt_strad: - vi5 = smem_keys[ie5] - ki5 = f32_order_key(vi5) ^ cutlass.Int32(-2147483648) - r5 = cutlass.Int32(0) - j5 = cutlass.Int32(0) - while j5 < cnt_strad: - vj5 = smem_keys[j5] - kj5 = f32_order_key(vj5) ^ cutlass.Int32( - -2147483648 - ) - if kj5 > ki5: - r5 = r5 + cutlass.Int32(1) - elif kj5 == ki5 and j5 < ie5: - r5 = r5 + cutlass.Int32(1) - j5 = j5 + cutlass.Int32(1) - if r5 < need0: - pos = rank_above_fine + r5 - if pos < cutlass.Int32(kK): - if cutlass.const_expr( - self.return_output_values - ): - output_values_row[pos] = self.dtype(vi5) - output_indices_row[pos] = smem_vals[ie5] - ie5 = ie5 + cutlass.Int32(32) + # Large class only (the small one is handled + # above, ahead of the pure-tie pass, so it + # never reaches here): block-parallel 4-level + # MSB radix over the compacted class (scans + # touch class pairs only; warp0 shuffle-scan + # digit search - 3 block barriers per level + # instead of 5). + if tidx == cutlass.Int32(0): + smem_hist[256] = cutlass.Int32(0) + smem_hist[257] = need0 + smem_hist[258] = cutlass.Int32(0) + cute.arch.barrier() + for lvl2 in cutlass.range_constexpr(4): + shift2 = cutlass.const_expr(24 - 8 * lvl2) + iz3 = tidx + while iz3 < cutlass.Int32(256): + smem_hist[iz3] = cutlass.Int32(0) + iz3 = iz3 + cutlass.Int32(num_threads) cute.arch.barrier() - else: - # block-parallel 4-level MSB radix over the - # compacted class (scans touch class pairs - # only; warp0 shuffle-scan digit search — 3 - # block barriers per level instead of 5). - if tidx == cutlass.Int32(0): - smem_hist[256] = cutlass.Int32(0) - smem_hist[257] = need0 - smem_hist[258] = cutlass.Int32(0) + uthr_c2 = smem_hist[256] + ic2 = tidx + while ic2 < cnt_strad: + uk3 = f32_order_key(smem_keys[ic2]) + pm2 = cutlass.Int32(1) + if cutlass.const_expr(lvl2 > 0): + if (uk3 >> cutlass.Int32(shift2 + 8)) != ( + uthr_c2 >> cutlass.Int32(shift2 + 8) + ): + pm2 = cutlass.Int32(0) + if pm2 == cutlass.Int32(1): + dg2 = (uk3 >> cutlass.Int32(shift2)) & cutlass.Int32( + 0xFF + ) + atomicAdd(smem_hist.iterator + dg2, cutlass.Int32(1)) + ic2 = ic2 + cutlass.Int32(num_threads) cute.arch.barrier() - for lvl2 in cutlass.range_constexpr(4): - shift2 = cutlass.const_expr(24 - 8 * lvl2) - iz3 = tidx - while iz3 < cutlass.Int32(256): - smem_hist[iz3] = cutlass.Int32(0) - iz3 = iz3 + cutlass.Int32(num_threads) - cute.arch.barrier() - uthr_c2 = smem_hist[256] - ic2 = tidx - while ic2 < cnt_strad: - uk3 = f32_order_key(smem_keys[ic2]) - pm2 = cutlass.Int32(1) - if cutlass.const_expr(lvl2 > 0): - if (uk3 >> cutlass.Int32(shift2 + 8)) != ( - uthr_c2 >> cutlass.Int32(shift2 + 8) - ): - pm2 = cutlass.Int32(0) - if pm2 == cutlass.Int32(1): - dg2 = ( - uk3 >> cutlass.Int32(shift2) - ) & cutlass.Int32(0xFF) - atomicAdd( - smem_hist.iterator + dg2, cutlass.Int32(1) - ) - ic2 = ic2 + cutlass.Int32(num_threads) - cute.arch.barrier() - if warp_id == cutlass.Int32(0): - ws3 = cutlass.Int32(0) - for jd3 in cutlass.range_constexpr(8): - di3 = ( + if warp_id == cutlass.Int32(0): + ws3 = cutlass.Int32(0) + for jd3 in cutlass.range_constexpr(8): + di3 = ( + cutlass.Int32(255) + - lane * cutlass.Int32(8) + - cutlass.Int32(jd3) + ) + ws3 = ws3 + smem_hist[di3] + pre6 = ws3 + for so2 in cutlass.range_constexpr(5): + oth2 = cute.arch.shuffle_sync_up( + pre6, + cutlass.Int32(1 << so2), + mask_and_clamp=0, + ) + if lane >= cutlass.Int32(1 << so2): + pre6 = pre6 + oth2 + needl3 = smem_hist[257] + if pre6 >= needl3 and (pre6 - ws3) < needl3: + base5 = pre6 - ws3 + dstar2 = cutlass.Int32(0) + above5 = base5 + sd5 = cutlass.Int32(0) + for jd4 in cutlass.range_constexpr(8): + di4 = ( cutlass.Int32(255) - lane * cutlass.Int32(8) - - cutlass.Int32(jd3) - ) - ws3 = ws3 + smem_hist[di3] - pre6 = ws3 - for so2 in cutlass.range_constexpr(5): - oth2 = cute.arch.shuffle_sync_up( - pre6, - cutlass.Int32(1 << so2), - mask_and_clamp=0, - ) - if lane >= cutlass.Int32(1 << so2): - pre6 = pre6 + oth2 - needl3 = smem_hist[257] - if pre6 >= needl3 and (pre6 - ws3) < needl3: - base5 = pre6 - ws3 - dstar2 = cutlass.Int32(0) - above5 = base5 - sd5 = cutlass.Int32(0) - for jd4 in cutlass.range_constexpr(8): - di4 = ( - cutlass.Int32(255) - - lane * cutlass.Int32(8) - - cutlass.Int32(jd4) - ) - ra5 = base5 - base5 = base5 + smem_hist[di4] - if base5 >= needl3 and sd5 == cutlass.Int32(0): - dstar2 = di4 - above5 = ra5 - sd5 = cutlass.Int32(1) - smem_hist[256] = uthr_c2 | ( - dstar2 << cutlass.Int32(shift2) + - cutlass.Int32(jd4) ) - smem_hist[257] = needl3 - above5 - smem_hist[258] = smem_hist[258] + above5 - cute.arch.barrier() - u_thr2 = smem_hist[256] - cnt_ab2 = smem_hist[258] - need_eq2 = smem_hist[257] - kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) - if tidx == cutlass.Int32(0): - s_iscalars[4] = cutlass.Int32(0) - s_iscalars[0] = cutlass.Int32(0) - cute.arch.barrier() - ir3 = tidx - while ir3 < cnt_strad: - vv3 = smem_keys[ir3] - uk4 = f32_order_key(vv3) - ks4 = uk4 ^ cutlass.Int32(-2147483648) - if ks4 > kthr2: - o4 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(4), - cutlass.Int32(1), + ra5 = base5 + base5 = base5 + smem_hist[di4] + if base5 >= needl3 and sd5 == cutlass.Int32(0): + dstar2 = di4 + above5 = ra5 + sd5 = cutlass.Int32(1) + smem_hist[256] = uthr_c2 | ( + dstar2 << cutlass.Int32(shift2) ) - pos = rank_above_fine + o4 + smem_hist[257] = needl3 - above5 + smem_hist[258] = smem_hist[258] + above5 + cute.arch.barrier() + u_thr2 = smem_hist[256] + cnt_ab2 = smem_hist[258] + need_eq2 = smem_hist[257] + kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) + if tidx == cutlass.Int32(0): + s_iscalars[4] = cutlass.Int32(0) + s_iscalars[0] = cutlass.Int32(0) + cute.arch.barrier() + ir3 = tidx + while ir3 < cnt_strad: + vv3 = smem_keys[ir3] + uk4 = f32_order_key(vv3) + ks4 = uk4 ^ cutlass.Int32(-2147483648) + if ks4 > kthr2: + o4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(4), + cutlass.Int32(1), + ) + pos = rank_above_fine + o4 + if pos < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[pos] = self.dtype(vv3) + output_indices_row[pos] = smem_vals[ir3] + elif ks4 == kthr2: + q4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if q4 < need_eq2: + pos = rank_above_fine + cnt_ab2 + q4 if pos < cutlass.Int32(kK): if cutlass.const_expr(self.return_output_values): output_values_row[pos] = self.dtype(vv3) output_indices_row[pos] = smem_vals[ir3] - elif ks4 == kthr2: - q4 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), - cutlass.Int32(1), - ) - if q4 < need_eq2: - pos = rank_above_fine + cnt_ab2 + q4 - if pos < cutlass.Int32(kK): - if cutlass.const_expr( - self.return_output_values - ): - output_values_row[pos] = self.dtype(vv3) - output_indices_row[pos] = smem_vals[ir3] - ir3 = ir3 + cutlass.Int32(num_threads) - cute.arch.barrier() + ir3 = ir3 + cutlass.Int32(num_threads) + cute.arch.barrier() else: need0_s = cutlass.Int32(kK) - rank_above_fine if cnt_strad > need0_s and need0_s > cutlass.Int32(0): From f378bf87fd666d3b1abf9b1d3119326461a1f156 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 4 Aug 2026 19:59:01 -0700 Subject: [PATCH 092/117] [None][perf] GVR: let the scatter hand the tie class to the tail repair The exact-tail repair fires on a class of two candidates contending for one slot, and even after the previous commit it still cost 1.5us more than a step that does not fire. Attribution by clock stamp, on the pro 8k capture (phase-4 window, us/step): the rank itself 0.33, a candidate walk to collect the class 0.61, and 0.55 of machinery that runs whether or not it is needed. All three go away: * The scatter already classifies every candidate and hands out the intra-class ordinal, so it now parks each member as a (value bits, index) pair above the digit bins. The repair reads them directly - no collection walk, no staging barrier. * The pure-tie pre-check, its cross-warp fold and the publish barrier that fold needs exist only to decide whether the radix can be skipped. The small-class route needs none of it and used to run it anyway; it now lives in the large-class branch, which is the one with a radix to avoid. * The rank loop is forced rolled (unroll=1). Its trip count is 1-2, but an unrolled body plus its remainder ladder is code volume that ONE warp walks cold while every other warp waits at the barrier below, so the fetch latency has nothing to hide behind. The pair buffer's capacity is DERIVED from the histogram size, not assumed: K=2048 with R0 shrinks the bin count to 512, where a fixed 128 pairs would run four ints past the end of the allocation and into the per-thread count buffer that follows it. It is rounded down to a power of two so the scatter can wrap the ordinal with a mask instead of a bounds branch, and a class past the cap takes the large-class route, which does not read the buffer. Phase-4 window on the firing capture (pro 8k, 30 layers, 420 cold steps, us/step): step class before this after fires (class 2) 6.77 3.42 does not fire 3.14 3.16 Per-step paired against the same baseline, same node, same session, over the six cells that carried the regressions (6804 steps, previous tree vs this one): steps slower than baseline 92 -> 27 steps below 0.9 29 -> 18 worst single step 0.642 -> 0.675 mean 1.254 -> 1.248 Exactness: 30/30 layers on the firing capture; 1023/1023 records over flash 8k/64k/256k and pro 1024k on both assist tiers; the full decode test file passes on B200 and B300 (708 passed, 144 skipped, 1 xfailed). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 558 +++++++++--------- 1 file changed, 290 insertions(+), 268 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 707acf0085c5..07882ed648b6 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -59,6 +59,28 @@ def _env_flag(name: str) -> bool: # GVR_P4_SUB_DBG=2: publish the P4 HEAD triple (minmax / histogram build / # coarse search) instead of the tail triple, so the phase budget adds up. _P4_SUB_HEAD = os.environ.get("GVR_P4_SUB_DBG", "0").strip() == "2" +# Exact-tail small-class pair buffer. The scatter parks each member of the +# straddling tie class as (value bits, index) here, above the 256 digit bins +# the large-class radix zeroes and above its [256..258] scalars, so the repair +# never has to re-walk the candidates. The capacity is DERIVED from the bin +# count, never assumed: K=2048 with R0 shrinks the histogram to 512 bins (see +# the kNumBins override in __init__), where 260 + 2*128 would run 4 ints past +# the end of the allocation - into the per-thread count buffer that follows it. +# Rounded down to a power of two so the scatter can wrap the ordinal with a +# mask instead of a bounds branch; a class past the cap takes the large-class +# route, which does not use this buffer. +_PAIR_BASE = 260 +_PAIR_MAX = 128 + + +def _pair_cap_for(n_bins: int) -> int: + """Largest power-of-two pair count that fits [_PAIR_BASE, n_bins).""" + room = (n_bins - _PAIR_BASE) // 2 + if room < 1: + return 0 + return min(_PAIR_MAX, 1 << (room.bit_length() - 1)) + + _SKIP_DBG = _env_flag("GVR_SKIP_DBG") @@ -3735,6 +3757,7 @@ def phase4_rank_scatter( ): kK = cutlass.const_expr(self.top_k) kBins = cutlass.const_expr(self.kNumBins) + pair_cap = cutlass.const_expr(_pair_cap_for(self.kNumBins)) num_threads = cutlass.const_expr(self.num_threads) num_warps = cutlass.const_expr(self.num_warps) bins_per_warp = cutlass.const_expr(kBins // self.num_warps) @@ -4031,6 +4054,28 @@ def phase4_rank_scatter( output_indices_row[pos] = smem_vals[isc] elif sb == sb_star: o = atomicAdd(s_iscalars.iterator + cutlass.Int32(1), cutlass.Int32(1)) + if cutlass.const_expr( + self.p4_exact_tail and self.p4_tail_fast and self.p4_tail_v3 + ): + # The tie class is exactly what the exact-tail + # repair needs, and this pass has already + # classified every candidate AND handed out the + # intra-class ordinal. Park (value bits, index) + # here so the repair does not have to walk the + # candidates again: measured 0.61us of its + # remaining cost was that second walk. Pairs + # live above the digit bins the radix route + # zeroes; small classes are the only consumer. + # Unconditional, index wrapped: the buffer is + # only ever READ when the class fits it, and + # then the wrap is a no-op. Dropping the bound + # branch keeps this off the scatter's critical + # path, which every step pays. + ow = (o & cutlass.Int32(pair_cap - 1)) * cutlass.Int32(2) + smem_hist[cutlass.Int32(_PAIR_BASE) + ow] = float_as_int32(v) + smem_hist[cutlass.Int32(_PAIR_BASE) + ow + cutlass.Int32(1)] = ( + smem_vals[isc] + ) pos = rank_above_fine + o if pos < cutlass.Int32(kK): if cutlass.const_expr(self.return_output_values): @@ -4090,135 +4135,24 @@ def phase4_rank_scatter( fast_done = cutlass.Int32(1) if cnt_strad > need0 and need0 > cutlass.Int32(0): fast_done = cutlass.Int32(0) - # block-wide pure-tie check, ANY class - # size: min/max order key over the (b*, sb*) class. - # A pure-tie class needs NO repair — the scatter's - # arrival fill of bit-equal values is already - # value-set exact. Real fp8-lineage logits tie in - # the thousands, which used to take the full radix. - # Staging mirrors the head min/max (wcnt + hist - # slots [0..31], both dead here; pairs live at - # 260+). - if tidx == cutlass.Int32(0): - s_iscalars[0] = cutlass.Int32(0) - nh7 = cutlass.Int32(0) - kmn6 = cutlass.Int32(2147483647) - kmx6 = cutlass.Int32(-2147483648) - it6 = tidx - # The pure-tie pre-check only exists to SKIP the - # repair when the class is bit-uniform. The small - # route below ranks by (key, arrival) and rewrites - # exactly the need0 winner slots, which is already - # value-exact on a pure tie - so for a small class - # the check is a whole candidate walk bought for - # nothing. Only the large-class route, whose radix - # really is worth avoiding, still pays for it. - while it6 < cand_count and cnt_strad > cutlass.Int32(128): - v6 = smem_keys[it6] - b6 = cutlass.Int32((v6 - bmin_r) * inv1) - if b6 < cutlass.Int32(0): - b6 = cutlass.Int32(0) - if b6 > cutlass.Int32(kBins - 1): - b6 = cutlass.Int32(kBins - 1) - if b6 == b_star: - s6 = cutlass.Int32((v6 - f_lo) * finv) - if s6 < cutlass.Int32(0): - s6 = cutlass.Int32(0) - if s6 > cutlass.Int32(fbins - 1): - s6 = cutlass.Int32(fbins - 1) - if s6 == sb_star: - k6 = f32_order_key(v6) ^ cutlass.Int32(-2147483648) - if k6 < kmn6: - kmn6 = k6 - if k6 > kmx6: - kmx6 = k6 - # Buffering the member here saves the - # compaction its own walk, but the - # per-thread array is nbuf7 = - # kC/num_threads deep and every store - # is an unrolled dynamic-index search - # over it. Measured on pro 8k (class - # 2, one slot to fill): this pass - # 2.4us + compaction 1.2us, against a - # second candidate walk that costs - # ~0.3us. So buffer ONLY for classes - # too large for the re-walk route - # below; cnt_strad is block-uniform, - # so small classes skip the unrolled - # stores entirely. - if cnt_strad > cutlass.Int32(128): - for sl7 in cutlass.range_constexpr(nbuf7): - if cutlass.Int32(sl7) == nh7: - rv7[sl7] = v6 - ri7[sl7] = smem_vals[it6] - nh7 = nh7 + cutlass.Int32(1) - it6 = it6 + cutlass.Int32(num_threads) - kmn6 = cute.arch.warp_redux_sync(kmn6, "min") - kmx6 = cute.arch.warp_redux_sync(kmx6, "max") - if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = kmn6 - smem_hist[warp_id] = kmx6 - cute.arch.barrier() - # lane-parallel cross-warp fold: lane w holds - # slot w and one warp reduce settles it, instead - # of every thread walking all num_warps slots of - # two arrays with dependent SMEM reads. Same - # inputs in the same order on every warp, so the - # result stays bit-identical and leaderless. - pa8 = cutlass.Int32(2147483647) - pb8 = cutlass.Int32(-2147483648) - if lane < cutlass.Int32(self.num_warps): - pa8 = smem_wcnt[lane] - pb8 = smem_hist[lane] - kmn7 = cute.arch.warp_redux_sync(pa8, "min") - kmx7 = cute.arch.warp_redux_sync(pb8, "max") - if kmn7 == kmx7: - fast_done = cutlass.Int32(1) - if cnt_strad <= cutlass.Int32(128): - # Small mixed class: walk the candidates once - # more and claim straight into a pair buffer - # in the (dead) digit bins, so nothing has to - # ride in registers and smem_keys stays - # readable while the pass runs. 128 pairs fit - # under the 256 bins the radix route would - # otherwise zero. - if tidx == cutlass.Int32(0): - s_iscalars[0] = cutlass.Int32(0) - cute.arch.barrier() - itc9 = tidx - while itc9 < cand_count: - tv9 = smem_keys[itc9] - tb9 = cutlass.Int32((tv9 - bmin_r) * inv1) - if tb9 < cutlass.Int32(0): - tb9 = cutlass.Int32(0) - if tb9 > cutlass.Int32(kBins - 1): - tb9 = cutlass.Int32(kBins - 1) - if tb9 == b_star: - ts9 = cutlass.Int32((tv9 - f_lo) * finv) - if ts9 < cutlass.Int32(0): - ts9 = cutlass.Int32(0) - if ts9 > cutlass.Int32(fbins - 1): - ts9 = cutlass.Int32(fbins - 1) - if ts9 == sb_star: - to9 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), - cutlass.Int32(1), - ) - if to9 < cutlass.Int32(128): - smem_hist[to9 + to9] = float_as_int32(tv9) - smem_hist[to9 + to9 + cutlass.Int32(1)] = smem_vals[ - itc9 - ] - itc9 = itc9 + cutlass.Int32(num_threads) - cute.arch.barrier() - # warp0 exact pairwise rank over the pairs - # (rank = #{key greater} + #{key equal, - # earlier slot} is unique in [0, class)), - # rewriting each winner slot once. + if cnt_strad <= cutlass.Int32(pair_cap): + # Small mixed class: the scatter already parked + # every member as a (value bits, index) pair, + # so there is nothing to collect - go straight + # to the rank. No extra candidate walk, no + # staging barrier, nothing riding in registers. + # unroll=1 on the rank loop: the class is tiny, + # so the trip count is 1-2, but an unrolled + # body (plus its remainder ladder) is code + # volume that ONE warp walks cold while every + # other warp waits at the barrier below - the + # fetch latency has nothing to hide behind. + # Keeping it rolled trades a branch per trip + # for a body that fits a cache line or two. if warp_id == cutlass.Int32(0): ie9 = lane while ie9 < cnt_strad: - bi9 = smem_hist[ie9 + ie9] + bi9 = smem_hist[_PAIR_BASE + ie9 + ie9] ki9 = f32_order_key( cutlass.Float32( llvm.bitcast( @@ -4227,9 +4161,8 @@ def phase4_rank_scatter( ) ) ^ cutlass.Int32(-2147483648) r9 = cutlass.Int32(0) - j9 = cutlass.Int32(0) - while j9 < cnt_strad: - bj9 = smem_hist[j9 + j9] + for j9 in cutlass.range(0, cnt_strad, 1, unroll=1): + bj9 = smem_hist[_PAIR_BASE + j9 + j9] kj9 = f32_order_key( cutlass.Float32( llvm.bitcast( @@ -4241,7 +4174,6 @@ def phase4_rank_scatter( r9 = r9 + cutlass.Int32(1) elif kj9 == ki9 and j9 < ie9: r9 = r9 + cutlass.Int32(1) - j9 = j9 + cutlass.Int32(1) if r9 < need0: pos9 = rank_above_fine + r9 if pos9 < cutlass.Int32(kK): @@ -4255,159 +4187,249 @@ def phase4_rank_scatter( ) ) output_indices_row[pos9] = smem_hist[ - ie9 + ie9 + cutlass.Int32(1) + _PAIR_BASE + ie9 + ie9 + cutlass.Int32(1) ] ie9 = ie9 + cutlass.Int32(32) cute.arch.barrier() - elif fast_done == cutlass.Int32(0): - # mixed class: compact the members buffered by - # the pure-tie pass above into - # smem_keys/vals[0..cnt_strad). The buffering - # pass already read every candidate it needs, - # and the staging barrier above orders those - # reads before these writes, so the second - # full-candidate walk (and its barrier) is - # gone. Repair cost is a function of the CLASS - # size only, for any class size. - # warp-aggregated claim: intra-warp exclusive - # prefix via shfl scan + ONE atomic per warp - # (a thousand same-address claims serialize - # and scale with the class size) - pf7 = nh7 - for so3 in cutlass.range_constexpr(5): - oth3 = cute.arch.shuffle_sync_up( - pf7, cutlass.Int32(1 << so3), mask_and_clamp=0 - ) - if lane >= cutlass.Int32(1 << so3): - pf7 = pf7 + oth3 - tot7 = cute.arch.shuffle_sync(pf7, cutlass.Int32(31)) - wb7 = cutlass.Int32(0) - if lane == cutlass.Int32(31): - if tot7 > cutlass.Int32(0): - wb7 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), tot7 - ) - wb7 = cute.arch.shuffle_sync(wb7, cutlass.Int32(31)) - bs7 = wb7 + pf7 - nh7 - for sl8 in cutlass.range_constexpr(nbuf7): - if cutlass.Int32(sl8) < nh7: - smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] - smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] - cute.arch.barrier() - # Large class only (the small one is handled - # above, ahead of the pure-tie pass, so it - # never reaches here): block-parallel 4-level - # MSB radix over the compacted class (scans - # touch class pairs only; warp0 shuffle-scan - # digit search - 3 block barriers per level - # instead of 5). + else: + # Large class only. Everything below - + # the pure-tie pre-check walk, its + # cross-warp fold and the publish + # barrier that fold needs - exists to + # decide whether the radix can be + # skipped. The small route above needs + # none of it, and used to run it anyway + # for a class of two. + # block-wide pure-tie check, ANY class + # size: min/max order key over the (b*, sb*) class. + # A pure-tie class needs NO repair — the scatter's + # arrival fill of bit-equal values is already + # value-set exact. Real fp8-lineage logits tie in + # the thousands, which used to take the full radix. + # Staging mirrors the head min/max (wcnt + hist + # slots [0..31], both dead here; pairs live at + # 260+). if tidx == cutlass.Int32(0): - smem_hist[256] = cutlass.Int32(0) - smem_hist[257] = need0 - smem_hist[258] = cutlass.Int32(0) + s_iscalars[0] = cutlass.Int32(0) + nh7 = cutlass.Int32(0) + kmn6 = cutlass.Int32(2147483647) + kmx6 = cutlass.Int32(-2147483648) + it6 = tidx + # The pure-tie pre-check only exists to SKIP the + # repair when the class is bit-uniform. The small + # route below ranks by (key, arrival) and rewrites + # exactly the need0 winner slots, which is already + # value-exact on a pure tie - so for a small class + # the check is a whole candidate walk bought for + # nothing. Only the large-class route, whose radix + # really is worth avoiding, still pays for it. + while it6 < cand_count: + v6 = smem_keys[it6] + b6 = cutlass.Int32((v6 - bmin_r) * inv1) + if b6 < cutlass.Int32(0): + b6 = cutlass.Int32(0) + if b6 > cutlass.Int32(kBins - 1): + b6 = cutlass.Int32(kBins - 1) + if b6 == b_star: + s6 = cutlass.Int32((v6 - f_lo) * finv) + if s6 < cutlass.Int32(0): + s6 = cutlass.Int32(0) + if s6 > cutlass.Int32(fbins - 1): + s6 = cutlass.Int32(fbins - 1) + if s6 == sb_star: + k6 = f32_order_key(v6) ^ cutlass.Int32(-2147483648) + if k6 < kmn6: + kmn6 = k6 + if k6 > kmx6: + kmx6 = k6 + # Buffer the member so the compaction + # below needs no walk of its own. The + # array is nbuf7 = kC/num_threads + # deep and every store is an unrolled + # dynamic-index search over it, which + # is why only the large class - whose + # alternative is a 4-level radix - + # pays for it. + for sl7 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl7) == nh7: + rv7[sl7] = v6 + ri7[sl7] = smem_vals[it6] + nh7 = nh7 + cutlass.Int32(1) + it6 = it6 + cutlass.Int32(num_threads) + kmn6 = cute.arch.warp_redux_sync(kmn6, "min") + kmx6 = cute.arch.warp_redux_sync(kmx6, "max") + if lane == cutlass.Int32(0): + smem_wcnt[warp_id] = kmn6 + smem_hist[warp_id] = kmx6 cute.arch.barrier() - for lvl2 in cutlass.range_constexpr(4): - shift2 = cutlass.const_expr(24 - 8 * lvl2) - iz3 = tidx - while iz3 < cutlass.Int32(256): - smem_hist[iz3] = cutlass.Int32(0) - iz3 = iz3 + cutlass.Int32(num_threads) - cute.arch.barrier() - uthr_c2 = smem_hist[256] - ic2 = tidx - while ic2 < cnt_strad: - uk3 = f32_order_key(smem_keys[ic2]) - pm2 = cutlass.Int32(1) - if cutlass.const_expr(lvl2 > 0): - if (uk3 >> cutlass.Int32(shift2 + 8)) != ( - uthr_c2 >> cutlass.Int32(shift2 + 8) - ): - pm2 = cutlass.Int32(0) - if pm2 == cutlass.Int32(1): - dg2 = (uk3 >> cutlass.Int32(shift2)) & cutlass.Int32( - 0xFF + # lane-parallel cross-warp fold: lane w holds + # slot w and one warp reduce settles it, instead + # of every thread walking all num_warps slots of + # two arrays with dependent SMEM reads. Same + # inputs in the same order on every warp, so the + # result stays bit-identical and leaderless. + pa8 = cutlass.Int32(2147483647) + pb8 = cutlass.Int32(-2147483648) + if lane < cutlass.Int32(self.num_warps): + pa8 = smem_wcnt[lane] + pb8 = smem_hist[lane] + kmn7 = cute.arch.warp_redux_sync(pa8, "min") + kmx7 = cute.arch.warp_redux_sync(pb8, "max") + if kmn7 == kmx7: + fast_done = cutlass.Int32(1) + if fast_done == cutlass.Int32(0): + # mixed class: compact the members buffered by + # the pure-tie pass above into + # smem_keys/vals[0..cnt_strad). The buffering + # pass already read every candidate it needs, + # and the staging barrier above orders those + # reads before these writes, so the second + # full-candidate walk (and its barrier) is + # gone. Repair cost is a function of the CLASS + # size only, for any class size. + # warp-aggregated claim: intra-warp exclusive + # prefix via shfl scan + ONE atomic per warp + # (a thousand same-address claims serialize + # and scale with the class size) + pf7 = nh7 + for so3 in cutlass.range_constexpr(5): + oth3 = cute.arch.shuffle_sync_up( + pf7, cutlass.Int32(1 << so3), mask_and_clamp=0 + ) + if lane >= cutlass.Int32(1 << so3): + pf7 = pf7 + oth3 + tot7 = cute.arch.shuffle_sync(pf7, cutlass.Int32(31)) + wb7 = cutlass.Int32(0) + if lane == cutlass.Int32(31): + if tot7 > cutlass.Int32(0): + wb7 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), tot7 ) - atomicAdd(smem_hist.iterator + dg2, cutlass.Int32(1)) - ic2 = ic2 + cutlass.Int32(num_threads) + wb7 = cute.arch.shuffle_sync(wb7, cutlass.Int32(31)) + bs7 = wb7 + pf7 - nh7 + for sl8 in cutlass.range_constexpr(nbuf7): + if cutlass.Int32(sl8) < nh7: + smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] + smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] cute.arch.barrier() - if warp_id == cutlass.Int32(0): - ws3 = cutlass.Int32(0) - for jd3 in cutlass.range_constexpr(8): - di3 = ( - cutlass.Int32(255) - - lane * cutlass.Int32(8) - - cutlass.Int32(jd3) - ) - ws3 = ws3 + smem_hist[di3] - pre6 = ws3 - for so2 in cutlass.range_constexpr(5): - oth2 = cute.arch.shuffle_sync_up( - pre6, - cutlass.Int32(1 << so2), - mask_and_clamp=0, - ) - if lane >= cutlass.Int32(1 << so2): - pre6 = pre6 + oth2 - needl3 = smem_hist[257] - if pre6 >= needl3 and (pre6 - ws3) < needl3: - base5 = pre6 - ws3 - dstar2 = cutlass.Int32(0) - above5 = base5 - sd5 = cutlass.Int32(0) - for jd4 in cutlass.range_constexpr(8): - di4 = ( + # Large class only (the small one is handled + # above, ahead of the pure-tie pass, so it + # never reaches here): block-parallel 4-level + # MSB radix over the compacted class (scans + # touch class pairs only; warp0 shuffle-scan + # digit search - 3 block barriers per level + # instead of 5). + if tidx == cutlass.Int32(0): + smem_hist[256] = cutlass.Int32(0) + smem_hist[257] = need0 + smem_hist[258] = cutlass.Int32(0) + cute.arch.barrier() + for lvl2 in cutlass.range_constexpr(4): + shift2 = cutlass.const_expr(24 - 8 * lvl2) + iz3 = tidx + while iz3 < cutlass.Int32(256): + smem_hist[iz3] = cutlass.Int32(0) + iz3 = iz3 + cutlass.Int32(num_threads) + cute.arch.barrier() + uthr_c2 = smem_hist[256] + ic2 = tidx + while ic2 < cnt_strad: + uk3 = f32_order_key(smem_keys[ic2]) + pm2 = cutlass.Int32(1) + if cutlass.const_expr(lvl2 > 0): + if (uk3 >> cutlass.Int32(shift2 + 8)) != ( + uthr_c2 >> cutlass.Int32(shift2 + 8) + ): + pm2 = cutlass.Int32(0) + if pm2 == cutlass.Int32(1): + dg2 = ( + uk3 >> cutlass.Int32(shift2) + ) & cutlass.Int32(0xFF) + atomicAdd( + smem_hist.iterator + dg2, cutlass.Int32(1) + ) + ic2 = ic2 + cutlass.Int32(num_threads) + cute.arch.barrier() + if warp_id == cutlass.Int32(0): + ws3 = cutlass.Int32(0) + for jd3 in cutlass.range_constexpr(8): + di3 = ( cutlass.Int32(255) - lane * cutlass.Int32(8) - - cutlass.Int32(jd4) + - cutlass.Int32(jd3) ) - ra5 = base5 - base5 = base5 + smem_hist[di4] - if base5 >= needl3 and sd5 == cutlass.Int32(0): - dstar2 = di4 - above5 = ra5 - sd5 = cutlass.Int32(1) - smem_hist[256] = uthr_c2 | ( - dstar2 << cutlass.Int32(shift2) - ) - smem_hist[257] = needl3 - above5 - smem_hist[258] = smem_hist[258] + above5 + ws3 = ws3 + smem_hist[di3] + pre6 = ws3 + for so2 in cutlass.range_constexpr(5): + oth2 = cute.arch.shuffle_sync_up( + pre6, + cutlass.Int32(1 << so2), + mask_and_clamp=0, + ) + if lane >= cutlass.Int32(1 << so2): + pre6 = pre6 + oth2 + needl3 = smem_hist[257] + if pre6 >= needl3 and (pre6 - ws3) < needl3: + base5 = pre6 - ws3 + dstar2 = cutlass.Int32(0) + above5 = base5 + sd5 = cutlass.Int32(0) + for jd4 in cutlass.range_constexpr(8): + di4 = ( + cutlass.Int32(255) + - lane * cutlass.Int32(8) + - cutlass.Int32(jd4) + ) + ra5 = base5 + base5 = base5 + smem_hist[di4] + if base5 >= needl3 and sd5 == cutlass.Int32(0): + dstar2 = di4 + above5 = ra5 + sd5 = cutlass.Int32(1) + smem_hist[256] = uthr_c2 | ( + dstar2 << cutlass.Int32(shift2) + ) + smem_hist[257] = needl3 - above5 + smem_hist[258] = smem_hist[258] + above5 + cute.arch.barrier() + u_thr2 = smem_hist[256] + cnt_ab2 = smem_hist[258] + need_eq2 = smem_hist[257] + kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) + if tidx == cutlass.Int32(0): + s_iscalars[4] = cutlass.Int32(0) + s_iscalars[0] = cutlass.Int32(0) cute.arch.barrier() - u_thr2 = smem_hist[256] - cnt_ab2 = smem_hist[258] - need_eq2 = smem_hist[257] - kthr2 = u_thr2 ^ cutlass.Int32(-2147483648) - if tidx == cutlass.Int32(0): - s_iscalars[4] = cutlass.Int32(0) - s_iscalars[0] = cutlass.Int32(0) - cute.arch.barrier() - ir3 = tidx - while ir3 < cnt_strad: - vv3 = smem_keys[ir3] - uk4 = f32_order_key(vv3) - ks4 = uk4 ^ cutlass.Int32(-2147483648) - if ks4 > kthr2: - o4 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(4), - cutlass.Int32(1), - ) - pos = rank_above_fine + o4 - if pos < cutlass.Int32(kK): - if cutlass.const_expr(self.return_output_values): - output_values_row[pos] = self.dtype(vv3) - output_indices_row[pos] = smem_vals[ir3] - elif ks4 == kthr2: - q4 = atomicAdd( - s_iscalars.iterator + cutlass.Int32(0), - cutlass.Int32(1), - ) - if q4 < need_eq2: - pos = rank_above_fine + cnt_ab2 + q4 + ir3 = tidx + while ir3 < cnt_strad: + vv3 = smem_keys[ir3] + uk4 = f32_order_key(vv3) + ks4 = uk4 ^ cutlass.Int32(-2147483648) + if ks4 > kthr2: + o4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(4), + cutlass.Int32(1), + ) + pos = rank_above_fine + o4 if pos < cutlass.Int32(kK): if cutlass.const_expr(self.return_output_values): output_values_row[pos] = self.dtype(vv3) output_indices_row[pos] = smem_vals[ir3] - ir3 = ir3 + cutlass.Int32(num_threads) - cute.arch.barrier() + elif ks4 == kthr2: + q4 = atomicAdd( + s_iscalars.iterator + cutlass.Int32(0), + cutlass.Int32(1), + ) + if q4 < need_eq2: + pos = rank_above_fine + cnt_ab2 + q4 + if pos < cutlass.Int32(kK): + if cutlass.const_expr( + self.return_output_values + ): + output_values_row[pos] = self.dtype(vv3) + output_indices_row[pos] = smem_vals[ir3] + ir3 = ir3 + cutlass.Int32(num_threads) + cute.arch.barrier() else: need0_s = cutlass.Int32(kK) - rank_above_fine if cnt_strad > need0_s and need0_s > cutlass.Int32(0): From 7f823dea65f2aeeeccaf6fcc51e3f92c7f653a56 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Wed, 5 Aug 2026 21:57:32 -0700 Subject: [PATCH 093/117] [None][perf] GVR: rank the tail class with the block, not one warp The exact-tail repair's small-class route ranked the parked tie class on warp 0: each lane took a stride of the class and walked the WHOLE class per element, re-reading each partner out of SMEM and re-deriving its order key every comparison. Fifteen warps sat at the barrier below while that happened. Now each warp owns a stride of the class and its 32 lanes split the comparisons, so the per-lane trip count is ceil(n/num_warps) * ceil(n/32) plus one warp reduce - one trip for the class of two that fires on the captured rows, eight for a class of fifty. Same rank definition (#{key greater} + #{key equal, earlier slot}), so the selection is unchanged. Phase 4 on the pro capture (30 layers, cold steps, device clock, us/step): unit before after pro 8k 3.15 2.99 pro 128k 3.22 3.10 pro 512k 3.88 3.56 Exactness: 30/30 layers on the firing capture; 1947/1947 records over flash 8k/64k/256k/1024k and pro 1024k on both assist tiers; the full decode test file passes (708 passed, 144 skipped, 1 xfailed). This also removes the reason the fine histogram level could not be dropped: with the old warp-0 rank, collapsing the sub-binning and handing the whole coarse class to the repair cost more in the tail than it saved in the fine level (+0.59 against -0.28 measured). With the block rank the two now net out even, so the fine level stays for another reason - the coarse class it avoids ranking is 11 at the median and 49 at the max over 520 captured steps - but the ranking is no longer what decides it. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 07882ed648b6..f6ff8b974ee4 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -4141,55 +4141,58 @@ def phase4_rank_scatter( # so there is nothing to collect - go straight # to the rank. No extra candidate walk, no # staging barrier, nothing riding in registers. - # unroll=1 on the rank loop: the class is tiny, - # so the trip count is 1-2, but an unrolled - # body (plus its remainder ladder) is code - # volume that ONE warp walks cold while every - # other warp waits at the barrier below - the - # fetch latency has nothing to hide behind. - # Keeping it rolled trades a branch per trip - # for a body that fits a cache line or two. - if warp_id == cutlass.Int32(0): - ie9 = lane - while ie9 < cnt_strad: - bi9 = smem_hist[_PAIR_BASE + ie9 + ie9] - ki9 = f32_order_key( + # Rank the class with the WHOLE BLOCK, not + # one warp. Each warp owns a stride of the + # class and its 32 lanes split the + # comparisons, so the cost is + # ceil(n/num_warps) * ceil(n/32) trips per + # lane plus one warp reduce - 1 trip for the + # class of two that fires today, 8 for a class + # of fifty. The old form had a single warp walk + # n^2/32 steps while the other fifteen waited + # at the barrier below, which is what made a + # larger class unaffordable. + e9 = warp_id + while e9 < cnt_strad: + be9 = smem_hist[_PAIR_BASE + e9 + e9] + ke9 = f32_order_key( + cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, be9.ir_value()) + ) + ) ^ cutlass.Int32(-2147483648) + c9 = cutlass.Int32(0) + j9 = lane + while j9 < cnt_strad: + bj9 = smem_hist[_PAIR_BASE + j9 + j9] + kj9 = f32_order_key( cutlass.Float32( llvm.bitcast( - cutlass.Float32.mlir_type, bi9.ir_value() + cutlass.Float32.mlir_type, bj9.ir_value() ) ) ) ^ cutlass.Int32(-2147483648) - r9 = cutlass.Int32(0) - for j9 in cutlass.range(0, cnt_strad, 1, unroll=1): - bj9 = smem_hist[_PAIR_BASE + j9 + j9] - kj9 = f32_order_key( - cutlass.Float32( - llvm.bitcast( - cutlass.Float32.mlir_type, bj9.ir_value() - ) - ) - ) ^ cutlass.Int32(-2147483648) - if kj9 > ki9: - r9 = r9 + cutlass.Int32(1) - elif kj9 == ki9 and j9 < ie9: - r9 = r9 + cutlass.Int32(1) - if r9 < need0: - pos9 = rank_above_fine + r9 - if pos9 < cutlass.Int32(kK): - if cutlass.const_expr(self.return_output_values): - output_values_row[pos9] = self.dtype( - cutlass.Float32( - llvm.bitcast( - cutlass.Float32.mlir_type, - bi9.ir_value(), - ) + if kj9 > ke9: + c9 = c9 + cutlass.Int32(1) + elif kj9 == ke9 and j9 < e9: + c9 = c9 + cutlass.Int32(1) + j9 = j9 + cutlass.Int32(32) + r9 = self.warp_reduce_sum_i32(c9) + if lane == cutlass.Int32(0) and r9 < need0: + pos9 = rank_above_fine + r9 + if pos9 < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[pos9] = self.dtype( + cutlass.Float32( + llvm.bitcast( + cutlass.Float32.mlir_type, + be9.ir_value(), ) ) - output_indices_row[pos9] = smem_hist[ - _PAIR_BASE + ie9 + ie9 + cutlass.Int32(1) - ] - ie9 = ie9 + cutlass.Int32(32) + ) + output_indices_row[pos9] = smem_hist[ + _PAIR_BASE + e9 + e9 + cutlass.Int32(1) + ] + e9 = e9 + cutlass.Int32(num_warps) cute.arch.barrier() else: # Large class only. Everything below - From d17a58ac35c9014acba9ff7eddae8f9b1b5f9604 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 8 Aug 2026 09:13:20 -0700 Subject: [PATCH 094/117] [None][perf] GVR routing: the weak band no longer holds at n_comp 65536 The weak band hands a mid-length row back to the stock kernel on the premise that stock's split grid wins there outright. At the top of the band that premise is now false. Measured on the full grid (kernel-only, us/step, 176 cells x 61677 steps per arm): cell stock counts rungs list flash 256k B8 14.03 11.84 14.67 5.85 pro 256k B8 15.48 13.10 16.01 7.44 Stock is not the fastest arm there - counts beats it by 2.19 and 2.38. The list arm is faster still, but it does not pay for itself at this batch: re-measured on the indexer kernel with the tight lines parked, the list emission costs +10.75us at batch 8 / ctx 256k against an 8.11us kernel saving, so LIST_EMIT_MAX_B stays at 4. The counts emission at the same point costs +0.27us (+0.27 at B4 is 3.02, at B16 it is free) - the table this band was fitted against charged +2.0us at batch 8, before the parking made the emission 2.7-5x cheaper. Net +1.9 to +2.1us per step. Replaying the routed grid over the same measurements: cell before after flash 256k B8 1.119 1.327 (+19%) pro 256k B8 1.112 1.314 (+18%) whole grid 1.702 1.704 Only the top of the band moves. The interior (49152 <= n_comp < 65536) is untouched - no capture unit lands there, so it keeps the stock kernel until someone measures it rather than inheriting a premise that has now been shown to expire. The full decode test file passes (708 passed, 144 skipped, 1 xfailed). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/blackwell/top_k/gvr_routing.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index bf7f72eb64b2..ed081c5e7c84 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -104,8 +104,17 @@ # 1.5us there, which is enough to take the upper half of the band back # off the stock kernel (2 cells fall through now, down from 4). ASSIST_WEAK_MIN_N = 49152 -ASSIST_WEAK_MAX_N_SMALL_K = 98304 # k <= ASSIST_WEAK_K -ASSIST_WEAK_MAX_N_LARGE_K = 98304 +# Narrowed again from 98304 to 65536: at n_comp 65536 the band's premise no +# longer holds. Measured on the full grid (kernel-only, us/step), the stock +# kernel is NOT the fastest arm there - counts beats it by 2.19 (flash) and +# 2.38 (pro), and the counts emission at that batch costs only +0.27us on the +# indexer (re-measured after the tight-line parking; the old table charged +# +2.0us at batch 8, which is what kept the band this wide). Net +1.9 to +# +2.1us per step, i.e. those cells go 1.11 -> ~1.31 against the baseline. +# The interior of the band (49152 <= n_comp < 65536) is untouched: no grid +# unit lands there, so it stays on the stock kernel until someone measures it. +ASSIST_WEAK_MAX_N_SMALL_K = 65536 # k <= ASSIST_WEAK_K +ASSIST_WEAK_MAX_N_LARGE_K = 65536 ASSIST_WEAK_K = 512 ASSIST_WEAK_MAX_B = 8 From 9466f59150d00768b705605752feaf7ce7964af5 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 8 Aug 2026 10:22:05 -0700 Subject: [PATCH 095/117] [None][perf] GVR: drop phase 4's fine level, at compile time Phase 4's cost is a chain of sequential stages, not work: its total barely moves when the candidate count triples (575 -> 2048 cost 0.2us) and it is flat from n_comp 2051 to 131072. So the only way to cut it is to remove a stage, and the fine 256-bin level is the one that can go: the straddling COARSE bin holds 11 candidates at the median and 49 at the max over 520 captured steps, which the tail repair now absorbs directly since it ranks with the whole block. Collapsing the sub-binning (finv 0 sends every member of the coarse class to sub-bin 0 == sb*) makes the scatter park the whole class and the tail rank it. A class past the pair buffer still falls into the tail's radix, which handles any size. The removal has to be at COMPILE time. Gating the same three pieces behind a runtime branch was measured twice and is a wash - the fine window only fell 0.89 -> 0.64us because the branch itself costs the stage. Compiled out, it falls to 0.10us: phase 4 window before after min/max 0.22 0.21 histogram 0.44 0.43 coarse search 0.51 0.51 fine level 0.89 0.10 scatter 0.62 0.61 tail repair 0.37 0.70 phase 4 total 2.99 2.66 Across shapes (device clock, us/step; kernel total in brackets): pro 8k 2.99 -> 2.66 [6.32 -> 6.29] pro 128k 3.10 -> 2.85 [11.49 -> 10.41] pro 512k 3.56 -> 3.19 [22.26 -> 21.69] flash 256k -> 2.74 [ -> 14.40] Gated to the configuration that owns the small-class tail route, so the stock kernel's codegen is untouched; p4_no_fine=False restores the old body for A/B. Exactness: 30/30 layers on four capture units; 1947/1947 records over flash 8k/64k/256k/1024k and pro 1024k on both assist tiers; the full decode test file passes (708 passed, 144 skipped, 1 xfailed). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 140 +++++++++++------- 1 file changed, 85 insertions(+), 55 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index f6ff8b974ee4..83090f5fba2a 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -318,6 +318,7 @@ def __init__( p4_exact_tail: Optional[bool] = None, p4_tail_fast: Optional[bool] = None, # [p4tt] p4_tail_v3: Optional[bool] = None, + p4_no_fine: Optional[bool] = None, p1r_rescue: bool = True, num_bins: Optional[int] = None, p4_warp_redundant: bool = True, @@ -789,6 +790,22 @@ def __init__( use_ext_counts or use_ext_cand or ext_rungs or self_scan or enable_block_skip ) self.p4_tail_v3 = bool(p4_tail_v3) + # p4_no_fine: drop the 256-bin fine level from phase 4 outright and + # let the tail repair rank the whole straddling COARSE bin instead. + # Phase 4's cost is a chain of sequential stages, not work: its total + # barely moves when the candidate count triples, so the only way to + # cut it is to remove a stage. The coarse bin holds 11 candidates at + # the median and 49 at the max over 520 captured steps - inside the + # tail's pair buffer - and the tail now ranks with the whole block, + # so it absorbs the class cheaply. A class past the buffer falls into + # the tail's radix, which handles any size. Gated to the same + # configuration as that small-class route; the stock kernel keeps the + # fine level so its codegen is untouched. + if p4_no_fine is None: + p4_no_fine = bool( + self.p4_exact_tail and self.p4_tail_fast and self.p4_tail_v3 + ) and not (self.p4_fine_rangetest or self.p4_scat_rangetest) + self.p4_no_fine = bool(p4_no_fine) # p1r_rescue: rebuild the refine bracket from the row when the # seed bracket is degenerate. ON by default - upstream's identity # shortcut is wrong on real data (every request's first decode @@ -3934,6 +3951,18 @@ def phase4_rank_scatter( # bin b* value range under the inv1 binning: [f_lo, f_lo + 1/inv1) f_lo = bmin_r + cutlass.Float32(b_star) / inv1 finv = (cutlass.Float32(fbins - 1) + cutlass.Float32(0.99)) * inv1 + if cutlass.const_expr(self.p4_no_fine): + # Sub-binning collapsed: finv 0 sends every member of the + # straddling coarse bin to sub-bin 0 == sb*, so the + # scatter parks the whole coarse class and the tail ranks + # it. The re-zero, the build and the search below are not + # traced at all - this is a compile-time removal, not a + # runtime branch, because a runtime branch still costs the + # stage (measured: 0.64us for a fine window whose work was + # gated off). + finv = cutlass.Float32(0.0) + sb_star = cutlass.Int32(0) + rank_above_fine = rank_above # bin b* spans [f_lo, f_hi); the clamped ends fold # out-of-range values into bin 0 and bin kBins-1, so those # two drop the matching side. Only the range-test arms read @@ -3942,61 +3971,62 @@ def phase4_rank_scatter( f_hi = f_lo + cutlass.Float32(1.0) / inv1 lo_edge = b_star == cutlass.Int32(0) hi_edge = b_star == cutlass.Int32(kBins - 1) - # re-zero (only fbins slots) + build fine sub-hist of bin-b* cands - iz = tidx - while iz < cutlass.Int32(fbins): - smem_hist[iz] = cutlass.Int32(0) - iz = iz + cutlass.Int32(num_threads) - cute.arch.barrier() - if cutlass.const_expr(self.p4_fine_rangetest): - # A candidate belongs to bin b* exactly when its value - # lies in [f_lo, f_hi), so the filter is two compares - - # the bin recompute (subtract + multiply + two clamps) - # per candidate is redundant work. The clamped ends of - # the binning fold out-of-range values INTO bin 0 and - # bin kBins-1, so those two bins drop the matching side - # of the range test to stay bit-identical. - ifb = tidx - while ifb < cand_count: - vf = smem_keys[ifb] - inb = vf >= f_lo and vf < f_hi - if lo_edge: - inb = vf < f_hi - if hi_edge: - inb = vf >= f_lo - if inb: - sb = cutlass.Int32((vf - f_lo) * finv) - if sb < cutlass.Int32(0): - sb = cutlass.Int32(0) - if sb > cutlass.Int32(fbins - 1): - sb = cutlass.Int32(fbins - 1) - atomicAdd(smem_hist.iterator + sb, cutlass.Int32(1)) - ifb = ifb + cutlass.Int32(num_threads) - else: - ifb = tidx - while ifb < cand_count: - vfo = smem_keys[ifb] - cbo = cutlass.Int32((vfo - bmin_r) * inv1) - if cbo < cutlass.Int32(0): - cbo = cutlass.Int32(0) - if cbo > cutlass.Int32(kBins - 1): - cbo = cutlass.Int32(kBins - 1) - if cbo == b_star: - sbo = cutlass.Int32((vfo - f_lo) * finv) - if sbo < cutlass.Int32(0): - sbo = cutlass.Int32(0) - if sbo > cutlass.Int32(fbins - 1): - sbo = cutlass.Int32(fbins - 1) - atomicAdd(smem_hist.iterator + sbo, cutlass.Int32(1)) - ifb = ifb + cutlass.Int32(num_threads) - cute.arch.barrier() - # fine sub-bin search, resolved lane-parallel on every - # warp (see _p4_fine_rw): the two publish barriers and the - # fbins/num_warps-deep single-lane walk that used to sit - # here are gone, and the answer comes back in registers. - sb_star, rank_above_fine = self._p4_fine_rw( - smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane - ) + if cutlass.const_expr(not self.p4_no_fine): + # re-zero (only fbins slots) + build fine sub-hist of bin-b* cands + iz = tidx + while iz < cutlass.Int32(fbins): + smem_hist[iz] = cutlass.Int32(0) + iz = iz + cutlass.Int32(num_threads) + cute.arch.barrier() + if cutlass.const_expr(self.p4_fine_rangetest): + # A candidate belongs to bin b* exactly when its value + # lies in [f_lo, f_hi), so the filter is two compares - + # the bin recompute (subtract + multiply + two clamps) + # per candidate is redundant work. The clamped ends of + # the binning fold out-of-range values INTO bin 0 and + # bin kBins-1, so those two bins drop the matching side + # of the range test to stay bit-identical. + ifb = tidx + while ifb < cand_count: + vf = smem_keys[ifb] + inb = vf >= f_lo and vf < f_hi + if lo_edge: + inb = vf < f_hi + if hi_edge: + inb = vf >= f_lo + if inb: + sb = cutlass.Int32((vf - f_lo) * finv) + if sb < cutlass.Int32(0): + sb = cutlass.Int32(0) + if sb > cutlass.Int32(fbins - 1): + sb = cutlass.Int32(fbins - 1) + atomicAdd(smem_hist.iterator + sb, cutlass.Int32(1)) + ifb = ifb + cutlass.Int32(num_threads) + else: + ifb = tidx + while ifb < cand_count: + vfo = smem_keys[ifb] + cbo = cutlass.Int32((vfo - bmin_r) * inv1) + if cbo < cutlass.Int32(0): + cbo = cutlass.Int32(0) + if cbo > cutlass.Int32(kBins - 1): + cbo = cutlass.Int32(kBins - 1) + if cbo == b_star: + sbo = cutlass.Int32((vfo - f_lo) * finv) + if sbo < cutlass.Int32(0): + sbo = cutlass.Int32(0) + if sbo > cutlass.Int32(fbins - 1): + sbo = cutlass.Int32(fbins - 1) + atomicAdd(smem_hist.iterator + sbo, cutlass.Int32(1)) + ifb = ifb + cutlass.Int32(num_threads) + cute.arch.barrier() + # fine sub-bin search, resolved lane-parallel on every + # warp (see _p4_fine_rw): the two publish barriers and the + # fbins/num_warps-deep single-lane walk that used to sit + # here are gone, and the answer comes back in registers. + sb_star, rank_above_fine = self._p4_fine_rw( + smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane + ) if tidx == cutlass.Int32(0): s_iscalars[4] = cutlass.Int32(0) # cnt_above s_iscalars[0] = cutlass.Int32(0) # cnt_mid (b*, sub>sb*) From 53836075066be258b60fff1195ffc0f64e809688 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sat, 8 Aug 2026 11:31:56 -0700 Subject: [PATCH 096/117] [None][perf] GVR: collect the dead weight the fine level left behind With the fine level compiled out, three things it fed are dead but were still being computed every step: * the scatter's sub-bin recompute - a subtract, a multiply and two clamps per candidate that lands in the straddling bin, whose result is a constant 0 == sb* once the sub-binning is collapsed; * f_lo, whose only consumer is that recompute, and which costs an fp32 divide; * the min side of phase 4's range scan. The accepted threshold is an EXACT lower bound - every candidate got there by comparing >= against it - so the scan only has to find the max. That drops half the walk, one warp reduce and one arm of the cross-warp fold. The window it widens is at the BOTTOM, and the k-th lives at the top. Phase 4 on the pro 8k capture (device clock, us/step): window fine kept fine dropped + this min/max 0.22 0.21 0.15 histogram 0.44 0.43 0.33 coarse search 0.51 0.51 0.52 fine level 0.89 0.10 0.04 scatter 0.62 0.61 0.60 tail repair 0.37 0.70 0.65 total 2.99 2.66 2.38 Same node, same binary, p4_no_fine off vs on (device clock, us/step): unit fine kept dropped pro 8k 3.05 2.38 pro 128k 3.14 3.23 pro 512k 3.58 3.31 flash 256k 3.11 2.75 pro 128k is the one shape that does not want it: its collection line sits at rank 2048 rather than 1536, and with the sub-binning collapsed every member of the straddling class takes the same atomic claim, so the scatter goes 0.68 -> 1.01 and the tail 0.34 -> 0.73 there. The other three shapes pay neither. Averaged over the four it is -0.31us/step; the knob (p4_no_fine) is there if a shape-aware default turns out to be worth it. Exactness: 30/30 layers on four capture units; 2277/2277 records over flash 8k/64k/256k and pro 1024k on both assist tiers; the full decode test file passes (708 passed, 144 skipped, 1 xfailed). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 56 ++++++++++++++----- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 4 ++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 83090f5fba2a..766d8a272a97 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -3820,18 +3820,31 @@ def phase4_rank_scatter( bmax_r = bmin_r + cutlass.Float32(1e-6) if run_stock_range: # ---- block min/max over candidates ---- + # The accepted threshold is an EXACT lower bound: every + # candidate got here by comparing >= against it. So the min + # side of this scan buys nothing but a slightly tighter + # window - and the window only has to be tight where the + # k-th lives, which is the TOP of it. Taking the threshold + # instead drops half the scan, one warp reduce and one arm + # of the cross-warp fold. Only the assist tiers have a + # published threshold, hence the gate. + use_thr_min = cutlass.const_expr(self.p4_no_fine) local_cmin = cutlass.Float32(self.FLT_MAX) local_cmax = cutlass.Float32(self.NEG_FLT_MAX) i5 = tidx while i5 < cand_count: v = smem_keys[i5] - local_cmin = _fmin_f32_inline(local_cmin, v) + if cutlass.const_expr(not use_thr_min): + local_cmin = _fmin_f32_inline(local_cmin, v) local_cmax = cute.arch.fmax(local_cmax, v) i5 = i5 + cutlass.Int32(num_threads) - cmin = self.warp_reduce_min_f32(local_cmin) + cmin = cutlass.Float32(0.0) + if cutlass.const_expr(not use_thr_min): + cmin = self.warp_reduce_min_f32(local_cmin) cmax = self.warp_reduce_max_f32(local_cmax) if lane == cutlass.Int32(0): - smem_wcnt[warp_id] = float_as_uint32(cmin) + if cutlass.const_expr(not use_thr_min): + smem_wcnt[warp_id] = float_as_uint32(cmin) smem_hist[warp_id] = float_as_uint32(cmax) cute.arch.barrier() # lane-parallel cross-warp fold: lane w holds slot w and one @@ -3842,13 +3855,17 @@ def phase4_rank_scatter( pmn = cutlass.Float32(self.FLT_MAX) pmx = cutlass.Float32(self.NEG_FLT_MAX) if lane < cutlass.Int32(self.num_warps): - pmn = cutlass.Float32( - llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[lane].ir_value()) - ) + if cutlass.const_expr(not use_thr_min): + pmn = cutlass.Float32( + llvm.bitcast(cutlass.Float32.mlir_type, smem_wcnt[lane].ir_value()) + ) pmx = cutlass.Float32( llvm.bitcast(cutlass.Float32.mlir_type, smem_hist[lane].ir_value()) ) - bmin_r = _fmin_f32_inline(bmin_r, self.warp_reduce_min_f32(pmn)) + if cutlass.const_expr(use_thr_min): + bmin_r = s_thr[0] + else: + bmin_r = _fmin_f32_inline(bmin_r, self.warp_reduce_min_f32(pmn)) bmax_r = cute.arch.fmax(bmax_r, self.warp_reduce_max_f32(pmx)) if bmax_r <= bmin_r: bmax_r = bmin_r + cutlass.Float32(1e-6) @@ -3949,8 +3966,11 @@ def phase4_rank_scatter( # enough to resolve the straddling bin to ≤1 distinct value. fbins = cutlass.const_expr(256) # bin b* value range under the inv1 binning: [f_lo, f_lo + 1/inv1) - f_lo = bmin_r + cutlass.Float32(b_star) / inv1 - finv = (cutlass.Float32(fbins - 1) + cutlass.Float32(0.99)) * inv1 + f_lo = cutlass.Float32(0.0) + finv = cutlass.Float32(0.0) + if cutlass.const_expr(not self.p4_no_fine): + f_lo = bmin_r + cutlass.Float32(b_star) / inv1 + finv = (cutlass.Float32(fbins - 1) + cutlass.Float32(0.99)) * inv1 if cutlass.const_expr(self.p4_no_fine): # Sub-binning collapsed: finv 0 sends every member of the # straddling coarse bin to sub-bin 0 == sb*, so the @@ -4070,11 +4090,19 @@ def phase4_rank_scatter( output_values_row[pos] = self.dtype(v) output_indices_row[pos] = smem_vals[isc] elif bin_i == b_star: - sb = cutlass.Int32((v - f_lo) * finv) - if sb < cutlass.Int32(0): - sb = cutlass.Int32(0) - if sb > cutlass.Int32(fbins - 1): - sb = cutlass.Int32(fbins - 1) + # With the fine level compiled out the sub-bin is a + # constant 0 == sb*, so the whole three-way split + # below collapses to the park arm and the per- + # candidate recompute (a subtract, a multiply and two + # clamps) is dead. Keep it a compile-time constant so + # the scatter does not carry it. + sb = cutlass.Int32(0) + if cutlass.const_expr(not self.p4_no_fine): + sb = cutlass.Int32((v - f_lo) * finv) + if sb < cutlass.Int32(0): + sb = cutlass.Int32(0) + if sb > cutlass.Int32(fbins - 1): + sb = cutlass.Int32(fbins - 1) if sb > sb_star: o = atomicAdd(s_iscalars.iterator + cutlass.Int32(0), cutlass.Int32(1)) pos = rank_above + o diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 81b621279fec..45891a0f3a05 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -68,6 +68,7 @@ def _compile( enable_block_skip: bool = False, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, + p4_no_fine: "bool | None" = None, p4_exact_tail: "bool | None" = None, p4_tail_fast: "bool | None" = None, p1r_rescue: bool = True, @@ -213,6 +214,7 @@ def _compile( enable_block_skip=enable_block_skip, pdl_wait_late=pdl_wait_late, p4_tail_v3=p4_tail_v3, + p4_no_fine=p4_no_fine, p4_exact_tail=p4_exact_tail, p4_tail_fast=p4_tail_fast, p1r_rescue=p1r_rescue, @@ -682,6 +684,7 @@ def gvr_topk_decode( p2_warp_redundant: bool = True, pdl_wait_late: bool = True, p4_tail_v3: "bool | None" = None, + p4_no_fine: "bool | None" = None, p4_exact_tail: "bool | None" = None, p4_tail_fast: "bool | None" = None, p1r_rescue: bool = True, @@ -976,6 +979,7 @@ def gvr_topk_decode( enable_block_skip, pdl_wait_late, p4_tail_v3, + p4_no_fine, p4_exact_tail, p4_tail_fast, p1r_rescue, From 1bd86c026e4fe61f59a1636d74f958aeaa20de3d Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:58:22 -0700 Subject: [PATCH 097/117] [None][test] give the GVR decode unittest entry 120 minutes on B300 The whole test file runs as ONE integration item (test_unittests_v2[unittest/...test_cute_dsl_gvr_topk_decode.py]), and an item without a TIMEOUT directive gets the harness default of 3600s with --timeout-method=thread - which kills the whole pytest process on breach. The file compiles dozens of CuTe DSL kernel variants and takes ~43 minutes on a dedicated B300; on the flex CI runners (fewer CPUs, so slower kernel compilation) it crosses the hour. That is exactly the observed failure shape, three CI runs in a row: precisely TWO "Test terminated unexpectedly" results - the file-level item plus whichever inner case the clock happened to land on (p4_exact_tail_ties[...512-16384-1] on 08/03, launch_autoconfig[dtype2-1024-131072-2] on 08/06, launch_autoconfig[dtype0-2048-32768-1] on 08/09) - with no assertion failures, and the stage total at 84 minutes: ~24 minutes for the other entries plus the kill at the 3600s mark. The same file passes 708/144/1 in ~43 minutes on a dedicated B300 node, three times across different commits. TIMEOUT (120) follows the existing precedent in the same list (thop/parallel TIMEOUT (90)). B200 lists do not carry this entry, which is why only B300 stages ever showed the failure. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- tests/integration/test_lists/test-db/l0_b300.yml | 2 +- tests/integration/test_lists/test-db/l0_dgx_b300.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_lists/test-db/l0_b300.yml b/tests/integration/test_lists/test-db/l0_b300.yml index 95938a31cd98..c7b935df83fc 100644 --- a/tests/integration/test_lists/test-db/l0_b300.yml +++ b/tests/integration/test_lists/test-db/l0_b300.yml @@ -19,7 +19,7 @@ l0_b300: - unittest/_torch/attention --ignore=unittest/_torch/attention/sparse/test_cute_dsl_fp8_paged_mqa_logits.py --ignore=unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py --ignore=unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py - unittest/_torch/attention/sparse/test_cute_dsl_fp8_paged_mqa_logits.py - unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py - - unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py + - unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py TIMEOUT (120) - unittest/_torch/thop/parallel TIMEOUT (90) - unittest/_torch/thop/serial - unittest/_torch/executor # 250s diff --git a/tests/integration/test_lists/test-db/l0_dgx_b300.yml b/tests/integration/test_lists/test-db/l0_dgx_b300.yml index 736319d8409f..0dac8a4fd41c 100644 --- a/tests/integration/test_lists/test-db/l0_dgx_b300.yml +++ b/tests/integration/test_lists/test-db/l0_dgx_b300.yml @@ -18,7 +18,7 @@ l0_dgx_b300: - unittest/_torch/attention --ignore=unittest/_torch/attention/sparse/test_cute_dsl_fp8_paged_mqa_logits.py --ignore=unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py --ignore=unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py - unittest/_torch/attention/sparse/test_cute_dsl_fp8_paged_mqa_logits.py - unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py - - unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py + - unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py TIMEOUT (120) - unittest/_torch/executor # ------------- modules (multi-GPU) --------------- - unittest/_torch/modules/test_mla_helix.py From fda4f39ac5885982ccf3cc927a440e445d1b7faf Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 9 Aug 2026 10:48:51 -0700 Subject: [PATCH 098/117] [None][fix] correct GVR ext list-tier consumption and gate its state The production list tier (TRTLLM_GVR_EXT=1, batch <= 4, n_comp >= 64k) was silently broken end to end; the new ext-tier unit tests run the emitter contract verbatim and caught four defects: - the consumer kwargs never passed accept_cap, so the kernel derived segment bases from kC while the emitter wrote at LIST_SEG_A bases - the kernel clamped segment bases/extents by min(accept_cap, kC); the emitter geometry is accept_cap (K512's kC=3072 diet read bases 0/3072/6144 against writes at 0/8192/16384). Only the admission bound keeps the kC clamp - the Phase-1 skip preview admitted parked seed rows (t1/t2 = 1e30); with no in-band count the raw lines reached the threshold scratch and the defensive rerun spun on the poisoned bracket, emitting -1 for the whole row. The preview now requires real lines; parked rows keep Phase 1 and the list take is unaffected - the clamped-histogram fallback bracketed segment C by a parked upper line; it now reuses the need_max segment-max pass instead Also route on the tier emitted THIS step (emission and consumption happen inside the same forward; the routes-on-previous-tier member is gone), skip the ~5GB candidate buffers when the engine's static length cannot reach the list tier, skip seed-row maintenance on rungs steps, and drop the TRTLLM_GVR_DUMP diagnostic block from the decode hot path. Why nothing caught this before: the harness always ran three live lines with B* <= kC, and e2e at batch 128 never fires the list tier (LIST_EMIT_MAX_B=4). New tests: packed seed row (K 512/1024/2048 x band/fat/miss/inf), candidate list (hit/pads/hist/void/bucketed at production geometry), 32-grain block-max records (exact/pad_inf). Full decode suite on B200: 727 passed / 144 skipped / 1 xfailed. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../attention_backend/sparse/dsa/indexer.py | 83 +--- .../attention_backend/sparse/gvr_ext.py | 87 ++-- .../blackwell/top_k/gvr_topk_decode.py | 453 +++++++----------- .../sparse/test_cute_dsl_gvr_topk_decode.py | 223 +++++++++ 4 files changed, 458 insertions(+), 388 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py index 340bf32927d3..f3e0c32ca9e3 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py @@ -683,10 +683,8 @@ def __init__( self.use_cute_dsl_paged_mqa_logits = ( sparse_params.use_cute_dsl_paged_mqa_logits and IS_CUTLASS_DSL_AVAILABLE ) - # GVR emission-assisted decode (opt-in, experimental): the FP4 - # indexer epilogue emits seed counts / the bucketed candidate - # list and the top-k consumes them (see gvr_ext / gvr_routing). - # Env-gated so the default path stays byte-identical. + # GVR emission-assisted decode (opt-in, experimental): the FP4 indexer + # epilogue emits candidates for the top-k (see gvr_ext / gvr_routing). self.use_gvr_ext = ( os.environ.get("TRTLLM_GVR_EXT", "0") == "1" and self.use_cute_dsl_topk @@ -1716,31 +1714,32 @@ def sparse_attn_indexer( gvr_emit_kwargs = {} if self.use_gvr_ext and next_n == 1 and not dsl_atom_split: - from ..gvr_ext import GvrExtState + from ..gvr_ext import LIST_EMIT_MIN_N, GvrExtState + # indexer_max_seq_len is already the compressed length + # (get_indexer_max_seq_len divides); do not divide again. + n_comp = indexer_max_seq_len if self._gvr_ext is None: self._gvr_ext = GvrExtState( max_rows=metadata.max_num_sequences, top_k=self.index_topk, device=q_fp8.device, + # the list tier is only reachable at + # n_comp >= LIST_EMIT_MIN_N; skip its large + # candidate buffers when the engine's static + # length cannot get there + enable_list_tier=n_comp >= LIST_EMIT_MIN_N, ) st = self._gvr_ext - # indexer_max_seq_len is ALREADY the compressed - # length - get_indexer_max_seq_len divides by the - # compress ratio, and it is what goes to the top-k - # as max_seq_len below. Dividing again shifted every - # routing threshold by two doublings: the list tier - # became unreachable and 30 of the 154 grid shapes - # fell back to the stock kernel instead of 4. - n_comp = indexer_max_seq_len emit_tier, self._gvr_route = st.plan( batch_size, n_comp, torch.cuda.get_device_properties(q_fp8.device).multi_processor_count, compress_ratio=max(self.compress_ratio, 1), ) - st.update_seed_rows(batch_size, emit_tier) - gvr_emit_kwargs = st.indexer_emit_kwargs(emit_tier, batch_size) + if emit_tier in ("counts", "list"): + st.update_seed_rows(batch_size, emit_tier) + gvr_emit_kwargs = st.indexer_emit_kwargs(emit_tier, batch_size) if self._gvr_route.attach_block_max or emit_tier in ( "counts", "list", @@ -1827,11 +1826,8 @@ def sparse_attn_indexer( if not metadata.use_cute_dsl_topk: heuristic_scratch = metadata.heuristic_scratch_values[:num_gen_tokens] - # tier "none": too short for any assist to pay, or inside - # the mid-row band where the stock kernel's split grid wins - # (see gvr_routing) - fall through to the stock branch. The - # untouched ext state reads as cold start, which its closed - # loop already handles. + # tier "none" (see gvr_routing) falls through to the stock + # branch; the untouched ext state reads as a cold start there. if ( self.use_gvr_ext and self._gvr_ext is not None @@ -1840,16 +1836,13 @@ def sparse_attn_indexer( and next_n == 1 and num_gen_tokens <= 256 ): - # emission-assisted GVR: consume what the indexer - # epilogue emitted this step (packed seed row / - # bucketed list / block_max per the picked route) + # emission-assisted GVR: consume what the indexer epilogue + # emitted this step (seed row / list / block_max per route) st = self._gvr_ext out_slice = topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] - # the GVR op takes RAW-domain seq_lens (its kernel - # ceil-divides by compress_ratio internally). On the - # DSL indexer path the live compressed lengths are in - # gen_indexer_kv_lens_cuda_runtime (kv_lens_cuda_2d - # stays zero there - same trap as the indexer call). + # GVR op takes RAW-domain seq_lens (kernel ceil-divides by + # compress_ratio); on the DSL path the live compressed lens + # are gen_indexer_kv_lens_cuda_runtime, kv_lens_cuda_2d is 0. if self.compress_ratio > 1: gvr_lens = metadata.gen_indexer_kv_lens_cuda_runtime assert gvr_lens is not None @@ -1872,34 +1865,6 @@ def sparse_attn_indexer( max_seq_len=indexer_max_seq_len, **ext_kw, ) - # diagnostic dump (NO_GRAPH runs only): capture the - # inputs AND the in-run selection so an offline pass - # can check score-multiset equality vs torch.topk on - # the exact production path. prev_topk still holds - # the pre-op value here (copy-back is below). The - # min-seq guard skips warmup/dummy rows (n < K). - # TRTLLM_GVR_DUMP holds the target directory. - gvr_dump_dir = os.environ.get("TRTLLM_GVR_DUMP") - if ( - gvr_dump_dir - and getattr(self, "_gvr_dumped", 0) < 6 - and int(seq_1d.min()) >= self.index_topk * self.compress_ratio - ): - os.makedirs(gvr_dump_dir, exist_ok=True) - self._gvr_dumped = getattr(self, "_gvr_dumped", 0) + 1 - torch.save( - { - "logits": logits_decode.clone().cpu(), - "seq_raw": seq_1d.clone().cpu(), - "pre": st.prev_topk[:num_gen_tokens].clone().cpu(), - "sel": out_slice.clone().cpu(), - "layer": self.layer_idx, - }, - os.path.join( - gvr_dump_dir, - f"dump_l{self.layer_idx}_{self._gvr_dumped}.pt", - ), - ) st.prev_topk[:num_gen_tokens].copy_(out_slice) elif self.use_cute_dsl_topk and self._enable_heuristic_topk: torch.ops.trtllm.cute_dsl_gvr_topk_decode( @@ -1914,10 +1879,8 @@ def sparse_attn_indexer( order_row=metadata.kv_lens_row_reorder, ) elif self.use_cute_dsl_topk and (self.compress_ratio == 1 or next_n == 1): - # request-level seq_lens must be 1-D; on the DSL - # indexer path the live compressed lengths are in - # gen_indexer_kv_lens_cuda_runtime (kv_lens_cuda_2d - # stays zero there) + # seq_lens must be 1-D; on the DSL path the live compressed + # lens are gen_indexer_kv_lens_cuda_runtime (2d buf is zero) if self.compress_ratio > 1: radix_lens = ( metadata.gen_indexer_kv_lens_cuda_runtime diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py index 525025eb35cf..034b2d3a212f 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py @@ -16,47 +16,45 @@ """Emission-assisted GVR top-k state for the DSA decode path. Owns the persistent (graph-address-stable) buffers the emission tiers -ride on, the device-side closed-loop seed-row update (pure tensor ops: -CUDA-graph capturable, validated 11/11-step replay-exact against eager) -and the per-step routing decision. All of it is opt-in: without the -flag the DSA decode path is byte-identical to before. +ride on, the device-side closed-loop seed-row update (pure tensor ops, +CUDA-graph capturable) and the per-step routing decision. Opt-in: +without the flag the DSA decode path is unchanged. Tier semantics (see gvr_routing): * this step's TOP-K consumes what the PREVIOUS step's indexer epilogue emitted; * this step's INDEXER emits what the routing planned for the NEXT - step. N changes by at most one slot per step, so tier flapping is - a non-issue. + step. """ from typing import Optional import torch -from ...cute_dsl_kernels.blackwell.top_k.gvr_routing import TopkRoute, pick_config, plan_emission +from ...cute_dsl_kernels.blackwell.top_k.gvr_routing import ( + LIST_EMIT_MIN_N, + TopkRoute, + pick_config, + plan_emission, +) -# Bucketed list geometry (validated defaults: B* = 8192 segment cap, -# 24576-entry C segment; see the f15/f17 sweeps). +# Bucketed candidate-list geometry: two tight segments of LIST_SEG_A +# entries plus a LIST_CAP_C-entry loose segment. LIST_SEG_A = 8192 LIST_CAP_C = 24576 LIST_WIDTH = 2 * LIST_SEG_A + LIST_CAP_C -# Closed-loop line derivation around the published k-th anchor: t1 -# hugs the k-th value from below, t0/t2 guard by the (anchor - kth) -# span. Matches the graph_test.py-validated update. +# Closed-loop lines around the published k-th anchor: t1 hugs the k-th +# value from below; t0/t2 guard by the (anchor - kth) span. +__all__ = ["GvrExtState", "LIST_EMIT_MIN_N", "LIST_PARK_LINE"] + GUARD_LO = 2.0 GUARD_HI = 0.5 # List tier only: park the two tight lines above any score so every -# admitted entry lands in the loosest segment, which is the one that -# already claims its slots through a per-warp window instead of an -# exact ballot. Measured on the indexer kernel (nsys kernel-only): the -# emission cost drops 2.7-5x (e.g. batch 16 / ctx 512k, +88.7us -> -# +15.0us) and the top-k side stays at parity, because the consumer -# reads the segment counts from the control row and finds the two tight -# segments empty. Any finite value above the score range works; the -# kernel's eligibility check only needs the three lines to be -# increasing and the loosest one finite. +# admitted entry lands in the loosest segment. Any finite value above +# the score range works; the kernel's eligibility check only needs the +# three lines increasing and the loosest one finite. LIST_PARK_LINE = 1.0e30 @@ -81,27 +79,20 @@ def __init__( self.cand_idx = torch.zeros((max_rows, LIST_WIDTH), dtype=torch.int32, device=device) self.cand_ctl = torch.zeros((max_rows, 4), dtype=torch.int32, device=device) self.cand_cur = torch.zeros((max_rows, 4), dtype=torch.int32, device=device) - # GVR warm-start feedback: this layer's previous-step top-k - # (same stable-address feedback-loop shape as - # heuristic_prev_topk; zero-init -> first step's pre_idx points - # at index 0, a valid benign candidate) + # previous-step top-k feedback (address-stable; zero-init -> + # first step's pre_idx points at index 0, a benign candidate) self.prev_topk = torch.zeros((max_rows, top_k), dtype=torch.int32, device=device) # block_max prefix ([rows, nb_pad*4] fp32 warp-partials), # allocated lazily once max_seq_len is known self.block_max: Optional[torch.Tensor] = None - # tier the PREVIOUS indexer call emitted (what this step's - # top-k may consume); "rungs" until the first emission lands - self.emitted_tier = "rungs" def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: nb4 = ((max_seq_len + 255) // 256 * 256) // 128 * 4 # exact width: the runner asserts shape == (rows, nrec), so a # wider reused buffer would trip it if self.block_max is None or self.block_max.shape[1] != nb4: - # max_seq_len is engine-static, so this allocates once on the - # first (eager warmup) step; allocating inside CUDA graph - # capture would bake a dangling address into the graph, so - # fail loudly instead of corrupting the capture. + # allocating during CUDA graph capture would bake a dangling + # address into the graph, so fail loudly instead if torch.cuda.is_current_stream_capturing(): raise RuntimeError( "GvrExtState.ensure_block_max: (re)allocation requested " @@ -116,8 +107,8 @@ def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: def plan( self, batch: int, n_comp: int, num_sms: int, compress_ratio: int = 4 ) -> tuple[str, TopkRoute]: - """Route this step: (tier to EMIT next, launch knobs to CONSUME - what was emitted last step).""" + """Route this step: (tier the epilogue emits, launch knobs the + top-k consumes it with).""" emit_tier = plan_emission( batch, n_comp, self.top_k, have_epilogue=True, compress_ratio=compress_ratio ) @@ -125,16 +116,17 @@ def plan( # constructed with enable_list_tier=False: no candidate # buffers to emit into, demote to the counts tier emit_tier = "counts" - route = pick_config(self.emitted_tier, batch, n_comp, self.top_k, num_sms) + # emission and consumption happen inside the SAME forward (zero, + # emit, consume), so the consumer routes on this step's tier + route = pick_config(emit_tier, batch, n_comp, self.top_k, num_sms) return emit_tier, route def update_seed_rows(self, num_rows: int, emit_tier: str = "counts") -> None: """Device-side closed-loop line update from the last publish. - Pure tensor ops (graph-capturable). Rows whose xstate is not - valid (col 0 == 0, e.g. cold start) get non-finite lines, which - the kernel's validity guard routes to the stock path - the - closed loop never rides on host data quality. + Pure tensor ops (graph-capturable). Rows with invalid xstate + (col 0 == 0, e.g. cold start) get non-finite lines, which the + kernel's validity guard routes to the stock path. """ s = self.seed_row[:num_rows] x = self.xstate[:num_rows] @@ -170,14 +162,13 @@ def indexer_emit_kwargs(self, emit_tier: str, num_rows: int) -> dict: cand_ctl_out=self.cand_ctl[:num_rows], cand_cur_out=self.cand_cur[:num_rows], ) - self.emitted_tier = emit_tier return kw def topk_ext_kwargs( self, route: TopkRoute, num_rows: int, block_max: Optional[torch.Tensor] ) -> dict: - """kwargs for trtllm::cute_dsl_gvr_topk_decode consuming the - PREVIOUS step's emission per the picked route.""" + """kwargs for trtllm::cute_dsl_gvr_topk_decode consuming this + step's emission per the picked route.""" kw: dict = { "xstate": self.xstate[:num_rows], "cluster_size": route.cluster_size, @@ -186,16 +177,18 @@ def topk_ext_kwargs( kw["num_threads"] = route.num_threads if route.tier in ("counts", "list"): kw["seed_thr"] = self.seed_row[:num_rows] - # rungs tier (first step / no emission yet): pass no seed at - # all - cold-start xstate is invalid so the lines would be - # non-finite anyway; the plain stock path is the right fallback - # (a [rows, 3] column view of the packed row is non-contiguous - # and would trip the runner's contract assert) + # rungs tier: pass no seed at all (a [rows, 3] column view of the + # packed row is non-contiguous and would trip the runner's assert) if route.tier == "list": + # accept_cap must match the emitter's segment geometry: the + # buffers are laid out at bases 0 / LIST_SEG_A / 2*LIST_SEG_A, + # and the consumer derives the C capacity from the tensor + # width minus 2*accept_cap. kw.update( cand_vals=self.cand_vals[:num_rows], cand_idx=self.cand_idx[:num_rows], cand_ctl=self.cand_ctl[:num_rows], + accept_cap=LIST_SEG_A, ) if route.attach_block_max and block_max is not None: kw["block_max"] = block_max diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 766d8a272a97..042d716ae8c8 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -52,13 +52,13 @@ def _env_flag(name: str) -> bool: # Diagnostic knob: compile per-phase clock64 stamps of the list path # into the spare xstate slots (harness-side analysis). Off by default; # NEVER set in production. -_P4_TAIL_DBG = _env_flag("GVR_P4_TAIL_DBG") +_P4_TAIL_DBG = _env_flag("TRTLLM_GVR_P4_TAIL_DBG") # P4 sub-phase clock64 breakdown -> xstate[1,2,4,5,6,7] (debug: clobbers # the closed-loop thr/anch publish; single-shot cells only, not chains) -_P4_SUB_DBG = _env_flag("GVR_P4_SUB_DBG") -# GVR_P4_SUB_DBG=2: publish the P4 HEAD triple (minmax / histogram build / +_P4_SUB_DBG = _env_flag("TRTLLM_GVR_P4_SUB_DBG") +# TRTLLM_GVR_P4_SUB_DBG=2: publish the P4 HEAD triple (minmax / histogram build / # coarse search) instead of the tail triple, so the phase budget adds up. -_P4_SUB_HEAD = os.environ.get("GVR_P4_SUB_DBG", "0").strip() == "2" +_P4_SUB_HEAD = os.environ.get("TRTLLM_GVR_P4_SUB_DBG", "0").strip() == "2" # Exact-tail small-class pair buffer. The scatter parks each member of the # straddling tie class as (value bits, index) here, above the 256 digit bins # the large-class radix zeroes and above its [256..258] scalars, so the repair @@ -81,7 +81,7 @@ def _pair_cap_for(n_bins: int) -> int: return min(_PAIR_MAX, 1 << (room.bit_length() - 1)) -_SKIP_DBG = _env_flag("GVR_SKIP_DBG") +_SKIP_DBG = _env_flag("TRTLLM_GVR_SKIP_DBG") # --------------------------------------------------------------------------- @@ -635,8 +635,7 @@ def __init__( # physical candidate-buffer capacity override (B* search) self.kC = int(kc_override) # acceptance band top B*: a cut whose count fits [K, B*] goes - # straight to Phase 4. Cost-bounded (refine ~1.9us/1k cands vs - # ~21-30us fallback), physically bounded by kC. + # straight to Phase 4. Physically bounded by kC. self.accept_cap = int(accept_cap) if accept_cap is not None else self.kC self.cand_cap = int(cand_cap) # list path: the score column is staged into a DEDICATED smem @@ -757,28 +756,13 @@ def __init__( p4_exact_tail = self.enable_p4_rank_scatter_exact and dtype == cutlass.Float32 self.p4_exact_tail = bool(p4_exact_tail) and self.enable_p4_rank_scatter_exact # p4_tail_fast: tiny-tie COLLECT+SELECT fast path inside the - # exact-tail fire branch. When the (b*, sb*) tie class holds <= 128 - # entries (the real firing cells have 2), ONE candidate pass collects - # (value_bits, cand_idx) pairs into SMEM and thread0 selects the - # top-need exactly, replacing the 4 unconditional radix passes - # (~5.3us -> ~1 pass on pro/512k). Larger tie classes fall through to - # the existing radix select. Pure optimization (the radix backstop - # keeps exactness identical either way); False compiles the original - # text (byte-identical PTX modulo kernel name) for A/B. - # Default gate = p4_exact_tail AND top_k >= 1024: the non-firing - # codegen tax concentrates at K512 cs=1 mid-N (flash 64k/128k - # -6.6/-9.1%, cross-GPU reproducible, 2026-07-20 b200-035) while the - # fire census (pro/512k bench + 9 per-layer fixture cells) contains - # NO K512 cell — so K512 keeps the original byte-identical kernel. + # exact-tail fire branch: when the boundary tie class holds few + # enough entries to buffer, one candidate pass replaces the radix + # passes; larger classes fall through to the radix backstop, so + # exactness is identical either way. if p4_tail_fast is None: # [p4tt] - # Was gated on top_k >= 1024, which left K=512 on the original - # full-candidate 4-level radix: every level re-zeroes 256 bins - # and re-walks the WHOLE candidate array, so a step whose - # boundary class is large costs ~5us against ~0.2us for its - # neighbours. The compacted path pays for the class only. - # Measured on the captured flash rows that trigger it - # (256k layer 20 step 4, K=512): 5.04us -> 2.17us, with the - # neighbouring steps unchanged at ~0.2us. + # default follows p4_exact_tail for every K (the compacted + # path pays for the boundary class only) p4_tail_fast = self.p4_exact_tail self.p4_tail_fast = bool(p4_tail_fast) and self.p4_exact_tail # [p4tt] # p4_tail_v3: compacted-class repair (block-parallel radix + @@ -790,27 +774,30 @@ def __init__( use_ext_counts or use_ext_cand or ext_rungs or self_scan or enable_block_skip ) self.p4_tail_v3 = bool(p4_tail_v3) - # p4_no_fine: drop the 256-bin fine level from phase 4 outright and - # let the tail repair rank the whole straddling COARSE bin instead. - # Phase 4's cost is a chain of sequential stages, not work: its total - # barely moves when the candidate count triples, so the only way to - # cut it is to remove a stage. The coarse bin holds 11 candidates at - # the median and 49 at the max over 520 captured steps - inside the - # tail's pair buffer - and the tail now ranks with the whole block, - # so it absorbs the class cheaply. A class past the buffer falls into - # the tail's radix, which handles any size. Gated to the same - # configuration as that small-class route; the stock kernel keeps the - # fine level so its codegen is untouched. + # p4_no_fine: drop the 256-bin fine level from phase 4 and let the + # tail repair rank the whole straddling COARSE bin instead. A class + # past the tail's pair buffer falls into its radix, which handles + # any size. Gated to the same configuration as that small-class + # route; the stock kernel keeps the fine level so its codegen is + # untouched. if p4_no_fine is None: p4_no_fine = bool( self.p4_exact_tail and self.p4_tail_fast and self.p4_tail_v3 ) and not (self.p4_fine_rangetest or self.p4_scat_rangetest) self.p4_no_fine = bool(p4_no_fine) + if self.p4_no_fine: + if not (self.p4_exact_tail and self.p4_tail_fast and self.p4_tail_v3): + raise ValueError("p4_no_fine requires the exact-tail repair chain") + if self.p4_fine_rangetest or self.p4_scat_rangetest: + raise ValueError("p4_no_fine is incompatible with the range-test arms") + if self.p4_exact_tail and self.p4_tail_fast and self.p4_tail_v3: + if _pair_cap_for(self.kNumBins) < 1: + raise ValueError( + f"kNumBins={self.kNumBins} leaves no room for the tail pair buffer" + ) # p1r_rescue: rebuild the refine bracket from the row when the - # seed bracket is degenerate. ON by default - upstream's identity - # shortcut is wrong on real data (every request's first decode - # step feeds a zero-init prev_topk). The knob exists to measure - # its cost, not to ship it off. + # seed bracket is degenerate (e.g. the zero-init prev_topk every + # request's first decode step feeds). ON by default. self.p1r_rescue = bool(p1r_rescue) # ------------------------------------------------------------------ @@ -1440,16 +1427,13 @@ def phase0_scan_bucket( uncapped), so {n0, void, n1, n2} fall out for free — the same contract the v5 emitter produced externally. - Perf shape (v13): the dense scan is a cp.async pipeline — each - thread streams one 16B vector per step into its private - slot-major smem staging slot (LDGSTS: no data registers, no - scoreboard stall until the wait), keeping ``stage_slots`` steps - in flight; classification reads the staged values and claims - passers with per-element direct smem atomics (they don't - synchronize the warp and hide under the async copy stream). - Segment overflow is resolved in-claim by spilling to the next - looser segment (a segment overflows at most once per row, and - divergent scalar atomics need no warp coordination).""" + The dense scan is a cp.async pipeline: each thread streams one + 16B vector per step into its private slot-major smem staging + slot, keeping ``stage_slots`` steps in flight; classification + reads the staged values and claims passers with per-element + direct smem atomics (they don't synchronize the warp). Segment + overflow is resolved in-claim by spilling to the next looser + segment (a segment overflows at most once per row).""" num_threads = cutlass.const_expr(self.num_threads) segA = cutlass.const_expr(self.accept_cap) capC = cutlass.const_expr(self.cap_c) @@ -1475,13 +1459,10 @@ def phase0_scan_bucket( # are the same non-synchronizing per-element atomics as the # dense loop - so the block loop needs no uniform trip counts. if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): - # v8.2: 8 blocks per warp iteration. Every lane vector-loads - # the SAME 8 bmax values (L1 broadcast - the pass decisions - # are warp-uniform registers), then the passing blocks are - # loaded back-to-back (independent 128B coalesced loads, so - # the memory level parallelism the naive one-block loop - # lacked is restored; at high skip rates an iteration is - # just the one 32B bmax vector). + # 8 blocks per warp iteration: every lane vector-loads the + # SAME 8 bmax values (L1 broadcast - the pass decisions are + # warp-uniform registers), then the passing blocks are + # loaded back-to-back as independent 128B coalesced loads. bm_addr = block_max_row.iterator.toint() nb0 = (N + cutlass.Int32(31)) >> cutlass.Int32(5) # Two-pass skip: (1) DENSE-scan the bmax array itself (it @@ -1489,10 +1470,8 @@ def phase0_scan_bucket( # PASSING BLOCK IDS into the idle C segment (single-band mode # never fills C; ids < 2^23 store exactly as floats); # (2) walk the compact list, 8 blocks per warp round issued - # unguarded back-to-back - every element read is useful and - # the loads pipeline. If the list overflows capC the row - # falls back to the dense full scan (routing should have - # sent it there anyway). + # unguarded back-to-back. If the list overflows capC the row + # falls back to the dense full scan. # pass-1 vectors: 128-bit (the bmax row base is only 16B # aligned: nb_pad %% 4) pass1_atom = cute.make_copy_atom( @@ -1661,17 +1640,13 @@ def phase0_scan_bucket( cpw = cutlass.const_expr(4) # cp.async caps at 16B per copy step1 = cutlass.const_expr(num_threads * cpw) st2log = cutlass.const_expr((2 * step1).bit_length() - 1) - # Pair-step cp.async pipeline: the scan is instruction-issue - # bound (pcsamp: no_instructions + wait dominate; long_scoreboard - # is 6%), so each step processes TWO 16B vectors per thread — - # loop/wait/commit/address overhead amortizes over 8 elements - # instead of 4 while the in-flight byte count stays put (2 pairs - # x 32B across the 4 staging slots). One commit group per pair; - # wait_group(1) pops the oldest pair. The staging buffer aliases - # smem_vals (only written after phase 0); every non-empty group - # is drained inside the loop, so nothing is in flight once the - # alias is read. FULL-pair steps only in the hot loop; the - # remainder takes the scalar tail below. + # Pair-step cp.async pipeline: each step processes TWO 16B + # vectors per thread (2 pairs x 32B across the 4 staging slots). + # One commit group per pair; wait_group(1) pops the oldest pair. + # The staging buffer aliases smem_vals (only written after phase + # 0); every non-empty group is drained inside the loop, so + # nothing is in flight once the alias is read. FULL-pair steps + # only in the hot loop; the remainder takes the scalar tail below. nfull = N >> st2log if cutlass.const_expr(self.enable_block_skip and block_max_row is not None): nfull = cutlass.Int32(0) @@ -2110,9 +2085,8 @@ def block_count_ge( # columns (the accepted rung's column seeds Phase 3 per CTA). # ------------------------------------------------------------------ - # ---- block-skip machinery (enable_block_skip; ported from the - # skip-finegrain development chain, measured-optimal configuration - # only: grain 32, int16 list, grouped strided build, UN=2 scan) ---- + # ---- block-skip machinery (enable_block_skip): grain 32, int16 + # list, grouped strided build, UN=2 scan ---- @cute.jit def _list_ld(self, smem_active, idx): @@ -3541,15 +3515,12 @@ def _kth_bin_search_rw(self, smem_hist, smem_wcnt, lo, binw, tidx, warp_id, lane # ------------------------------------------------------------------ # _p4_coarse_rw - redundant-warp coarse bin search for the fused - # rank-and-scatter path. Same result as the high->low walk it - # replaces (the straddling bin and the count strictly above it), - # but staged once and then resolved lane-parallel on every warp: - # an idx-shuffle scan + ballot locate the target slice, a second - # scan + the unique crossing test locate the bin inside it. Two - # publish barriers and a bins_per_warp-deep serial LDS+IADD chain in - # a single lane disappear; integer sums are associative so every - # warp lands on the same answer bit-for-bit. Mirrors - # _kth_bin_search_rw, which does the same for the snap path. + # rank-and-scatter path: returns the straddling bin and the count + # strictly above it, resolved lane-parallel on every warp (an + # idx-shuffle scan + ballot locate the target slice, a second scan + # + the unique crossing test locate the bin inside it). Integer + # sums are associative, so every warp lands on the same answer + # bit-for-bit. Mirrors _kth_bin_search_rw (snap path). # ------------------------------------------------------------------ @cute.jit def _p4_coarse_rw(self, smem_hist, smem_wcnt, warp_id, lane): @@ -3653,13 +3624,10 @@ def _p4_coarse_rw(self, smem_hist, smem_wcnt, warp_id, lane): # ------------------------------------------------------------------ # _p4_fine_rw - redundant-warp variant of the fine sub-bin search, - # the same transformation _p4_coarse_rw applies one level up. The - # serial form stages per-warp slice sums, has thread 0 walk the warp - # totals, then has one lane of the target warp walk that slice bin by - # bin, with three publish barriers. Here every warp resolves it from - # the staged sums with an idx-shuffle scan and a ballot, so two - # barriers and the fbins/num_warps-deep serial LDS chain disappear. - # Integer sums are associative: every warp lands on the same answer. + # the same transformation _p4_coarse_rw applies one level up: every + # warp resolves it from the staged per-warp sums with an idx-shuffle + # scan and a ballot. Integer sums are associative: every warp lands + # on the same answer. # ------------------------------------------------------------------ @cute.jit def _p4_fine_rw(self, smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane): @@ -3751,8 +3719,7 @@ def _p4_fine_rw(self, smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane): return sb_out, ra_out # ------------------------------------------------------------------ - # Phase 4 (alt): op#7 fused rank-and-scatter (enable_p4_rank_scatter). - # Ported verbatim from p4_recursive_digit/gvr_topk_decode_p4.py. + # Phase 4 (alt): fused rank-and-scatter (enable_p4_rank_scatter). # ------------------------------------------------------------------ @cute.jit def phase4_rank_scatter( @@ -3807,8 +3774,7 @@ def phase4_rank_scatter( if use_ext_r == cutlass.Int32(1): # list rows: the take walk pre-zeroed the hist and staged # per-warp maxima in smem_wcnt (its end barrier orders - # them); min := cut line by construction. The minmax - # scan, the zero pass and their three barriers vanish. + # them); min := cut line by construction. if cutlass.const_expr(ext_min is not None): bmin_r = ext_min for w in cutlass.range_constexpr(self.num_warps): @@ -3820,14 +3786,9 @@ def phase4_rank_scatter( bmax_r = bmin_r + cutlass.Float32(1e-6) if run_stock_range: # ---- block min/max over candidates ---- - # The accepted threshold is an EXACT lower bound: every - # candidate got here by comparing >= against it. So the min - # side of this scan buys nothing but a slightly tighter - # window - and the window only has to be tight where the - # k-th lives, which is the TOP of it. Taking the threshold - # instead drops half the scan, one warp reduce and one arm - # of the cross-warp fold. Only the assist tiers have a - # published threshold, hence the gate. + # The accepted threshold is an EXACT lower bound on every + # candidate, so it can stand in for the min. Only the + # assist tiers have a published threshold, hence the gate. use_thr_min = cutlass.const_expr(self.p4_no_fine) local_cmin = cutlass.Float32(self.FLT_MAX) local_cmax = cutlass.Float32(self.NEG_FLT_MAX) @@ -3848,10 +3809,8 @@ def phase4_rank_scatter( smem_hist[warp_id] = float_as_uint32(cmax) cute.arch.barrier() # lane-parallel cross-warp fold: lane w holds slot w and one - # warp reduce settles it, instead of every thread walking all - # num_warps slots of two arrays with dependent SMEM reads. - # min/max reassociate freely, so the result is bit-identical - # and every warp still lands on it without a leader. + # warp reduce settles it. min/max reassociate freely, so the + # result is bit-identical on every warp without a leader. pmn = cutlass.Float32(self.FLT_MAX) pmx = cutlass.Float32(self.NEG_FLT_MAX) if lane < cutlass.Int32(self.num_warps): @@ -3960,8 +3919,7 @@ def phase4_rank_scatter( # ---- EXACT: one fine-histogram recursion on the straddling bin b* ---- if cutlass.const_expr(self.enable_p4_rank_scatter_exact): - # FIXED small fine-bin count (independent of kNumBins) — cuts the - # re-zero + 3-step cost (esp. K=2048 where kNumBins=2048); 256 + # FIXED small fine-bin count (independent of kNumBins): 256 # sub-bins over bin b* gives kNumBins×256 effective resolution, # enough to resolve the straddling bin to ≤1 distinct value. fbins = cutlass.const_expr(256) @@ -3975,11 +3933,8 @@ def phase4_rank_scatter( # Sub-binning collapsed: finv 0 sends every member of the # straddling coarse bin to sub-bin 0 == sb*, so the # scatter parks the whole coarse class and the tail ranks - # it. The re-zero, the build and the search below are not - # traced at all - this is a compile-time removal, not a - # runtime branch, because a runtime branch still costs the - # stage (measured: 0.64us for a fine window whose work was - # gated off). + # it. MUST stay a compile-time removal (the re-zero, build + # and search below untraced), not a runtime branch. finv = cutlass.Float32(0.0) sb_star = cutlass.Int32(0) rank_above_fine = rank_above @@ -4000,12 +3955,10 @@ def phase4_rank_scatter( cute.arch.barrier() if cutlass.const_expr(self.p4_fine_rangetest): # A candidate belongs to bin b* exactly when its value - # lies in [f_lo, f_hi), so the filter is two compares - - # the bin recompute (subtract + multiply + two clamps) - # per candidate is redundant work. The clamped ends of - # the binning fold out-of-range values INTO bin 0 and - # bin kBins-1, so those two bins drop the matching side - # of the range test to stay bit-identical. + # lies in [f_lo, f_hi). The clamped ends of the binning + # fold out-of-range values INTO bin 0 and bin kBins-1, + # so those two bins must drop the matching side of the + # range test to stay bit-identical. ifb = tidx while ifb < cand_count: vf = smem_keys[ifb] @@ -4041,9 +3994,8 @@ def phase4_rank_scatter( ifb = ifb + cutlass.Int32(num_threads) cute.arch.barrier() # fine sub-bin search, resolved lane-parallel on every - # warp (see _p4_fine_rw): the two publish barriers and the - # fbins/num_warps-deep single-lane walk that used to sit - # here are gone, and the answer comes back in registers. + # warp (see _p4_fine_rw); the answer comes back in + # registers. sb_star, rank_above_fine = self._p4_fine_rw( smem_hist, smem_wcnt, fbins, rank_above, warp_id, lane ) @@ -4115,20 +4067,13 @@ def phase4_rank_scatter( if cutlass.const_expr( self.p4_exact_tail and self.p4_tail_fast and self.p4_tail_v3 ): - # The tie class is exactly what the exact-tail - # repair needs, and this pass has already - # classified every candidate AND handed out the - # intra-class ordinal. Park (value bits, index) - # here so the repair does not have to walk the - # candidates again: measured 0.61us of its - # remaining cost was that second walk. Pairs + # Park (value bits, index) so the exact-tail + # repair never re-walks the candidates. Pairs # live above the digit bins the radix route # zeroes; small classes are the only consumer. # Unconditional, index wrapped: the buffer is # only ever READ when the class fits it, and - # then the wrap is a no-op. Dropping the bound - # branch keeps this off the scatter's critical - # path, which every step pays. + # then the wrap is a no-op. ow = (o & cutlass.Int32(pair_cap - 1)) * cutlass.Int32(2) smem_hist[cutlass.Int32(_PAIR_BASE) + ow] = float_as_int32(v) smem_hist[cutlass.Int32(_PAIR_BASE) + ow + cutlass.Int32(1)] = ( @@ -4166,16 +4111,10 @@ def phase4_rank_scatter( # slot range [rank_above_fine, kK). Unambiguous rows (the # overwhelming majority) pay two scalar compares; the counters # and the fine histogram are reused, so SMEM does not grow. - # boundary-class repair: collect the (b*, sb*) tie - # class compactly (ONE candidate pass), then select inside - # it. Tiny jobs (need x class <= 512) keep the thread0 - # serial select (cheapest at that size). Bigger classes up - # to capc get a 4-level MSB radix over the COLLECTED class - # (each level scans <= capc pairs with all threads instead - # of re-scanning every candidate) — the old serial select - # was O(need x class) and measured 8.5us on real chain rows - # (need ~100 x class ~100). Classes beyond capc take the - # UNMODIFIED full-candidate radix below (verbatim copy). + # boundary-class repair: pure-tie classes (one key value) + # exit on a warp-reduce precheck; classes parked in the + # pair buffer rank block-parallel over the parked pairs; + # anything larger takes the full-candidate radix below. if cutlass.const_expr(self.p4_exact_tail and self.p4_tail_fast): # [p4tt] if cutlass.const_expr(self.p4_tail_v3): need0 = cutlass.Int32(kK) - rank_above_fine @@ -4197,19 +4136,9 @@ def phase4_rank_scatter( # Small mixed class: the scatter already parked # every member as a (value bits, index) pair, # so there is nothing to collect - go straight - # to the rank. No extra candidate walk, no - # staging barrier, nothing riding in registers. - # Rank the class with the WHOLE BLOCK, not - # one warp. Each warp owns a stride of the - # class and its 32 lanes split the - # comparisons, so the cost is - # ceil(n/num_warps) * ceil(n/32) trips per - # lane plus one warp reduce - 1 trip for the - # class of two that fires today, 8 for a class - # of fifty. The old form had a single warp walk - # n^2/32 steps while the other fifteen waited - # at the barrier below, which is what made a - # larger class unaffordable. + # to the rank. Rank with the WHOLE BLOCK: each + # warp owns a stride of the class and its 32 + # lanes split the comparisons. e9 = warp_id while e9 < cnt_strad: be9 = smem_hist[_PAIR_BASE + e9 + e9] @@ -4253,20 +4182,12 @@ def phase4_rank_scatter( e9 = e9 + cutlass.Int32(num_warps) cute.arch.barrier() else: - # Large class only. Everything below - - # the pure-tie pre-check walk, its - # cross-warp fold and the publish - # barrier that fold needs - exists to - # decide whether the radix can be - # skipped. The small route above needs - # none of it, and used to run it anyway - # for a class of two. - # block-wide pure-tie check, ANY class - # size: min/max order key over the (b*, sb*) class. + # Large class only: block-wide pure-tie check + # (min/max order key over the (b*, sb*) class) + # decides whether the radix can be skipped. # A pure-tie class needs NO repair — the scatter's # arrival fill of bit-equal values is already - # value-set exact. Real fp8-lineage logits tie in - # the thousands, which used to take the full radix. + # value-set exact. # Staging mirrors the head min/max (wcnt + hist # slots [0..31], both dead here; pairs live at # 260+). @@ -4276,14 +4197,10 @@ def phase4_rank_scatter( kmn6 = cutlass.Int32(2147483647) kmx6 = cutlass.Int32(-2147483648) it6 = tidx - # The pure-tie pre-check only exists to SKIP the - # repair when the class is bit-uniform. The small - # route below ranks by (key, arrival) and rewrites - # exactly the need0 winner slots, which is already - # value-exact on a pure tie - so for a small class - # the check is a whole candidate walk bought for - # nothing. Only the large-class route, whose radix - # really is worth avoiding, still pays for it. + # The pure-tie pre-check SKIPs the repair when + # the class is bit-uniform; only the large-class + # route runs it (the small route is already + # value-exact on a pure tie). while it6 < cand_count: v6 = smem_keys[it6] b6 = cutlass.Int32((v6 - bmin_r) * inv1) @@ -4292,11 +4209,15 @@ def phase4_rank_scatter( if b6 > cutlass.Int32(kBins - 1): b6 = cutlass.Int32(kBins - 1) if b6 == b_star: - s6 = cutlass.Int32((v6 - f_lo) * finv) - if s6 < cutlass.Int32(0): - s6 = cutlass.Int32(0) - if s6 > cutlass.Int32(fbins - 1): - s6 = cutlass.Int32(fbins - 1) + # with the fine level compiled out the + # whole coarse bin IS the class + s6 = cutlass.Int32(0) + if cutlass.const_expr(not self.p4_no_fine): + s6 = cutlass.Int32((v6 - f_lo) * finv) + if s6 < cutlass.Int32(0): + s6 = cutlass.Int32(0) + if s6 > cutlass.Int32(fbins - 1): + s6 = cutlass.Int32(fbins - 1) if s6 == sb_star: k6 = f32_order_key(v6) ^ cutlass.Int32(-2147483648) if k6 < kmn6: @@ -4304,13 +4225,9 @@ def phase4_rank_scatter( if k6 > kmx6: kmx6 = k6 # Buffer the member so the compaction - # below needs no walk of its own. The + # below needs no walk of its own; the # array is nbuf7 = kC/num_threads - # deep and every store is an unrolled - # dynamic-index search over it, which - # is why only the large class - whose - # alternative is a 4-level radix - - # pays for it. + # deep (unrolled dynamic-index store). for sl7 in cutlass.range_constexpr(nbuf7): if cutlass.Int32(sl7) == nh7: rv7[sl7] = v6 @@ -4341,17 +4258,12 @@ def phase4_rank_scatter( if fast_done == cutlass.Int32(0): # mixed class: compact the members buffered by # the pure-tie pass above into - # smem_keys/vals[0..cnt_strad). The buffering - # pass already read every candidate it needs, - # and the staging barrier above orders those - # reads before these writes, so the second - # full-candidate walk (and its barrier) is - # gone. Repair cost is a function of the CLASS - # size only, for any class size. + # smem_keys/vals[0..cnt_strad). The staging + # barrier above orders the buffered reads + # before these writes. # warp-aggregated claim: intra-warp exclusive # prefix via shfl scan + ONE atomic per warp - # (a thousand same-address claims serialize - # and scale with the class size) + # (same-address claims would serialize) pf7 = nh7 for so3 in cutlass.range_constexpr(5): oth3 = cute.arch.shuffle_sync_up( @@ -4373,13 +4285,11 @@ def phase4_rank_scatter( smem_keys[bs7 + cutlass.Int32(sl8)] = rv7[sl8] smem_vals[bs7 + cutlass.Int32(sl8)] = ri7[sl8] cute.arch.barrier() - # Large class only (the small one is handled - # above, ahead of the pure-tie pass, so it - # never reaches here): block-parallel 4-level + # Large class only (the small one never + # reaches here): block-parallel 4-level # MSB radix over the compacted class (scans # touch class pairs only; warp0 shuffle-scan - # digit search - 3 block barriers per level - # instead of 5). + # digit search). if tidx == cutlass.Int32(0): smem_hist[256] = cutlass.Int32(0) smem_hist[257] = need0 @@ -4627,8 +4537,7 @@ def phase4_rank_scatter( # Two-stage descending digit scan (mirrors the # fine 3-step search): per-warp partial sums, # thread0 picks the target warp, its lane0 walks - # the warp's digit range — 2*num_warps serial - # steps instead of 256. + # the warp's digit range. fdw = cutlass.const_expr(256 // self.num_warps) wsum2 = cutlass.Int32(0) for jd in cutlass.range_constexpr(fdw): @@ -5629,8 +5538,7 @@ def run_one_row( block_max_row = None if cutlass.const_expr(self.use_ext_counts and seed_thr is not None): # packed seed row [>=6] fp32: [0..2] lines, [3..5] counts as - # floats (exact to 2^24) - ONE 32B sector serves both, halving - # the serial cold loads of the admission preview + # floats (exact to 2^24) - ONE 32B sector serves both seed_thr_row = seed_thr[row_idx, None] seed_counts_row = None elif cutlass.const_expr(self.ext_rungs and seed_thr is not None): @@ -5900,16 +5808,11 @@ def run_one_row( s_cluster_partial_m = None smem_gath = None - # PDL wait placed as late as possible: everything above (row - # resolution, SMEM allocation, config folding - and, on a cold - # call, the instruction fetch for all of it) overlaps with the - # producer indexer's tail, because dependent CTAs stage onto SMs - # as producer CTAs retire. Nothing above reads producer-written - # data: seq_lens/pre_idx come from the host-side metadata and the - # feedback buffer, while logits / block_max / seed_thr / cand are - # first touched below. Instruction-fetch starvation is 37-44% of - # this kernel's stall cycles on the small-N cells, so warming it - # under the producer's shadow is the point. + # PDL wait placed as late as possible so the prologue overlaps + # the producer indexer's tail. INVARIANT: nothing above may read + # producer-written data - seq_lens/pre_idx come from host-side + # metadata and the feedback buffer; logits / block_max / + # seed_thr / cand are first touched below. if cutlass.const_expr(self.pdl_wait_late): griddepcontrol_wait() @@ -6183,10 +6086,8 @@ def _run_phases( # Use the epilogue rungs ONLY when the row is valid (finite t_0, # xstate contract) AND some rung count already lies in [K, kC]. # A miss/invalid row runs the full stock path (P1 + P1b + vseed + - # count): real data shows stock beats ext-bracket refine on - # misses (pro-1M cold: stock-skip 21us vs ext-miss 51us). All - # threads read the same control words, so the predicate is - # CTA-uniform and the dynamic branches below stay convergent. + # count). All threads read the same control words, so the + # predicate is CTA-uniform and the branches below stay convergent. if cutlass.const_expr(_P4_TAIL_DBG): ck0 = cutlass.Int64(0) ck1 = cutlass.Int64(0) @@ -6194,12 +6095,11 @@ def _run_phases( ckE = cute.arch.clock64() # row-phase entry (device-residency ref) ext_row = cutlass.Int32(0) if cutlass.const_expr(self.use_ext_counts): - # line validity mirrors ext_rungs: ALL THREE lines finite and - # strictly ascending. The old t0-only guard let a NaN in - # t1/t2 get parked into the refine brackets (the same failure - # mode that broke ext_rungs exactness on-chain); invalid rows - # fall to the stock path, exactness never rides on the host - # loop's line quality. + # line validity mirrors ext_rungs: ALL THREE lines must be + # finite and strictly ascending (a NaN in t1/t2 must not + # reach the refine brackets); invalid rows fall to the stock + # path, so exactness never rides on the host loop's line + # quality. if ( seed_thr_row[0] < cutlass.Float32(1e37) and seed_thr_row[0] > cutlass.Float32(-1e37) @@ -6222,6 +6122,10 @@ def _run_phases( ): claimed_p = cutlass.Int32(cand_ctl_row[0]) void_p = cutlass.Int32(cand_ctl_row[1]) + # real (non-parked) lines only: the skip stages the raw + # lines into the threshold scratch, and a parked line + # (1e30) would poison every later bracket. Parked rows + # keep Phase 1; the list take below is independent. if ( void_p == cutlass.Int32(0) and claimed_p >= cutlass.Int32(self.top_k + 64) @@ -6230,6 +6134,7 @@ def _run_phases( and seed_thr_row[0] > cutlass.Float32(-1e37) and seed_thr_row[1] > seed_thr_row[0] and seed_thr_row[2] > seed_thr_row[1] + and seed_thr_row[2] < cutlass.Float32(1e29) ): ext_row = cutlass.Int32(1) # ---- self_scan phase 0: fused scan-bucket ---- @@ -6400,13 +6305,11 @@ def _run_phases( # A duplicate/invalid preIdx gather (cold-start zero-init slots, stale # slots pointing past N, an all-tied gather) produces an unusable # bracket. When N > K real selection work remains, so rebuild the - # bracket from the data itself (P1r) and run the normal pipeline — - # the old identity shortcut here returned indices [0, K), which is - # NOT the top-K on real data (production hit: the first decode step - # of every request feeds the zero-init prev_topk feedback buffer). - # If the bracket is STILL degenerate after the rescue, every - # in-range value is identical (or N <= K), and identity output is - # then exact — keep the shortcut for exactly those rows. + # bracket from the data itself (P1r) and run the normal pipeline; + # an identity shortcut here would NOT be the top-K. If the bracket + # is STILL degenerate after the rescue, every in-range value is + # identical (or N <= K), and identity output is then exact — keep + # the shortcut for exactly those rows. if cutlass.const_expr(self.p1r_rescue): v_lo = s_thr[1] v_hi = s_thr[2] @@ -6496,8 +6399,11 @@ def _run_phases( void_c = cutlass.Int32(cand_ctl_row[1]) n1_c = cutlass.Int32(cand_ctl_row[2]) n2_c = cutlass.Int32(cand_ctl_row[3]) - segA = cutlass.const_expr(min(self.accept_cap, self.kC)) - bstar = segA + # segment bases/extents follow the EMITTER geometry + # (accept_cap); the admission bound is additionally + # clamped by the physical candidate capacity kC. + segA = cutlass.const_expr(self.accept_cap) + bstar = cutlass.const_expr(min(self.accept_cap, self.kC)) if cutlass.const_expr(_P4_TAIL_DBG): ck0 = cute.arch.clock64() ck1 = ck0 @@ -6585,6 +6491,9 @@ def _run_phases( hs_len = lenA hs_band = n2_c need_max = cutlass.Int32(1) + if b_hi >= cutlass.Float32(1e29): + # parked upper line: bracket by the segment max + need_max = cutlass.Int32(1) if hs_band < cutlass.Int32(1): hs_band = cutlass.Int32(1) samp_f = (cutlass.Float32(1.0) * hs_len) / hs_band @@ -6623,9 +6532,8 @@ def _run_phases( # by segment/band); the descend base stays in # sample units too - the post-load exact-count # net absorbs the sampling error. - # fire target = 1.25x the K-need: the sampled - # estimate carries ~1-2% noise and firing at the - # band's bottom edge would demote half the time + # fire target = 1.25x the K-need (headroom over + # the sampling noise) kneedS = cutlass.Int32( (cutlass.Float32(1.25) * (kK_l - base_c)) * samp_f + cutlass.Float32(0.5) @@ -6733,10 +6641,8 @@ def _run_phases( lane_c = tidx & cutlass.Int32(self.WARP_SIZE - 1) # fused P4 prologue: zero the coarse hist here and # accumulate the candidate max INSIDE the cut walk - # (per-fragment fmax is free ILP; min := cut line by - # construction). P4's minmax scan + zero pass and - # their three barriers disappear for list rows - the - # staging rides this walk's own end barrier. + # (min := cut line by construction); the staging + # rides this walk's own end barrier. izh_c = tidx while izh_c < cutlass.Int32(self.kNumBins): smem_hist[izh_c] = cutlass.Int32(0) @@ -6897,12 +6803,8 @@ def _run_phases( and self.dtype == cutlass.Float32 ): if ext_row == cutlass.Int32(1) and N < cutlass.Int32(16384): - # short rows only: the fused walk wins ~1us below - # ~16k (pass fixed costs dominate there); at 16k+ - # the claim-dense cells (pro 9.4% band) lose to - # count-then-place and the rest sit at parity, so - # the two-pass path keeps them (measured both ways, - # loncheng cells; captures all sit above the gate) + # short rows only (< 16k): fused claim-collect; + # longer rows keep the two-pass count-then-place path cut_p = s_thr[0] # parked line (staged at P1-init) rbase = input_row.iterator.toint() vw_p = cutlass.const_expr(self.vec_bits // self.dtype.width) @@ -7083,10 +6985,8 @@ def _run_phases( # ---- Phase 2: R0 histogram-ladder admission (single-CTA fast # path) or the secant threshold search ---- - # enable_r0 gates to cluster_size==1 for now: op#26's R0 scans the - # full row in one CTA. The slice-parallel + cluster count-merge - # variant that lets R0 cover the cs>1 long-row branch lands in a - # later commit; until then cs>1 keeps the secant path. + # enable_r0 gates to cluster_size==1: R0 scans the full row in + # one CTA; cs>1 keeps the secant path. if cutlass.const_expr(self.enable_r0): # P1b rung placement -> ONE M-ary R0 count pass -> accept the # tightest rung with count in [K, kC]. On a miss, fall back to @@ -7109,12 +7009,10 @@ def _run_phases( # Parking and staging happen in the P1-init thread0 # block, one barrier for the whole prologue. if cutlass.const_expr(not self.enable_block_skip): - # v3: the parked M-ary pass counted the SAME - # threshold in all three columns (3x compare - # + 3 ptcnt columns for identical values). - # Count it ONCE with the refine primitive - - # same per-thread ptcnt cache and cluster - # merge P3 consumes - and accept in place. + # parked admission: count the parked threshold + # ONCE with the refine primitive - same + # per-thread ptcnt cache and cluster merge P3 + # consumes - and accept in place. if s_r0col[0] == cutlass.Int32(self.M_qf): self.block_count_ge( input_row, @@ -7265,12 +7163,10 @@ def _run_phases( ) cute.arch.barrier() if run_mary and tidx == 0: - # tightest admissible rung = SMALLEST count in [K, kC]. - # (Explicit argmin: with r0_vseed the pmean column is not - # sorted into the rung order; for sorted rungs this is - # equivalent to the old "last m in window" rule.) - # Dropped rungs (block-skip rung tightening) hold PARTIAL - # counts — never admissible. + # tightest admissible rung = SMALLEST count in [K, kC] + # (explicit argmin: with r0_vseed the pmean column is not + # sorted into the rung order). Dropped rungs (block-skip + # rung tightening) hold PARTIAL counts — never admissible. dmask_c = cutlass.Int32(0) if cutlass.const_expr(self.enable_block_skip): dmask_c = s_active_cnt[2] @@ -7314,13 +7210,11 @@ def _run_phases( smem_ptcnt[tidx] = smem_ptcnt_multi[bc * cutlass.Int32(num_threads) + tidx] cute.arch.barrier() # ---- R0 miss: SEEDED bounded log-falsi refine ---- - # At large N the M2D rungs straddle [K, kC]; the refine must - # find a threshold with count in [K, kC] between the measured - # rungs. SEED the loop with the rung bracket AND its known - # counts (clo/chi) so it does log-count regula-falsi from - # iter 0 with no re-measure and no separate R1 shot -> ~2-3 - # count passes (op#26 efficiency) instead of ~6. done=1 on - # accept so Phase 3 skips its retry-shrink. + # The refine must find a threshold with count in [K, kC] + # between the measured rungs. SEED the loop with the rung + # bracket AND its known counts (clo/chi) so it does + # log-count regula-falsi from iter 0 with no re-measure. + # done=1 on accept so Phase 3 skips its retry-shrink. if bc < cutlass.Int32(0): if cutlass.const_expr(self.enable_block_skip): if tidx == cutlass.Int32(0): @@ -7957,14 +7851,11 @@ def pick_config( if n_row < 65536: cluster_size = 1 elif has_block_max and n_row >= 200_000: - # Block-skip sweet spot is cs == 1 with a large per-CTA slice: - # the compact list + rung tightening (cs1-only) beat the - # row-split configs outright once the bounds prune the scan - # (cold protocol, real data: BS1 1.18x, BS64 2.14x, BS1024 - # 4.16x vs this policy's stock picks; splitting shrinks each - # CTA's slice below the skip break-even and disables - # tightening). Below 200k the wrapper drops block_max anyway - # (skip_min_n gate) and the stock picks apply. + # Block-skip requires cs == 1 with a large per-CTA slice + # (splitting shrinks each CTA's slice below the skip + # break-even and disables rung tightening). Below 200k the + # wrapper drops block_max anyway (skip_min_n gate) and the + # stock picks apply. cluster_size = 1 elif num_rows <= 4 and n_row >= 131072: cluster_size = 8 diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py index ea382f8b4145..95ccff38ee87 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py @@ -1278,3 +1278,226 @@ def test_cute_dsl_gvr_topk_decode_tie_flood_beyond_capacity(): ) torch.cuda.synchronize() _tie_aware_check(out_indices, logits, seq_lens, top_k, 1, compress_ratio=1) + + +# --------------------------------------------------------------------------- +# Emission-assisted (ext) tiers: packed seed row / candidate list / block max. +# Inputs emulate the indexer epilogue host-side against the layout contracts +# in ``gvr_ext`` (segments at bases 0 / LIST_SEG_A / 2*LIST_SEG_A, packed row +# = lines at [0..2] + exact counts at [3..5] + skip pass count at [6]). +# --------------------------------------------------------------------------- + +_FLT_MAX = 3.4028234663852886e38 + + +def _lines_at_counts(logits_f32, n_eff, targets): + """Per-row threshold lines placed at exact counts (descending targets + -> ascending line values). count(logits >= line[j]) == targets[j].""" + num_rows = logits_f32.shape[0] + lines = torch.empty((num_rows, len(targets)), dtype=torch.float32, device=logits_f32.device) + for r in range(num_rows): + ne = int(n_eff[r]) + row = logits_f32[r, :ne] + for j, c in enumerate(targets): + c = min(int(c), ne) + lines[r, j] = torch.kthvalue(row, ne - c + 1).values + return lines + + +def _pack_seed_row(logits_f32, n_eff, lines, block_max=None): + """[rows, 8] fp32 packed seed row: lines + exact counts (+ skip count).""" + num_rows, N = logits_f32.shape + pos = torch.arange(N, device=logits_f32.device)[None, :] + valid = pos < n_eff[:, None] + counts = torch.stack( + [((logits_f32 >= lines[:, j : j + 1]) & valid).sum(-1) for j in range(3)], 1 + ).int() + pack = torch.zeros((num_rows, 8), dtype=torch.float32, device=logits_f32.device) + pack[:, 0:3] = lines + pack[:, 3:6] = counts.float() + if block_max is not None: + pack[:, 6] = (block_max >= lines[:, 0:1]).sum(dim=1).float() + return pack.contiguous() + + +@skip_not_sm100 +@pytest.mark.parametrize("top_k", [512, 1024, 2048]) +@pytest.mark.parametrize("mode", ["band", "fat", "miss", "inf"]) +def test_cute_dsl_gvr_topk_decode_ext_counts(top_k, mode): + """Packed seed row ([rows, 8]: lines + exact counts) consumption. + + band: one line's count sits inside the admission band (direct path). + fat: every count overshoots the candidate capacity -> full fallback. + miss: lines above the row max (count 0) -> seed rejected. + inf: non-finite lines (production cold start) -> validity guard. + """ + N, batch = 131072, 4 + logits, pre_idx, seq_lens = _make_inputs(batch, N, top_k, torch.float32, 1, seed=7, varlen=True) + n_eff = seq_lens.to(device=logits.device, dtype=torch.long) + if mode == "band": + lines = _lines_at_counts(logits, n_eff, (4 * top_k, 2 * top_k, top_k + top_k // 4)) + elif mode == "fat": + lines = _lines_at_counts(logits, n_eff, (32768, 24576, 16384)) + elif mode == "miss": + pos = torch.arange(N, device=logits.device)[None, :] + rowmax = torch.where(pos < n_eff[:, None], logits, float("-inf")).amax(-1) + lines = rowmax[:, None] + torch.tensor([1.0, 2.0, 3.0], device=logits.device) + else: + lines = torch.full((batch, 3), float("inf"), device=logits.device) + seed_row = _pack_seed_row(logits, n_eff, lines) + xstate = torch.zeros((batch, 8), dtype=torch.float32, device=logits.device) + out_indices = torch.empty(batch, top_k, dtype=torch.int32, device="cuda") + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, + pre_idx, + seq_lens, + out_indices, + top_k=top_k, + cluster_size=1, + seed_thr=seed_row, + xstate=xstate, + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1) + if mode == "band": + # the closed loop must republish valid state for the next step + assert bool((xstate[:, 0] > 0).all().item()) + + +@skip_not_sm100 +@pytest.mark.parametrize("mode", ["hit", "pads", "hist", "void", "bucketed"]) +def test_cute_dsl_gvr_topk_decode_ext_list(mode): + """Candidate-list tier at the production geometry (accept_cap = + LIST_SEG_A, width = LIST_WIDTH). + + hit: parked lines (production shape), claimed inside [K+64, B*] + -> line cut, single mapped load. + pads: same + interleaved idx=-1 window sentinels (claimed counts + them; the K+64 slack absorbs them). + hist: claimed past B* but list complete -> clamped-histogram + fallback over segment C. + void: collection overflows LIST_CAP_C -> void=1 -> full scan. + bucketed: three live lines spread across segments A/B/C, cut at the + tightest line inside the band. + """ + from tensorrt_llm._torch.attention_backend.sparse.gvr_ext import ( + LIST_CAP_C, + LIST_PARK_LINE, + LIST_SEG_A, + LIST_WIDTH, + ) + + top_k, N, batch = 512, 131072, 2 + logits, pre_idx, seq_lens = _make_inputs( + batch, N, top_k, torch.float32, 1, seed=11, varlen=True + ) + dev = logits.device + n_eff = seq_lens.to(device=dev, dtype=torch.long) + if mode == "bucketed": + lines = _lines_at_counts(logits, n_eff, (20000, 4000, 600)) + else: + n0 = {"hit": 4096, "pads": 4096, "hist": 12000, "void": 30000}[mode] + l0 = _lines_at_counts(logits, n_eff, (n0,)) + lines = torch.cat( + [l0, torch.full_like(l0, LIST_PARK_LINE), torch.full_like(l0, 2 * LIST_PARK_LINE)], 1 + ) + seed_row = _pack_seed_row(logits, n_eff, lines) + cand_vals = torch.full((batch, LIST_WIDTH), float("-inf"), dtype=torch.float32, device=dev) + cand_idx = torch.full((batch, LIST_WIDTH), -1, dtype=torch.int32, device=dev) + cand_ctl = torch.zeros((batch, 4), dtype=torch.int32, device=dev) + pads = 64 if mode == "pads" else 0 + for r in range(batch): + ne = int(n_eff[r]) + row = logits[r, :ne] + hits = torch.nonzero(row >= lines[r, 0], as_tuple=False).flatten() + cand_ctl[r, 2] = int((row >= lines[r, 1]).sum()) + cand_ctl[r, 3] = int((row >= lines[r, 2]).sum()) + # emission order is value-blind: shuffle, then classify by the + # tightest line passed; a full segment spills to the looser one + perm = hits[torch.randperm(hits.numel(), device=dev)] + v = row[perm] + seg = torch.where(v >= lines[r, 2], 0, torch.where(v >= lines[r, 1], 1, 2)) + in_a = seg == 0 + ord_a = torch.cumsum(in_a.int(), 0) + stay_a = in_a & (ord_a <= LIST_SEG_A) + in_b = (seg == 1) | (in_a & ~stay_a) + ord_b = torch.cumsum(in_b.int(), 0) + stay_b = in_b & (ord_b <= LIST_SEG_A) + in_c = (seg == 2) | (in_b & ~stay_b) + ord_c = torch.cumsum(in_c.int(), 0) + stay_c = in_c & (ord_c <= LIST_CAP_C - pads) + slot = torch.full_like(seg, -1) + slot[stay_a] = ord_a[stay_a] - 1 + slot[stay_b] = LIST_SEG_A + (ord_b[stay_b] - 1) + slot[stay_c] = 2 * LIST_SEG_A + (ord_c[stay_c] - 1) + if pads: + # sentinels displace C entries later in emission order: the + # kept ordinals shift by how many sentinels landed before them + pad_slots = torch.randperm(int(stay_c.sum()) + pads, device=dev)[:pads] + keep = slot[stay_c] - 2 * LIST_SEG_A + shift = (pad_slots[None, :] <= keep[:, None]).sum(-1) + slot[stay_c] = 2 * LIST_SEG_A + keep + shift + live = slot >= 0 + cand_vals[r, slot[live].long()] = v[live] + cand_idx[r, slot[live].long()] = perm[live].int() + cand_ctl[r, 0] = int(hits.numel()) + pads + cand_ctl[r, 1] = 1 if int(in_c.sum()) > LIST_CAP_C - pads else 0 + out_indices = torch.empty(batch, top_k, dtype=torch.int32, device="cuda") + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, + pre_idx, + seq_lens, + out_indices, + top_k=top_k, + cluster_size=1, + seed_thr=seed_row, + cand_vals=cand_vals, + cand_idx=cand_idx, + cand_ctl=cand_ctl, + accept_cap=LIST_SEG_A, + num_threads=512, + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1) + + +@skip_not_sm100 +@pytest.mark.parametrize("tail_mode", ["exact", "pad_inf"]) +def test_cute_dsl_gvr_topk_decode_ext_block_max(tail_mode): + """32-grain positional upper-bound records + packed seed row. + + The skip walk may only skip units whose bound clears no line, so it + must be exact under both legal tail bounds: the tight max over valid + positions and the worst legal inflation (+FLT_MAX on a partially + valid record). + """ + top_k, N, batch = 1024, 262144, 2 + seq_lens = torch.tensor([N, N - 37], dtype=torch.int32, device="cuda") + logits, pre_idx, seq_lens = _make_inputs( + batch, N, top_k, torch.float32, 1, seed=13, seq_lens=seq_lens + ) + dev = logits.device + n_eff = seq_lens.to(device=dev, dtype=torch.long) + pos = torch.arange(N, device=dev)[None, :] + masked = torch.where(pos < n_eff[:, None], logits, float("-inf")) + records = masked.view(batch, N // 32, 32).amax(-1) + if tail_mode == "pad_inf": + rec_start = torch.arange(N // 32, device=dev)[None, :] * 32 + partial = (rec_start < n_eff[:, None]) & (rec_start + 32 > n_eff[:, None]) + records = torch.where(partial, torch.full_like(records, _FLT_MAX), records) + records = records.contiguous() + lines = _lines_at_counts(logits, n_eff, (4 * top_k, 2 * top_k, top_k + top_k // 4)) + seed_row = _pack_seed_row(logits, n_eff, lines, block_max=records) + out_indices = torch.empty(batch, top_k, dtype=torch.int32, device="cuda") + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, + pre_idx, + seq_lens, + out_indices, + top_k=top_k, + cluster_size=1, + seed_thr=seed_row, + block_max=records, + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1) From fb9643ece80839c07635cbc4f7d27e9891da550c Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 9 Aug 2026 10:49:54 -0700 Subject: [PATCH 099/117] [None][chore] GVR routing exclusivity, entry guards, comment slimming - rungs tier: the skip prefix and the cluster split are mutually exclusive; the cluster split is decided first and the prefix attaches only when the row keeps cluster_size 1 (the K<=512, n>=196608 overlap previously set both) - merge the two identical weak-band constants into ASSIST_WEAK_MAX_N - kernel ctor rejects p4_no_fine without the exact-tail chain or with the range-test arms, and kNumBins that leave no tail pair buffer - op entry rejects ext tensors with non-fp32 logits or next_n > 1 (the ext tiers are compiled for fp32 single-token rows only) - pure-tie precheck: the fine-bin recompute is compiled out under p4_no_fine, matching the scatter - slim narrative comments across the GVR files (verified equivalent by docstring-stripped AST compare); fix stale docs (p4_tail_fast default claim, the tail repair description, the FP4 runner emit-meta shapes); debug envs renamed under the TRTLLM_GVR_ prefix Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../_torch/custom_ops/cute_dsl_custom_ops.py | 54 ++-- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 280 +++++++----------- .../blackwell/top_k/gvr_routing.py | 110 ++----- .../cute_dsl_kernels/top_k/run_gvr_topk.py | 66 ++--- 4 files changed, 176 insertions(+), 334 deletions(-) diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 293b2276449c..0ca818b3b6f5 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -7430,8 +7430,7 @@ def _compile( order_row_fake = (cute.runtime.make_fake_compact_tensor( cutlass.Int32, (n_batch, ), stride_order=(0, )) if seqlen_sorted else None) - # emission-assisted tiers (list/counts/rungs, see - # gvr_routing): fake shapes mirror the harness wrapper + # emission-assisted tier fake tensors (list/counts/rungs) block_max_fake = (cute.runtime.make_fake_compact_tensor( cutlass.Float32, (n_rows, cute.sym_int()), stride_order=(1, 0), @@ -7482,8 +7481,8 @@ def _compile( cand_cap=cand_cap, accept_cap=accept_cap, kc_override=kc_override, - # ext modes need 3 rung slots (M_thr == 3); the qfrac - # VALUES are irrelevant (P1b skipped), only slot count + # ext modes need 3 rung slots (M_thr == 3); only the + # slot count matters, not the qfrac values (P1b skipped) r0_qfracs=((0.85, 0.35) if (use_ext_counts or ext_rungs) else None), ) @@ -7768,6 +7767,9 @@ def forward( or enable_block_skip): assert not lb_mode and order_row is None, ( "ext tiers are single-CTA/sort-path only") + assert logits.dtype == torch.float32 and next_n == 1, ( + "ext tiers are compiled for fp32 logits and next_n==1; " + f"got dtype={logits.dtype} next_n={next_n}") if num_threads is not None: tuning = dict(tuning, num_threads_per_block=num_threads) elif use_ext_cand and top_k <= 512: @@ -9039,10 +9041,8 @@ def _compile(cls, seed_counts_fake = None if emit_seed_counts: if seed_packed: - # single [rows, 8] fp32 packed seed row: lines at - # cols 0..2 (kernel reads (row, j<=2) unchanged), - # counts accumulate as fp32 at cols 3..5; the same - # tensor is bound to both params. + # [rows, 8] fp32 packed seed row: lines at cols 0..2, + # counts at cols 3..5; same tensor bound to both params. seed_thr_fake = cute.runtime.make_fake_compact_tensor( cutlass.Float32, (cute.sym_int(), 8), stride_order=(1, 0), @@ -9097,11 +9097,8 @@ def _compile(cls, sm_fake, cutlass.Int32(1), cutlass.Int32(1), - # stream sits before the emission tensors in __call__ - - # upstream's positional order, with the emission slots - # appended after it. Keep this list in that order: the - # runtime call below drops the stream (the TVM FFI env - # stream is used) and is otherwise the same sequence. + # keep __call__'s argument order: stream, then emission + # slots (the runtime call drops the stream, same sequence) fake_stream, block_max_fake, hit_stats_fake, @@ -9164,10 +9161,12 @@ def forward( output_dtype: output logits dtype emit_block_meta: also emit per-128-block metadata for the fused GVR top-k. Requires ``hit_bitmap`` - [B, nb_pad*4] int32 (1 bit per compressed kv position, - request-level). ``block_max_out``/``hit_stats_out`` - ([B*next_n, nb_pad] fp32 / [B*next_n, nb_pad, 4] fp32) - are allocated when not supplied. + [B, >= nb_pad*4] int32 (1 bit per compressed kv + position, request-level). ``block_max_out`` + [B*next_n, nb_pad*4] fp32 (4 warp-partial records per + block) is allocated when not supplied; + ``hit_stats_out`` [B*next_n, 4] fp32 must be supplied + when ``emit_hit_stats`` is set. Returns: logits [B*next_n, max_context_len]; with emit_block_meta, the tuple (logits, block_max, hit_stats). @@ -9265,10 +9264,9 @@ def forward( "emit_seed_counts requires emit_block_meta") if seed_counts_out is None: # Packed contract: seed_thr IS the [rows, 8] fp32 seed - # row (top-k pre-packed layout). Lines at cols 0..2; - # counts accumulate as fp32 at cols 3..5 - the caller - # zeroes cols 3..7 and writes lines each step. The - # same buffer then feeds the top-k launch directly. + # row; lines at cols 0..2, counts accumulate as fp32 at + # cols 3..5. Caller zeroes cols 3..7 and writes lines + # each step. seed_packed = True assert ( seed_thr is not None and seed_thr.dtype == torch.float32 @@ -9325,7 +9323,7 @@ def forward( elif emit_cand_bucketed: assert emit_seed_counts, ( "emit_cand_bucketed requires emit_seed_counts") - # SoA v5 contract: cand_out = fp32 VALUES [rows, 2*segA+capC], + # SoA contract: cand_out = fp32 VALUES [rows, 2*segA+capC], # cand_idx_out = int32 positions (same width), cand_cur_out = # int32 [rows, 4] cursors (caller-zeroed), cand_ctl_out = # int32 [rows, 4] {n0, void, n1, n2} (caller-zeroed) @@ -9407,11 +9405,8 @@ def forward( None, None, None, None, None, None, None, None) return logits - # NOTE: the optional emission tensors ARE written by the kernel but - # deliberately not in mutates_args - torch.library's in-place - # bookkeeping IndexErrors on optional mutates left at their None - # default (same precedent as heuristic_scratch on - # trtllm::indexer_topk_decode). + # NOTE: the optional emission tensors ARE written by the kernel but must + # stay out of mutates_args (torch.library IndexErrors on None defaults). @torch.library.custom_op("trtllm::cute_dsl_fp4_paged_mqa_logits", mutates_args=(), device_types="cuda") @@ -9484,9 +9479,8 @@ def cute_dsl_fp4_paged_mqa_logits( cand_idx_out=cand_idx_out, cand_ctl_out=cand_ctl_out, cand_cur_out=cand_cur_out) - # with emission on, the runner returns (logits, block_max, - # hit_stats) - the emission buffers are caller-owned mutates, - # the op face stays logits-only + # with emission on the runner returns a tuple; the op returns + # logits only (emission buffers are caller-owned) return ret[0] if isinstance(ret, tuple) else ret @torch.library.register_fake("trtllm::cute_dsl_fp4_paged_mqa_logits") diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index dd587afc330c..b4aac136177b 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -64,21 +64,16 @@ # form is also accepted by older wrappers, so keep it version-independent. _RND_RN = "rn" -# Reduction identities for the emit_block_meta epilogue path; match the GVR -# kernel's FLT_MAX/NEG_FLT_MAX sentinels (gvr_topk_decode.py) so the fused -# Phase 1's degenerate checks behave identically to the gather path. +# Reduction identities for emit_block_meta; must match the GVR kernel's +# FLT_MAX/NEG_FLT_MAX sentinels (gvr_topk_decode.py). _META_FLT_MAX = 3.4028235e38 _META_NEG_FLT_MAX = -3.4028235e38 -# --------------------------------------------------------------------------- # Global-memory reductions for the per-row hit aggregate (emit_hit_stats). -# fp32 has no native atomic min/max; we use the standard order-preserving -# int encoding enc(f) = bits(f) >= 0 ? bits(f) : bits(f) ^ 0x7FFFFFFF -# (an involution; signed-int order == float order, -0.0 quirk harmless for -# min/max seeding) and red.global.{min,max}.s32. Sum uses red.global.add.f32 -# (order-nondeterministic — perturbs only the heuristic mean seed). -# --------------------------------------------------------------------------- +# fp32 min/max use the order-preserving int encoding +# enc(f) = bits(f) >= 0 ? bits(f) : bits(f) ^ 0x7FFFFFFF (an involution) +# with red.global.{min,max}.s32; sum uses red.global.add.f32. @dsl_user_op def _red_global_fmin_ordered(addr_i64, fval, *, loc=None, ip=None): llvm.inline_asm( @@ -519,84 +514,69 @@ def __init__( # one contiguous LSU phase after the for-t loop (epilogue micro-opt). self.use_batched_store = use_batched_store # When True, the epilogue additionally emits per-128-token-block - # metadata consumed by the fused GVR top-k (gvr_topk_decode.py — - # fused_preidx_stats / enable_block_skip). Emission is fully - # WARP-AUTONOMOUS: each of the WG's 4 warps writes one partial - # record per tile per t (record index = tile*4 + warp); the GVR - # consumer folds the 4 partials per block. No cross-warp barrier — - # a per-tile named-barrier fold costs +53% indexer wall-clock. - # block_max [num_rows, nb_pad*4] fp32 — warp-partial max of - # f32(stored logit) over valid positions (kv_pos < ctx); - # fold(4) is the lossless block-skip upper bound. Computed on - # the POST-conversion value so it bounds what GVR reads back, - # bit-exactly. - # hit_agg [num_rows, 4] fp32 — PER-ROW aggregate + # metadata consumed by the fused GVR top-k (gvr_topk_decode.py). + # Emission is warp-autonomous: each of the WG's 4 warps writes one + # partial record per tile per t (record index = tile*4 + warp); + # the GVR consumer folds the 4 partials per block. No cross-warp + # barrier. + # block_max [num_rows, nb_pad*4] fp32 — warp-partial max of + # f32(stored logit) over valid positions (kv_pos < ctx), + # computed on the POST-conversion value so it bounds what GVR + # reads back bit-exactly. + # hit_agg [num_rows, 4] fp32 — per-row aggregate # {enc_min, enc_max, sum, cnt} of stored logits at positions # flagged in hit_bitmap; min/max slots hold the # order-preserving int encoding (see _red_global_fmin_ordered). # Buffer must be pre-initialized to - # {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} per step. Replaces GVR - # Phase 1's random gather. - # hit_bitmap [batch, nb_pad*4] int32 — 1 bit per kv - # position (request-level; from the previous step's top-k). - # Cost per tile per t: 1 bitmap LDG/thread + 1 warp redux + 1 - # lane-0 STG (block_max) + <= 4 LANE-LOCAL accumulator ops (hit - # stats); the hit accumulators flush ONCE per q-transition via - # atomics (_flush_hit_agg). A per-tile warp-redux emission of the - # hit stats was tried first and cost +72% indexer wall-clock. + # {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} per step. + # hit_bitmap [batch, nb_pad*4] int32 — 1 bit per kv position + # (request-level; from the previous step's top-k). + # Hit accumulators are lane-local and flush once per q-transition + # via atomics (_flush_hit_agg). # # emit_hit_stats sub-knob (only meaningful with emit_block_meta): - # False emits block_max ONLY (no bitmap read, no hit aggregate) — - # sufficient for GVR block-skip without the fused Phase 1. + # False emits block_max ONLY (no bitmap read, no hit aggregate). self.emit_block_meta = emit_block_meta self.emit_hit_stats = emit_hit_stats - # emit_seed_counts (L1 of the epilogue suite; requires - # emit_block_meta): per row, count stored logits >= each of the - # T=3 caller-provided thresholds (cross-step seed thresholds from - # the GVR xstate). Lane-local accumulation across tiles + one - # warp-redux + lane-0 red.global.add.s32 per threshold per row - # transition — the hit-stats cost profile. Counts are computed on - # the POST-conversION value over valid positions only, matching - # the top-k consumer's verification semantics - # (workspace/epilogue_topk_interface.md). + # emit_seed_counts (requires emit_block_meta): per row, count + # stored logits >= each of the T=3 caller-provided thresholds. + # Counts are computed on the POST-conversion value over valid + # positions only. if emit_seed_counts and not emit_block_meta: raise ValueError("emit_seed_counts requires emit_block_meta") self.emit_seed_counts = emit_seed_counts # seed_packed: single [num_rows, 8] fp32 seed row per the top-k - # pre-packed contract - lines at cols 0..2, counts ACCUMULATED AS - # FLOATS at cols 3..5 (exact to 2^24; red.global.add.f32). The - # caller zeroes cols 3..7 and writes the lines each step; the - # same buffer feeds the top-k launch with no host repack. + # pre-packed contract - lines at cols 0..2, counts accumulated as + # floats at cols 3..5 (exact to 2^24; red.global.add.f32). The + # caller zeroes cols 3..7 and writes the lines each step. if seed_packed and not emit_seed_counts: raise ValueError("seed_packed requires emit_seed_counts") self.seed_packed = seed_packed - # emit_cand (L2 of the epilogue suite): unordered pre-collect of all - # (value, index) pairs >= the t_0 seed threshold via warp ballot + - # lane0 batch atomic claim. claimed >= K certifies the candidate - # set covers the true top-K (contract in epilogue_topk_interface.md). + # emit_cand: unordered pre-collect of all (value, index) pairs >= + # the t_0 seed threshold. claimed >= K certifies the candidate set + # covers the true top-K. if emit_cand and not emit_seed_counts: raise ValueError("emit_cand requires emit_seed_counts (t_0 source)") self.emit_cand = emit_cand self.cand_cap = cand_cap - # emit_cand_bucketed (v5 list contract): three fixed SoA segments - # (A=[0,segA) holds >= t2, B=[segA,2segA) holds [t1,t2), C= - # [2segA,2segA+capC) holds [t0,t1)); a full segment spills to the - # next looser one. A/B use EXACT ballot claims (their prefixes - # must stay pad-free - the consumer's prefix math assumes it), C - # keeps the claim-window scheme (pads are legal there). Cursors - # live in caller-zeroed cand_cur [rows,4]; ctl [rows,4] carries - # {n0 incl C pads, void, n1, n2} with n1/n2 flushed from the L1 - # seed counters. + # emit_cand_bucketed: three fixed SoA segments (A=[0,segA) holds + # >= t2, B=[segA,2segA) holds [t1,t2), C=[2segA,2segA+capC) holds + # [t0,t1)); a full segment spills to the next looser one. A/B use + # EXACT ballot claims (their prefixes must stay pad-free - the + # consumer's prefix math assumes it), C keeps the claim-window + # scheme (pads are legal there). Cursors live in caller-zeroed + # cand_cur [rows,4]; ctl [rows,4] carries {n0 incl C pads, void, + # n1, n2} with n1/n2 flushed from the seed counters. if emit_cand_bucketed and not emit_seed_counts: raise ValueError("emit_cand_bucketed requires emit_seed_counts") if emit_cand_bucketed and emit_cand: raise ValueError("emit_cand_bucketed and emit_cand are exclusive") self.emit_cand_bucketed = emit_cand_bucketed self.accept_cap = accept_cap - # Per-warp claim window: one atomic claims (hits + CAND_WIN) slots; - # subsequent hits consume the window latency-free. The unconsumed - # tail is sentinel-filled (idx = -1) at q-transition/loop end, so - # `claimed` over-approximates the true count (counts[r][0] exact). + # Per-warp claim window: one atomic claims (hits + CAND_WIN) slots. + # The unconsumed tail is sentinel-filled (idx = -1) at + # q-transition/loop end, so `claimed` over-approximates the true + # count (counts[r][0] exact). self.CAND_WIN = 8 # epi_bytes covers fp16 and bf16 (FP8 only handled fp16). self.epi_bytes = 2 if epi_dtype in (cutlass.Float16, cutlass.BFloat16) else 4 @@ -819,8 +799,8 @@ def __call__( num_phys_blocks: cutlass.Int32, batch_size: cutlass.Int32, stream: cuda.CUstream, - # everything below is emission-only and defaulted, so the - # positional signature stays the one callers already use + # emission-only tensors; defaulted so the positional signature + # stays the one callers already use block_max: cute.Tensor = None, # [num_rows, nb_pad*4] fp32 warp-partials hit_stats: cute.Tensor = None, # [num_rows, 4] fp32 (emit_hit_stats) hit_bitmap: cute.Tensor = None, # [batch, nb_pad*4] int32 (emit_block_meta) @@ -1073,10 +1053,8 @@ def _flush_hit_agg( """Warp-reduce the per-lane hit accumulators and merge them into the per-row global aggregate via one set of atomics (encoded-int min/max + fp32 adds), then reset to identities. Called once per - q-transition per warp — atomic traffic is negligible. The - aggregate buffer must be pre-initialized to - {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} per step (prototype: test - harness; production: folded into the bitmap prepare kernel).""" + q-transition per warp. The aggregate buffer must be pre-initialized + to {enc(+FLT_MAX), enc(-FLT_MAX), 0, 0} per step.""" next_n = cutlass.const_expr(self.next_n) base_addr = mHitAgg.iterator.toint() for t in cutlass.range_constexpr(next_n): @@ -1133,8 +1111,7 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None, ca _red_global_add_s32(ctl_a, w_cnt) scnt[t * 3 + j] = cutlass.Int32(0) if cutlass.const_expr(self.seed_packed and spass is not None): - # packed col 6: adaptive-skip pass count (lane0-accumulated, - # so the warp redux is exactly the warp's record total) + # packed col 6: adaptive-skip pass count (lane0-accumulated) for t in cutlass.range_constexpr(next_n): w_bp = cute.arch.warp_redux_sync(spass[t], "add") if meta_lane == cutlass.Int32(0): @@ -1148,7 +1125,7 @@ def _flush_seed_counts(self, mSeedCounts, q_idx, scnt, meta_lane, spass=None, ca @cute.jit def _flush_cand_window_bucketed(self, mCand, mCandIdx, q_idx, cwbase, cwleft, meta_lane): """Sentinel-fill the unconsumed C-window tail in BOTH SoA columns - (score -inf, idx -1: the v5 consumer pads by score) and invalidate + (score -inf, idx -1: the consumer pads by score) and invalidate the window. Segment C sits at base 2*segA in each row.""" next_n = cutlass.const_expr(self.next_n) segA_f = cutlass.const_expr(self.accept_cap) @@ -1486,12 +1463,9 @@ def kernel( layout=sf_kv_smem_layout_staged, byte_alignment=128, ) - # Block-meta emission is fully warp-autonomous (each warp's lane 0 - # writes its own warp-partial record straight to GMEM; the GVR - # side folds 4 partials per block) — no SMEM scratch, no named - # barrier. A cross-warp SMEM fold + per-tile named barrier was - # tried first and cost +53% indexer wall-clock by serializing the - # epilogue's warp pipelining. + # Block-meta emission is warp-autonomous: each warp's lane 0 writes + # its own warp-partial record straight to GMEM; the GVR side folds + # 4 partials per block. No SMEM scratch, no named barrier. a_mcast_mask = cpasync.create_tma_multicast_mask( cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2 @@ -2214,18 +2188,14 @@ def kernel( else: # fp32, 4-byte weights MAX_NUM_W_IN_REG = 56 if next_n == 3 else 64 if cutlass.const_expr(self.emit_block_meta): - # Free ~8 registers for the meta accumulators/fragments - # — the epilogue's weight cache is tuned to the spill - # edge (see MAX_NUM_W_IN_REG SASS notes above). + # Free ~8 registers for the meta accumulators/fragments; + # the epilogue's weight cache sits at the spill edge. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 if cutlass.const_expr(self.emit_hit_stats): # Hit accumulators + bitmap word add ~6 more live # registers across the tile loop. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 - # emit_seed_counts: no budget cut — ncu shows the cost - # is epilogue ALU/issue, not registers (occupancy and - # block limits identical with or without a -8 cut; the - # cut itself measured neutral-to-slower). + # emit_seed_counts needs no extra budget cut. NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -2256,10 +2226,9 @@ def kernel( cwbase[_i] = cutlass.Int32(0) cwleft[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_hit_stats): - # Per-lane hit accumulators, carried across ALL + # Per-lane hit accumulators, carried across all # tiles of the same q and flushed once per - # q-transition — zero warp-wide ops per tile (the - # per-tile redux version cost +72% wall-clock). + # q-transition — no warp-wide ops per tile. hacc_min = cute.make_fragment(next_n, cutlass.Float32) hacc_max = cute.make_fragment(next_n, cutlass.Float32) hacc_sum = cute.make_fragment(next_n, cutlass.Float32) @@ -2269,13 +2238,10 @@ def kernel( hacc_max[_t] = cutlass.Float32(_META_NEG_FLT_MAX) hacc_sum[_t] = cutlass.Float32(0.0) hacc_cnt[_t] = cutlass.Int32(0) - # Batched bitmap read state: a per-tile LDG's - # ~300cy L2 trip lands on the critical path every - # tile (isolated at +42% kernel time; one-ahead - # prefetch did not help). All 32 lanes of a warp - # need the SAME word per tile, so instead lane l - # loads the word for tile j+l once per 32 tiles - # and each tile takes its word via one shuffle. + # Batched bitmap read state: all 32 lanes of a warp + # need the SAME word per tile, so lane l loads the + # word for tile j+l once per 32 tiles and each tile + # takes its word via one shuffle. meta_j = cutlass.Int32(0) hitw_batch = cutlass.Int32(0) @@ -2559,18 +2525,15 @@ def kernel( mLogits[(out_row, kv_pos)] = stored_t if cutlass.const_expr(self.emit_block_meta): # Meta reduction on the POST-conversion value so - # block_max bounds what GVR reads back bit-exactly - # (pre-conversion fp32 max could round up past a - # stored logit's converted value's block max). + # block_max bounds what GVR reads back bit-exactly. f32_t = cutlass.Float32(stored_t) bmax_v = cutlass.Float32(_META_NEG_FLT_MAX) if meta_valid: bmax_v = f32_t r_bmax = cute.arch.warp_redux_sync(bmax_v, "fmax") # Warp-autonomous store: record index = - # tile*4 + warp; the GVR consumer folds the 4 - # warp-partials per block (fold of partials == - # block stat — associative + identity-padded). + # tile*4 + warp; the GVR consumer folds the + # 4 warp-partials per block. if meta_lane == cutlass.Int32(0): out_row_m = q_idx * next_n + t rec_m = meta_kv_tile * cutlass.Int32(4) + meta_warp @@ -2588,26 +2551,20 @@ def kernel( # adaptive-skip pass count: one record # per (tile, warp); r_bmax is warp- # uniform so lane0 alone accumulates - # (the flush redux then sums warps) if meta_lane == cutlass.Int32(0): spass[t] = spass[t] + cutlass.Int32( r_bmax >= sthr[t * 3 + 0] ) if cutlass.const_expr(self.emit_cand): - # L2 pre-collect at t_0 with per-warp claim - # WINDOWS: refills claim (hits + CAND_WIN) - # slots in ONE atomic; between refills the - # warp consumes its window latency-free (a - # per-hit atomic round-trip stalled the - # epilogue 2x at 131k ctx). Unconsumed tail - # is sentinel-filled on flush; counts[r][0] - # stays the exact count. Gated on the - # already-computed warp-uniform 32-position - # bound: r_bmax < t_0 proves zero hits, so - # the common no-hit iteration costs ONE fp - # compare (no ballot, no select). bound >= - # t_0 guarantees a nonzero ballot (exact - # per-lane max, invalid -> -FLT_MAX). + # Candidate pre-collect at t_0 with per-warp + # claim windows: one atomic claims + # (hits + CAND_WIN) slots per refill. + # Unconsumed tail is sentinel-filled on + # flush; counts[r][0] stays the exact count. + # Gated on the warp-uniform 32-position + # bound: r_bmax < t_0 proves zero hits; + # bound >= t_0 guarantees a nonzero ballot + # (exact per-lane max, invalid -> -FLT_MAX). if r_bmax >= sthr[t * 3 + 0]: pred_c = cutlass.Int32(0) if meta_valid: @@ -2687,7 +2644,7 @@ def kernel( cwbase[t] = cwbase[t] + cnt_c cwleft[t] = cwleft[t] - cnt_c if cutlass.const_expr(self.emit_cand_bucketed): - # v5 bucketed SoA: A/B EXACT ballot claims + # Bucketed SoA: A/B EXACT ballot claims # (their prefixes must stay pad-free for # the consumer's prefix math), C keeps the # claim-window; a full segment spills to @@ -2901,14 +2858,12 @@ def kernel( cwbase[t] = cwbase[t] + cntC_k cwleft[t] = cwleft[t] - cntC_k if cutlass.const_expr(self.emit_hit_stats): - # Lane-local accumulation, fully BRANCHLESS - # (data-dependent `if meta_hit` compiled to - # real divergent branches whose condition - # waits on the bitmap LDG — 43% of all warp - # stall samples). Bit-mask select is also - # NaN-safe for OOB-tile garbage logits. No - # valid-mask needed: the bitmap contract - # only sets bits inside [0, ctx). + # Lane-local accumulation; keep it branchless + # (`if meta_hit` compiles to real divergent + # branches). Bit-mask select is NaN-safe for + # OOB-tile garbage logits. No valid-mask + # needed: the bitmap contract only sets bits + # inside [0, ctx). meta_hit = ( hit_word >> (kv_pos & cutlass.Int32(31)) ) & cutlass.Int32(1) @@ -3007,18 +2962,14 @@ def kernel( else: MAX_NUM_W_IN_REG = 56 if next_n == 3 else 64 if cutlass.const_expr(self.emit_block_meta): - # Free ~8 registers for the meta accumulators/fragments - # — the epilogue's weight cache is tuned to the spill - # edge (see MAX_NUM_W_IN_REG SASS notes above). + # Free ~8 registers for the meta accumulators/fragments; + # the epilogue's weight cache sits at the spill edge. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 if cutlass.const_expr(self.emit_hit_stats): # Hit accumulators + bitmap word add ~6 more live # registers across the tile loop. MAX_NUM_W_IN_REG = MAX_NUM_W_IN_REG - 8 - # emit_seed_counts: no budget cut — ncu shows the cost - # is epilogue ALU/issue, not registers (occupancy and - # block limits identical with or without a -8 cut; the - # cut itself measured neutral-to-slower). + # emit_seed_counts needs no extra budget cut. NUM_W_IN_REG = min(MAX_NUM_W_IN_REG, num_heads) w_cache = cute.make_fragment(NUM_W_IN_REG * next_n, self.epi_dtype) # Batched STG: hold reduced result per t in register; the @@ -3049,10 +3000,9 @@ def kernel( cwbase[_i] = cutlass.Int32(0) cwleft[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_hit_stats): - # Per-lane hit accumulators, carried across ALL + # Per-lane hit accumulators, carried across all # tiles of the same q and flushed once per - # q-transition — zero warp-wide ops per tile (the - # per-tile redux version cost +72% wall-clock). + # q-transition — no warp-wide ops per tile. hacc_min = cute.make_fragment(next_n, cutlass.Float32) hacc_max = cute.make_fragment(next_n, cutlass.Float32) hacc_sum = cute.make_fragment(next_n, cutlass.Float32) @@ -3062,13 +3012,10 @@ def kernel( hacc_max[_t] = cutlass.Float32(_META_NEG_FLT_MAX) hacc_sum[_t] = cutlass.Float32(0.0) hacc_cnt[_t] = cutlass.Int32(0) - # Batched bitmap read state: a per-tile LDG's - # ~300cy L2 trip lands on the critical path every - # tile (isolated at +42% kernel time; one-ahead - # prefetch did not help). All 32 lanes of a warp - # need the SAME word per tile, so instead lane l - # loads the word for tile j+l once per 32 tiles - # and each tile takes its word via one shuffle. + # Batched bitmap read state: all 32 lanes of a warp + # need the SAME word per tile, so lane l loads the + # word for tile j+l once per 32 tiles and each tile + # takes its word via one shuffle. meta_j = cutlass.Int32(0) hitw_batch = cutlass.Int32(0) @@ -3345,18 +3292,15 @@ def kernel( mLogits[(out_row, kv_pos)] = stored_t if cutlass.const_expr(self.emit_block_meta): # Meta reduction on the POST-conversion value so - # block_max bounds what GVR reads back bit-exactly - # (pre-conversion fp32 max could round up past a - # stored logit's converted value's block max). + # block_max bounds what GVR reads back bit-exactly. f32_t = cutlass.Float32(stored_t) bmax_v = cutlass.Float32(_META_NEG_FLT_MAX) if meta_valid: bmax_v = f32_t r_bmax = cute.arch.warp_redux_sync(bmax_v, "fmax") # Warp-autonomous store: record index = - # tile*4 + warp; the GVR consumer folds the 4 - # warp-partials per block (fold of partials == - # block stat — associative + identity-padded). + # tile*4 + warp; the GVR consumer folds the + # 4 warp-partials per block. if meta_lane == cutlass.Int32(0): out_row_m = q_idx * next_n + t rec_m = meta_kv_tile * cutlass.Int32(4) + meta_warp @@ -3374,26 +3318,20 @@ def kernel( # adaptive-skip pass count: one record # per (tile, warp); r_bmax is warp- # uniform so lane0 alone accumulates - # (the flush redux then sums warps) if meta_lane == cutlass.Int32(0): spass[t] = spass[t] + cutlass.Int32( r_bmax >= sthr[t * 3 + 0] ) if cutlass.const_expr(self.emit_cand): - # L2 pre-collect at t_0 with per-warp claim - # WINDOWS: refills claim (hits + CAND_WIN) - # slots in ONE atomic; between refills the - # warp consumes its window latency-free (a - # per-hit atomic round-trip stalled the - # epilogue 2x at 131k ctx). Unconsumed tail - # is sentinel-filled on flush; counts[r][0] - # stays the exact count. Gated on the - # already-computed warp-uniform 32-position - # bound: r_bmax < t_0 proves zero hits, so - # the common no-hit iteration costs ONE fp - # compare (no ballot, no select). bound >= - # t_0 guarantees a nonzero ballot (exact - # per-lane max, invalid -> -FLT_MAX). + # Candidate pre-collect at t_0 with per-warp + # claim windows: one atomic claims + # (hits + CAND_WIN) slots per refill. + # Unconsumed tail is sentinel-filled on + # flush; counts[r][0] stays the exact count. + # Gated on the warp-uniform 32-position + # bound: r_bmax < t_0 proves zero hits; + # bound >= t_0 guarantees a nonzero ballot + # (exact per-lane max, invalid -> -FLT_MAX). if r_bmax >= sthr[t * 3 + 0]: pred_c = cutlass.Int32(0) if meta_valid: @@ -3473,7 +3411,7 @@ def kernel( cwbase[t] = cwbase[t] + cnt_c cwleft[t] = cwleft[t] - cnt_c if cutlass.const_expr(self.emit_cand_bucketed): - # v5 bucketed SoA: A/B EXACT ballot claims + # Bucketed SoA: A/B EXACT ballot claims # (their prefixes must stay pad-free for # the consumer's prefix math), C keeps the # claim-window; a full segment spills to @@ -3687,14 +3625,12 @@ def kernel( cwbase[t] = cwbase[t] + cntC_k cwleft[t] = cwleft[t] - cntC_k if cutlass.const_expr(self.emit_hit_stats): - # Lane-local accumulation, fully BRANCHLESS - # (data-dependent `if meta_hit` compiled to - # real divergent branches whose condition - # waits on the bitmap LDG — 43% of all warp - # stall samples). Bit-mask select is also - # NaN-safe for OOB-tile garbage logits. No - # valid-mask needed: the bitmap contract - # only sets bits inside [0, ctx). + # Lane-local accumulation; keep it branchless + # (`if meta_hit` compiles to real divergent + # branches). Bit-mask select is NaN-safe for + # OOB-tile garbage logits. No valid-mask + # needed: the bitmap contract only sets bits + # inside [0, ctx). meta_hit = ( hit_word >> (kv_pos & cutlass.Int32(31)) ) & cutlass.Int32(1) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py index ed081c5e7c84..2bc3992e0b15 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_routing.py @@ -30,104 +30,51 @@ NEXT step (the emission tax is shape-dependent); ``pick_config`` maps (tier, B, N, K) to concrete launch knobs for THIS step. -All thresholds are measured on B200 (f15 layer-complete grid, -2026-07-27, cold-L2 kernel-only protocol, validated on both the shared -grid dataset and first-party captures). They are deployment defaults, -not universal truths - keep them in one place so retuning is a -constant edit, not a logic edit. +All thresholds are B200 deployment defaults, kept in one place so +retuning is a constant edit, not a logic edit. """ from dataclasses import dataclass from typing import Optional -# ---- measured thresholds (B200) ------------------------------------------ +# ---- thresholds (B200 deployment defaults) -------------------------------- # -# UNITS: every threshold here is fitted on KERNEL-ONLY time for both the -# indexer and the top-k. An earlier fit compared a wall-clock indexer -# (which carries a ~22us launch floor) against kernel-only top-k times; -# that inflated the apparent emission budget at small shapes and is what -# put the counts tier behind a batch gate it never needed. - -# The kernel's fixed cost does not shrink with N, so once the stock -# kernel finishes under that floor no tier can win. +# UNITS: every threshold is fitted on KERNEL-ONLY time for both the +# indexer and the top-k; do not mix in wall-clock numbers when retuning. + +# Below this the stock kernel already runs at the fixed-cost floor. ASSIST_MIN_N_COMP = 2048 -# Block-skip prefix pays for the counts tier from 65536 up, but not for -# the zero-emission rungs tier below 131072. Measured on captured V4 -# rows (5 layers x all decode steps, nsys kernel-only, flash n_comp -# 65537): counts 16.1us with the prefix vs 17.3 without at batch -# 4/8/16, rungs 18.5 with vs 17.9 without. A synthetic-Gaussian A/B put -# the counts break-even a doubling later - the block-max distribution -# of real rows decides the pass rate, so set this from captures only. +# Block-skip prefix break-evens. Retune these from real captures only: +# the real block-max distribution decides the pass rate. SKIP_MIN_N_COUNTS = 65536 # va: attach block_max from here up SKIP_MIN_N_RUNGS_FLASH = 131072 # vb (flash): bm pays from here -# Cluster split is a loss for the assist tiers below this point, -# block_max or not: at n_comp 65537 rungs measures 17.9 / 19.0 / 20.4us -# at cs 1 / 4 / 8 (batch 4) and cs8 spills to 31.5us at batch 16. SKIP_CS_MIN_N_RUNGS = 196608 # vb: cluster split from here up -# Emission cost measured on the FP4 indexer KERNEL (nsys kernel-only, -# ABBA-interleaved NVTX blocks, batch 1..64 x ctx 32k..512k) as the delta -# of the same kernel with the emission outputs attached: -# -# batch 1 2 4 8 16 64 -# counts (us) +5.6 +4.4 +2.9 +2.0 +0.4 0.0 -# list, bucketed +6.2 +6.2 +6.8 +10.5 +25.0 +63.1 (ctx 32k) -# +10.3 +14.7 +25.6 +44.5 +85.7 +417.8 (ctx 512k) -# -# The counts cost is a fixed reduction-latency chain that depends on -# BATCH ONLY and hides once there are enough rows to overlap it - it is -# not a fraction of the indexer, so it cannot price the tier out at -# scale. An earlier fit modelled it as 3% of a WALL-clock indexer time -# (which carries a ~22us launch floor) and so charged 12us at batch 64 / -# 512k, where the true cost is zero; that is what put the counts tier -# behind a batch gate. The list cost is real per-emitted-entry work and -# grows with batch and context alike. Parking the two tight lines above -# the score range (see gvr_ext.LIST_PARK_LINE) drops it 2.7-5x - every -# entry then lands in the one segment that claims through a per-warp -# window instead of an exact ballot - at top-k parity, which is what -# makes the list tier affordable past a single row. +# Emission-cost model: counts emission is a batch-only latency chain +# (hides at large batch); list emission grows with batch and context. LIST_EMIT_MIN_N = 65536 # shorter rows: the emission outweighs the saving LIST_EMIT_MAX_B = 4 # past four rows the list stops repaying its emission -COUNTS_MIN_TOKENS = 524288 # B * raw length; below this the counts -# latency chain is exposed and the zero-emission rungs tier wins +COUNTS_MIN_TOKENS = 524288 # B * raw length; below this the rungs tier wins RUNGS_ONLY_MIN_N = 16384 # short-row band where rungs also beats counts RUNGS_ONLY_MAX_N = 49152 -# Mid-row weak band: rows long enough that the stock kernel splits them -# across a cluster, but short enough (and at a small enough batch) that -# its split grid still fits one wave. The assist tiers cannot follow - -# splitting a row costs them more than the scan it saves, with or without -# the block-skip prefix - so the stock kernel wins outright and the -# epilogue should emit nothing. Narrowed after phase 4 got its coarse -# search and its boundary-class repair back: the tiers gained about -# 1.5us there, which is enough to take the upper half of the band back -# off the stock kernel (2 cells fall through now, down from 4). +# Mid-row weak band: the stock kernel's split grid fits one wave here +# and out-scans every assist tier, so the epilogue emits nothing. ASSIST_WEAK_MIN_N = 49152 -# Narrowed again from 98304 to 65536: at n_comp 65536 the band's premise no -# longer holds. Measured on the full grid (kernel-only, us/step), the stock -# kernel is NOT the fastest arm there - counts beats it by 2.19 (flash) and -# 2.38 (pro), and the counts emission at that batch costs only +0.27us on the -# indexer (re-measured after the tight-line parking; the old table charged -# +2.0us at batch 8, which is what kept the band this wide). Net +1.9 to -# +2.1us per step, i.e. those cells go 1.11 -> ~1.31 against the baseline. -# The interior of the band (49152 <= n_comp < 65536) is untouched: no grid -# unit lands there, so it stays on the stock kernel until someone measures it. -ASSIST_WEAK_MAX_N_SMALL_K = 65536 # k <= ASSIST_WEAK_K -ASSIST_WEAK_MAX_N_LARGE_K = 65536 -ASSIST_WEAK_K = 512 +# The band interior is unmeasured; it stays on the stock kernel. +ASSIST_WEAK_MAX_N = 65536 ASSIST_WEAK_MAX_B = 8 -# rungs-tier block_max pays only at small K: with K=1024 the tight-line -# pass rate runs too high and the prefix read is pure overhead. +# rungs block_max pays only at small K; at large K the prefix is overhead. RUNGS_BM_MAX_K = 512 # 512-thread build wins for list-hit rows at small K (work is O(list)). SMALL_K_LIST_THREADS = 512 SMALL_K_MAX = 512 -# GPC packing: cs=8 only while all row-clusters fit half the device -# (B=16 x cs8 wave-spill regression); cs4/2 keep a 10% headroom. +# GPC packing: cs=8 only while all row-clusters fit half the device; +# cs4/2 keep a 10% headroom. CS8_HALF_DEVICE = 2 CS_HEADROOM_NUM = 9 CS_HEADROOM_DEN = 10 @@ -153,16 +100,12 @@ def plan_emission( matching buffers and the next top-k launch routes on them. """ if n_comp < ASSIST_MIN_N_COMP: - # short rows: the stock kernel is already under our fixed cost, - # and this holds for the zero-emission rungs tier too - it is - # the same kernel, so the floor is the same + # short rows: the stock kernel is already under the fixed cost return "none" if have_epilogue and n_comp >= LIST_EMIT_MIN_N and batch <= LIST_EMIT_MAX_B: - # checked before the weak band below: that band is about the stock - # kernel out-scanning us, and a list hit never scans the row + # must stay ahead of the weak-band gate: a list hit never scans the row return "list" - weak_max = ASSIST_WEAK_MAX_N_SMALL_K if k <= ASSIST_WEAK_K else ASSIST_WEAK_MAX_N_LARGE_K - if batch <= ASSIST_WEAK_MAX_B and ASSIST_WEAK_MIN_N <= n_comp < weak_max: + if batch <= ASSIST_WEAK_MAX_B and ASSIST_WEAK_MIN_N <= n_comp < ASSIST_WEAK_MAX_N: return "none" # stock's split grid wins this band outright if ( have_epilogue @@ -181,16 +124,13 @@ def pick_config(tier: str, batch: int, n_comp: int, k: int, num_sms: int) -> Top if tier == "list": if k <= SMALL_K_MAX: r.num_threads = SMALL_K_LIST_THREADS - # list + block_max: miss rows fall back to a skip-walk instead - # of a dense re-scan (measured -19% on pro long chains). + # block_max: miss rows take a skip-walk instead of a dense re-scan r.attach_block_max = n_comp >= SKIP_MIN_N_COUNTS return r if tier == "counts": r.attach_block_max = n_comp >= SKIP_MIN_N_COUNTS return r - # rungs (vb) - if k <= RUNGS_BM_MAX_K and n_comp >= SKIP_MIN_N_RUNGS_FLASH: - r.attach_block_max = True + # rungs (vb); the skip prefix and cluster split are mutually exclusive if n_comp >= SKIP_CS_MIN_N_RUNGS: if batch * 8 <= num_sms // CS8_HALF_DEVICE: r.cluster_size = 8 @@ -198,4 +138,6 @@ def pick_config(tier: str, batch: int, n_comp: int, k: int, num_sms: int) -> Top r.cluster_size = 4 elif batch * 2 <= (num_sms * CS_HEADROOM_NUM) // CS_HEADROOM_DEN: r.cluster_size = 2 + if r.cluster_size == 1 and k <= RUNGS_BM_MAX_K and n_comp >= SKIP_MIN_N_RUNGS_FLASH: + r.attach_block_max = True return r diff --git a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py index 45891a0f3a05..041706d6fab5 100644 --- a/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py +++ b/tests/scripts/cute_dsl_kernels/top_k/run_gvr_topk.py @@ -230,9 +230,8 @@ def _compile( kc_override=kc_override, self_scan=self_scan, cap_c=cap_c, - # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed. The - # qfrac VALUES are irrelevant on this path (P1b is skipped) — only - # the slot count matters. + # ext counts need 3 rung slots (M_thr == 3): 2 qfracs + vseed; + # the qfrac values are unused here — only the slot count matters. r0_qfracs=(0.85, 0.35) if (use_ext_counts or ext_rungs) else None, ) return cute.compile( @@ -289,15 +288,11 @@ def emu_block_max( "pad_inf": a partially-valid tail unit is forced to +FLT_MAX, the worst legal inflation (the indexer masks by request-level ctx >= N_eff, so tail positions can inflate the bound). - Tests use this to prove inflated bounds only *disable* - skipping, never break correctness. records: "rotate": fold-correctness fixture — the 128-block max lands in ONE slot rotated by blk % 4, the other 3 hold - -FLT_MAX. Any 4 values folding to the block max are - legal; the rotation makes a consumer that drops or - mis-reads partial slots fail the tests. Valid ONLY - for grain-128 consumers (slots are NOT positional). + -FLT_MAX. Valid ONLY for grain-128 consumers (slots + are NOT positional). "positional": production semantics — record r is the exact max of positions [r*32, r*32+32) (the indexer's TMEM T2R partition gives warp w of a tile the contiguous @@ -338,9 +333,7 @@ def emu_block_max( # Rung offsets below each 32-position record's max for the meta-seed -# metadata (L6_geo.25 — offline-validated: 1.00 real scans on synth + -# real captures for both V4 models with the kernel's S=16 x 2-pass -# quantized search). +# metadata. _META_DELTAS = (0.25, 0.5, 1.0, 2.0, 4.0, 8.0) @@ -359,8 +352,6 @@ def emu_block_meta( upper bound 32). Positions beyond the row's effective length are excluded. Layout matches ``block_max``: ``[num_rows, nrec]`` int32 with ``nrec = ceil(N/128)*4`` (one record per 32 positions). - On the indexer side this is ~L ballots+popcounts per record in the - epilogue, same shape as the +1.3% block_max emission. """ assert next_n == 1, "emu_block_meta: next_n == 1 only" x = logits.float() @@ -394,8 +385,7 @@ def derive_seed_rungs( Estimates the per-row local slope of log2(count) vs threshold from the PREVIOUS step's 3 rung measurements and places the next step's guard rungs ``count_octaves`` octaves away from the mid rung (= the previous - accepted threshold). Real-chain validation (V4-Pro/Flash captures): - in-band admission 0.958/0.975 vs 0.82-0.97 for any fixed spread. + accepted threshold). Args: prev_thr: [rows] previous accepted threshold (xstate[:, 2]). @@ -418,10 +408,8 @@ def derive_seed_rungs( count_octaves / slope.clamp(min=0.05), torch.full_like(slope, fallback_spread), ).clamp(0.1, 4.0) - # undershoot hysteresis: a row whose PREVIOUS loose rung caught - # fewer than K cannot use its list (fallback ~26us); widen only - # that row's next down-guard by +2 octaves. Rows in band keep the - # tight spread (a fat list taxes every step's walk). + # undershoot hysteresis: a row whose previous loose rung caught + # fewer than K widens only its next down-guard by +2 octaves. oct_lo = torch.full_like(slope, count_octaves) if top_k is not None: oct_lo = torch.where(prev_counts[:, 0].float() < float(top_k), oct_lo + 2.0, oct_lo) @@ -770,17 +758,12 @@ def gvr_topk_decode( cute_dtype = _DTYPE_TORCH_TO_CUTE[logits.dtype] num_rows = logits.shape[0] - # Host dispatch gate: the compact walk only wins when the per-row - # compressed length is large (protocol: >= 2x at N=262k, 4-14% LOSS at - # N <= 131k). Below skip_min_n (compressed-index space, shape-based so - # no device sync) drop block_max and run the dense arms. None disables - # the gate (A/B probes). + # Host dispatch gate: below skip_min_n (compressed-index space, + # shape-based so no device sync) drop block_max; None disables the gate. if block_max is not None and skip_min_n is not None and logits.shape[1] < skip_min_n: block_max = None - # K > 512 at tiny batch: the acceptance band is proportionally tighter - # (kC/K = 6 vs 10), the bounds prune less, and the row-split configs - # win outright (cold protocol, pro 262k BS1: skip 21.3-21.6us at - # cs1/cs8 vs stock cs8 19.7us) -> keep the stock path. + # K > 512 at tiny batch: drop block_max (stock path) unless the admitted + # line is known up front (ext counts / packed seed / self_scan). if ( block_max is not None and num_rows < 8 @@ -789,20 +772,12 @@ def gvr_topk_decode( and not (seed_thr is not None and seed_counts is not None) and not (seed_thr is not None and seed_thr.shape[1] >= 6) ): - # Stock-path small-batch gate, tuned in the loose-rung era (the - # sample-quantile list kept 60%+ of the blocks at K>512). It does - # NOT apply when the admitted line is known up front: ext counts - # park the tight line in every rung slot, so the list forms at - # the accepted threshold and skips by the real band density - # (pro-1M: 12.7% pass -> the list is live again). self_scan owns - # its own skip economics likewise. block_max = None if self_scan: # fused self-contained mode: kernel scans/buckets the row itself. # Inputs: seed_thr (three closed-loop lines) + a write-only gmem - # POSITION column passed through the cand_idx slot. seed_counts is - # a dummy (the ext-counts preview reads it but zeros never pass - # the [K, kC] admission, so routing is owned by the phase-0 gate). + # POSITION column passed through the cand_idx slot; seed_counts is + # a dummy (zeros never pass the [K, kC] admission). assert seed_thr is not None, "self_scan requires seed_thr" assert cand_vals is None and cand_ctl is None, ( "self_scan excludes external candidate values/control" @@ -906,17 +881,12 @@ def gvr_topk_decode( N_dec = max_seq_len if max_seq_len is not None else N_cols if num_threads_per_block is None: if use_ext_cand and top_k <= 512: - # list-hit rows do O(list) work (~2-4K entries at K<=512), - # not O(N): the N-keyed 1024-thread pick pays barrier-heavy - # phases for no parallel gain (flash-512k v5: 8.4 -> 7.1us - # at 512 threads; K=1024 lists are big enough to keep the - # N-keyed pick). + # list-hit rows do O(list) work, not O(N): use 512 threads + # (K=1024 lists are big enough to keep the N-keyed pick). num_threads_per_block = 512 elif self_scan: - # self_scan owns the whole row scan in one CTA: the phase-0 - # cp.async pipeline scales with warp count at every N (the - # 512-thread short-row heuristic below is tuned for the - # stock multi-pass kernel and costs ~5us/cell here). + # self_scan scans the whole row in one CTA; the phase-0 + # cp.async pipeline scales with warp count at every N. num_threads_per_block = 1024 else: if max_seq_len is not None and logits.dtype != torch.float32: From 1ec6f2f640e208044c75005b7250772fec02322a Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:27:40 -0700 Subject: [PATCH 100/117] [None][test] split the heavy CuTe DSL files out of the B200 attention entry The bare directory entry ran the whole attention tree under the default 3600s wrapper timeout. The GVR decode file alone runs ~90 minutes, so the wrapper died mid-file on DGX_B200 (both the wrapper item and the in-flight case report 'Test terminated unexpectedly'). Mirror the B300 layout: --ignore the three CuTe DSL files in the directory entry and list them separately, GVR decode with TIMEOUT (120). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- tests/integration/test_lists/test-db/l0_b200.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_lists/test-db/l0_b200.yml b/tests/integration/test_lists/test-db/l0_b200.yml index b3a07d3b526b..5d9a1e0b8435 100644 --- a/tests/integration/test_lists/test-db/l0_b200.yml +++ b/tests/integration/test_lists/test-db/l0_b200.yml @@ -72,7 +72,10 @@ l0_b200: - test_e2e.py::test_ptp_quickstart_advanced_ngram[Llama-3.1-8B-Instruct-llama-3.1-model/Llama-3.1-8B-Instruct] - test_e2e.py::test_trtllm_bench_pytorch_backend_sanity[meta-llama/Llama-3.1-8B-llama-3.1-8b-False-False] - test_e2e.py::test_openai_chat_guided_decoding[openai/gpt-oss-120b] - - unittest/_torch/attention + - unittest/_torch/attention --ignore=unittest/_torch/attention/sparse/test_cute_dsl_fp8_paged_mqa_logits.py --ignore=unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py --ignore=unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py + - unittest/_torch/attention/sparse/test_cute_dsl_fp8_paged_mqa_logits.py + - unittest/_torch/attention/sparse/test_cute_dsl_fp4_paged_mqa_logits.py + - unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py TIMEOUT (120) - unittest/_torch/compilation - unittest/_torch/debugger - unittest/_torch/kv_cache_compression From 06207209c26724a5da46571bd82c52e43061dbeb Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:07:53 -0700 Subject: [PATCH 101/117] [None][fix] address GVR emission review findings Review round 2026-08-11 (brnguyen2, longcheng-nv), all inline findings: - list tier line-cut: the mapped-prefix copy re-measures the non-sentinel candidate count in flight and demotes to the stock path below K - a pad-inflated claim can no longer admit a starved list. Regression test ext_list[starved]. - update_seed_rows: slope-fit of log2(count) vs threshold from the previous step's lines+counts (harness derive_seed_lines_v4 construction) at K-relative target counts; list tier two-point-fits through the published exact k-th; no-fit rows take multiplicative guards (~9% of kth) instead of the degenerate 1e-4 span. Chained test ext_closed_loop (3 steps, kth drift 0.05, all three tiers). - rungs tier reachable: persistent contiguous [rows, 3] seed mirror passed for the rungs route; counts telemetry from the kernel's xstate[4..6] publish. - one shared per-step gate (batch <= 256, next_n == 1) for planning, emission and consumption; batch > 256 pays no emission tax and a stale route cannot be consumed. - prefill->decode handoff seeds prev_topk and zeroes xstate for new requests (same slot convention as heuristic_prev_topk); positional identity assumption documented at the consumption site. - seed width assert == 3 or == 8 with matching use_ext_counts derivation; ext tiers require enable_r0; list-capacity ctor reject; trace-time flag/tensor contract errors; block-skip single-band void contract; cand_ctl width comments; SKIP_MAX_BLOCKS budget comment; radix_lens comment reconciled with metadata (declared latent-bug fix); mutates_args limitation re-verified on the pinned torch (IndexError when a declared-mutable Optional is None at call) and documented precisely in both op docstrings. - rename per review naming feedback: TRTLLM_GVR_EXT -> TRTLLM_GVR_EMISSION, gvr_ext.py -> gvr_emission.py, GvrExtState -> GvrEmissionState. Validation on B200: full decode unit file 730 passed / 0 failed / 144 skipped / 1 xfailed; new ext_list[starved] and 3-tier ext_closed_loop chained tests pass; mutates_args declaration attempt reverted after uniform stock-path failures (None-case IndexError). Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../attention_backend/sparse/dsa/indexer.py | 100 ++++++++++++------ .../sparse/{gvr_ext.py => gvr_emission.py} | 98 +++++++++++++---- .../_torch/custom_ops/cute_dsl_custom_ops.py | 28 ++++- .../blackwell/top_k/gvr_topk_decode.py | 58 ++++++++-- .../sparse/test_cute_dsl_gvr_topk_decode.py | 91 +++++++++++++++- 5 files changed, 306 insertions(+), 69 deletions(-) rename tensorrt_llm/_torch/attention_backend/sparse/{gvr_ext.py => gvr_emission.py} (64%) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py index f3e0c32ca9e3..34c04ef3dcc2 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py @@ -684,14 +684,14 @@ def __init__( sparse_params.use_cute_dsl_paged_mqa_logits and IS_CUTLASS_DSL_AVAILABLE ) # GVR emission-assisted decode (opt-in, experimental): the FP4 indexer - # epilogue emits candidates for the top-k (see gvr_ext / gvr_routing). - self.use_gvr_ext = ( - os.environ.get("TRTLLM_GVR_EXT", "0") == "1" + # epilogue emits candidates for the top-k (see gvr_emission / gvr_routing). + self.use_gvr_emission = ( + os.environ.get("TRTLLM_GVR_EMISSION", "0") == "1" and self.use_cute_dsl_topk and self.use_cute_dsl_paged_mqa_logits and self.use_fp4 ) - self._gvr_ext = None # lazy GvrExtState (first decode step) + self._gvr_emission = None # lazy GvrEmissionState (first decode step) self._gvr_route = None self.weight_scale_factor = self.softmax_scale * self.n_heads**-0.5 @@ -1342,6 +1342,24 @@ def _call_paged_mqa_logits( max_seq_len, ) + def _ensure_gvr_emission(self, metadata: DSAtrtllmAttentionMetadata, device: torch.device): + """Lazy per-layer GVR ext state (see gvr_emission.GvrEmissionState).""" + if self._gvr_emission is None: + from ..gvr_emission import LIST_EMIT_MIN_N, GvrEmissionState + + # get_indexer_max_seq_len already returns the compressed length + n_comp = metadata.get_indexer_max_seq_len() + self._gvr_emission = GvrEmissionState( + max_rows=metadata.max_num_sequences, + top_k=self.index_topk, + device=device, + # the list tier is only reachable at n_comp >= LIST_EMIT_MIN_N; + # skip its large candidate buffers when the engine's static + # length cannot get there + enable_list_tier=n_comp >= LIST_EMIT_MIN_N, + ) + return self._gvr_emission + def sparse_attn_indexer( self, metadata: DSAtrtllmAttentionMetadata, @@ -1589,14 +1607,29 @@ def sparse_attn_indexer( # bottom of the decode block): new gens from finishing prefill # append after currently-active gens, i.e., slots # [num_generations : num_generations + num_contexts]. - if self._enable_heuristic_topk and has_prefill and not metadata.skip_indexer_for_ctx_reqs: - local_layer = metadata.kv_cache_manager.layer_offsets[self.layer_idx] + if ( + has_prefill + and not metadata.skip_indexer_for_ctx_reqs + and (self._enable_heuristic_topk or self.use_gvr_emission) + ): ctx_seq_lens = metadata.seq_lens[:num_contexts] # Per-sequence last context-token offset (exclusive cumsum minus 1). last_ctx_idx = (torch.cumsum(ctx_seq_lens, dim=0) - 1).to(dtype=torch.long) - metadata.heuristic_prev_topk[ - local_layer, num_generations : num_generations + num_contexts - ].copy_(topk_indices_buffer[last_ctx_idx, :]) + last_ctx_topk = topk_indices_buffer[last_ctx_idx, :] + if self._enable_heuristic_topk: + local_layer = metadata.kv_cache_manager.layer_offsets[self.layer_idx] + metadata.heuristic_prev_topk[ + local_layer, num_generations : num_generations + num_contexts + ].copy_(last_ctx_topk) + if self.use_gvr_emission: + # Ext state is positional like heuristic_prev_topk (no churn + # signal exists to key on): zeroed xstate cold-starts new gens + # onto the stock path; stale rows after churn only mis-place + # lines - counts are re-measured in-kernel, so exactness holds. + st = self._ensure_gvr_emission(metadata, topk_indices_buffer.device) + new_gens = slice(num_generations, num_generations + num_contexts) + st.prev_topk[new_gens].copy_(last_ctx_topk[:, : st.prev_topk.shape[1]]) + st.xstate[new_gens].zero_() reuse_topk = ( self.mtp_index_share @@ -1610,6 +1643,7 @@ def sparse_attn_indexer( metadata.shared_topk_indices[:num_generations, :] ) elif has_decode and not metadata.skip_indexer_for_gen_reqs: + gvr_step_ok = False # Get decode lengths per request (from seq_lens) for validation gen_seq_lens = metadata.seq_lens[num_contexts : num_contexts + num_generations] max_decode_len = gen_seq_lens.max().item() @@ -1713,32 +1747,29 @@ def sparse_attn_indexer( dsl_schedule_meta = metadata.scheduler_metadata_buffer_expanded gvr_emit_kwargs = {} - if self.use_gvr_ext and next_n == 1 and not dsl_atom_split: - from ..gvr_ext import LIST_EMIT_MIN_N, GvrExtState - + # one step gate shared with the consume branch below: + # emitting for a step the top-k cannot consume only + # churns the closed-loop state + gvr_step_ok = ( + self.use_gvr_emission + and use_custom_topk + and next_n == 1 + and not dsl_atom_split + and num_gen_tokens <= 256 + ) + if gvr_step_ok: + st = self._ensure_gvr_emission(metadata, q_fp8.device) # indexer_max_seq_len is already the compressed length # (get_indexer_max_seq_len divides); do not divide again. - n_comp = indexer_max_seq_len - if self._gvr_ext is None: - self._gvr_ext = GvrExtState( - max_rows=metadata.max_num_sequences, - top_k=self.index_topk, - device=q_fp8.device, - # the list tier is only reachable at - # n_comp >= LIST_EMIT_MIN_N; skip its large - # candidate buffers when the engine's static - # length cannot get there - enable_list_tier=n_comp >= LIST_EMIT_MIN_N, - ) - st = self._gvr_ext emit_tier, self._gvr_route = st.plan( batch_size, - n_comp, + indexer_max_seq_len, torch.cuda.get_device_properties(q_fp8.device).multi_processor_count, compress_ratio=max(self.compress_ratio, 1), ) - if emit_tier in ("counts", "list"): + if emit_tier in ("counts", "list", "rungs"): st.update_seed_rows(batch_size, emit_tier) + if emit_tier in ("counts", "list"): gvr_emit_kwargs = st.indexer_emit_kwargs(emit_tier, batch_size) if self._gvr_route.attach_block_max or emit_tier in ( "counts", @@ -1829,16 +1860,15 @@ def sparse_attn_indexer( # tier "none" (see gvr_routing) falls through to the stock # branch; the untouched ext state reads as a cold start there. if ( - self.use_gvr_ext - and self._gvr_ext is not None + self.use_gvr_emission + and gvr_step_ok + and self._gvr_emission is not None and self._gvr_route is not None and self._gvr_route.tier != "none" - and next_n == 1 - and num_gen_tokens <= 256 ): # emission-assisted GVR: consume what the indexer epilogue # emitted this step (seed row / list / block_max per route) - st = self._gvr_ext + st = self._gvr_emission out_slice = topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] # GVR op takes RAW-domain seq_lens (kernel ceil-divides by # compress_ratio); on the DSL path the live compressed lens @@ -1879,8 +1909,10 @@ def sparse_attn_indexer( order_row=metadata.kv_lens_row_reorder, ) elif self.use_cute_dsl_topk and (self.compress_ratio == 1 or next_n == 1): - # seq_lens must be 1-D; on the DSL path the live compressed - # lens are gen_indexer_kv_lens_cuda_runtime (2d buf is zero) + # latent-bug fix: the op takes 1-D request-level lens, but + # context_lens is the 2-D kv_lens_cuda_2d slice; on the + # FP4-DSL path the live compressed lens are + # gen_indexer_kv_lens_cuda_runtime if self.compress_ratio > 1: radix_lens = ( metadata.gen_indexer_kv_lens_cuda_runtime diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py similarity index 64% rename from tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py rename to tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py index 034b2d3a212f..770ce37647dd 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_ext.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py @@ -27,6 +27,7 @@ step. """ +import math from typing import Optional import torch @@ -44,10 +45,21 @@ LIST_CAP_C = 24576 LIST_WIDTH = 2 * LIST_SEG_A + LIST_CAP_C -# Closed-loop lines around the published k-th anchor: t1 hugs the k-th -# value from below; t0/t2 guard by the (anchor - kth) span. -__all__ = ["GvrExtState", "LIST_EMIT_MIN_N", "LIST_PARK_LINE"] - +__all__ = ["GvrEmissionState", "LIST_EMIT_MIN_N", "LIST_PARK_LINE"] + +# Closed-loop line placement: fit the slope of log2(count) vs threshold +# from the previous step's (lines, counts) and place the new lines at +# these K-relative target counts (t0 loosest .. t2 tightest). +LINE_TARGETS = (8.0, 5.0, 2.0) +LIST_T0_TARGET = 2.5 # list tier: single collect-line target (xK) +LIST_T0_COUNT_MAX = 6144.0 # keep n0 inside the [K+64, segA] admission band +SLOPE_MIN = 0.05 +SLOPE_MAX = 64.0 + +# No-fit fallback: multiplicative guards around the published k-th value +# (t1 hugs it from below; t0/t2 guard by GUARD_LO/GUARD_HI spans). +FALLBACK_REL = 2.0**0.125 - 1.0 +FALLBACK_ABS = 1e-3 GUARD_LO = 2.0 GUARD_HI = 0.5 @@ -58,7 +70,7 @@ LIST_PARK_LINE = 1.0e30 -class GvrExtState: +class GvrEmissionState: """Per-attention-backend emission state (persistent buffers).""" def __init__( @@ -69,6 +81,9 @@ def __init__( # packed seed row: lines at cols 0..2, counts (emission-filled) # at 3..5, adaptive-skip pass count at 6 self.seed_row = torch.zeros((max_rows, 8), dtype=torch.float32, device=device) + # contiguous alias of the three lines for the rungs tier (a + # [rows, 3] column view of the packed row is non-contiguous) + self.seed_rungs = torch.zeros((max_rows, 3), dtype=torch.float32, device=device) self.xstate = torch.zeros((max_rows, 8), dtype=torch.float32, device=device) self.cand_vals: Optional[torch.Tensor] = None self.cand_idx: Optional[torch.Tensor] = None @@ -95,7 +110,7 @@ def ensure_block_max(self, max_seq_len: int) -> torch.Tensor: # address into the graph, so fail loudly instead if torch.cuda.is_current_stream_capturing(): raise RuntimeError( - "GvrExtState.ensure_block_max: (re)allocation requested " + "GvrEmissionState.ensure_block_max: (re)allocation requested " "during CUDA graph capture; the block_max buffer must be " "created by a warmup step before capture" ) @@ -124,26 +139,70 @@ def plan( def update_seed_rows(self, num_rows: int, emit_tier: str = "counts") -> None: """Device-side closed-loop line update from the last publish. - Pure tensor ops (graph-capturable). Rows with invalid xstate - (col 0 == 0, e.g. cold start) get non-finite lines, which the - kernel's validity guard routes to the stock path. + Slope-fits log2(count) vs threshold from the previous step's + (lines, counts) and places the new lines at K-relative target + counts. Counts come from the packed row (counts/list emission) + or from the kernel's rung-count publish in xstate cols 4..6 + (rungs tier). Rows without a usable fit get multiplicative + guards around the published k-th value; rows with invalid + xstate (col 0 == 0, e.g. cold start) get non-finite lines, + which the kernel's validity guard routes to the stock path. + Pure tensor ops (graph-capturable). """ s = self.seed_row[:num_rows] x = self.xstate[:num_rows] - kth = x[:, 1] - anch = torch.maximum(x[:, 2], kth + 1e-5) - span = (anch - kth).clamp_min(1e-4) valid = x[:, 0] > 0 + kth = x[:, 1] + anchor = x[:, 2] + t_prev0 = s[:, 0] + t_prev2 = s[:, 2] + cnts = x[:, 4:7] if emit_tier == "rungs" else s[:, 3:6] + k = float(self.top_k) inf = torch.full_like(kth, float("inf")) - s[:, 0] = torch.where(valid, kth - GUARD_LO * span, inf) + d_fb = kth.abs() * FALLBACK_REL + FALLBACK_ABS if emit_tier == "list": + # two-point fit (t0_prev, n0) / (kth, K): kth is the exact + # k-th boundary on list rows + n0 = cnts[:, 0].clamp_min(1.0) + dthr = (kth - t_prev0).clamp_min(1e-3) + slope = ((torch.log2(n0) - math.log2(k)) / dthr).clamp(SLOPE_MIN, SLOPE_MAX) + tgt0 = min(LIST_T0_TARGET * k, LIST_T0_COUNT_MAX) + t0 = kth - math.log2(tgt0 / k) / slope + fit_ok = torch.isfinite(t_prev0) & (n0 > k) + t0 = torch.where(fit_ok, t0, kth - GUARD_LO * d_fb) park = torch.full_like(kth, LIST_PARK_LINE) - s[:, 1] = torch.where(valid, park, inf) - s[:, 2] = torch.where(valid, park + park, inf) + new0 = torch.where(valid, t0, inf) + new1 = torch.where(valid, park, inf) + new2 = torch.where(valid, park + park, inf) else: - s[:, 1] = torch.where(valid, kth - 1e-6, inf) - s[:, 2] = torch.where(valid, kth + GUARD_HI * span, inf) + c0 = cnts[:, 0].clamp_min(1.0) + c2 = cnts[:, 2].clamp_min(1.0) + dthr = (t_prev2 - t_prev0).clamp_min(1e-3) + slope = ((torch.log2(c0) - torch.log2(c2)) / dthr).clamp(SLOPE_MIN, SLOPE_MAX) + # anchor count estimate: slide the anchor onto the prev line fit + anch_c = (c2 * torch.exp2(-(anchor - t_prev2) * slope)).clamp(1.0, 1e6) + t0 = anchor + torch.log2(anch_c / (LINE_TARGETS[0] * k)) / slope + t1 = anchor + torch.log2(anch_c / (LINE_TARGETS[1] * k)) / slope + t2 = anchor + torch.log2(anch_c / (LINE_TARGETS[2] * k)) / slope + # t_prev2 < 1e29 also rejects a parked line left by a tier flip + fit_ok = torch.isfinite(t_prev0) & (t_prev2 < 1e29) & (c0 > c2) + t0 = torch.where(fit_ok, t0, kth - GUARD_LO * d_fb) + t1 = torch.where(fit_ok, t1, kth - 1e-6) + t2 = torch.where(fit_ok, t2, kth + GUARD_HI * d_fb) + # strictly ascending (kernel line-validity contract) + t1 = torch.maximum(t1, t0 + 1e-4) + t2 = torch.maximum(t2, t1 + 1e-4) + new0 = torch.where(valid, t0, inf) + new1 = torch.where(valid, t1, inf) + new2 = torch.where(valid, t2, inf) + s[:, 0] = new0 + s[:, 1] = new1 + s[:, 2] = new2 s[:, 3:8] = 0.0 + rungs = self.seed_rungs[:num_rows] + rungs[:, 0] = new0 + rungs[:, 1] = new1 + rungs[:, 2] = new2 if self.cand_ctl is not None: self.cand_ctl[:num_rows].zero_() self.cand_cur[:num_rows].zero_() @@ -177,8 +236,9 @@ def topk_ext_kwargs( kw["num_threads"] = route.num_threads if route.tier in ("counts", "list"): kw["seed_thr"] = self.seed_row[:num_rows] - # rungs tier: pass no seed at all (a [rows, 3] column view of the - # packed row is non-contiguous and would trip the runner's assert) + elif route.tier == "rungs": + # [rows, 3] seed selects the op's ext_rungs variant + kw["seed_thr"] = self.seed_rungs[:num_rows] if route.tier == "list": # accept_cap must match the emitter's segment geometry: the # buffers are laid out at bases 0 / LIST_SEG_A / 2*LIST_SEG_A, diff --git a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py index 0ca818b3b6f5..6067a39966dd 100644 --- a/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py +++ b/tensorrt_llm/_torch/custom_ops/cute_dsl_custom_ops.py @@ -7745,11 +7745,11 @@ def forward( # emission-assisted tiers: mode from which ext tensors the # caller handed in (see gvr_routing.plan_emission) if seed_thr is not None: - assert seed_thr.shape[1] == 3 or seed_thr.shape[1] >= 6, ( - "seed_thr width must be 3 (rungs) or >= 6 (packed " - f"counts row); got {seed_thr.shape[1]} - refusing to " - "silently ignore the seed") - use_ext_counts = seed_thr is not None and seed_thr.shape[1] >= 6 + assert seed_thr.shape[1] == 3 or seed_thr.shape[1] == 8, ( + "seed_thr must be [rows, 3] (rungs) or [rows, 8] " + "(packed lines + counts row, the width the kernel " + f"is compiled for); got width {seed_thr.shape[1]}") + use_ext_counts = seed_thr is not None and seed_thr.shape[1] == 8 ext_rungs = seed_thr is not None and seed_thr.shape[1] == 3 use_ext_cand = cand_vals is not None enable_block_skip = block_max is not None @@ -7864,6 +7864,16 @@ def cute_dsl_gvr_topk_decode( max_batch_size: Required with ``counters``; ignored otherwise. Power of 2 in ``[64, 1024]``, must match the value passed to LB prepare. + + Of the optional hint tensors the kernel WRITES ``xstate`` (the + closed-loop publish); it cannot be declared in ``mutates_args``: + torch.library raises IndexError when a declared-mutable Optional + arg is None at call time (re-verified on the pinned torch), and + most calls pass no hints. Under torch.compile/functionalization + the undeclared write is invisible, so the hint path is eager / + CUDA-graph only. ``TRTLLM_GVR_EMISSION=1`` gates the + emission-assisted wiring that feeds these tensors (opt-in, + experimental). """ if not is_sm_100f(): raise ValueError( @@ -9170,6 +9180,14 @@ def forward( Returns: logits [B*next_n, max_context_len]; with emit_block_meta, the tuple (logits, block_max, hit_stats). + + The optional emission tensors (``block_max_out`` / + ``seed_thr`` / ``cand_*``) are written by the kernel but + cannot be declared in ``mutates_args``: torch.library raises + IndexError when a declared-mutable Optional arg is None at + call time (re-verified on the pinned torch), and plain + logits-only calls pass none of them. Emission is therefore + eager / CUDA-graph only. """ B, next_n, H, half_D = q.shape N = next_n * H diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 042d716ae8c8..e99116be9677 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -495,7 +495,9 @@ def __init__( self.enable_block_skip = bool(enable_block_skip) self.SKIP_BLOCK = 32 self.SKIP_BLOCK_LOG2 = 5 - self.SKIP_MAX_BLOCKS = 8192 # covers N up to 262144 at grain 32 + self.SKIP_MAX_BLOCKS = 8192 # smem active-list budget (32KB of local + # ids); bounds block-skip to N_local <= 262144 at grain 32 - longer + # rows run the dense fallback and owe nothing to block-skip self.SKIP_UNROLL = 2 self.skip_order = "grouped" if enable_block_skip and num_threads not in (512, 1024): @@ -692,6 +694,14 @@ def __init__( self.cap_c = 0 if use_ext_cand and not use_ext_counts: raise ValueError("use_ext_cand requires use_ext_counts") + if (use_ext_counts or ext_rungs or use_ext_cand) and not enable_r0: + # the effective flags below are and-ed with enable_r0; reject + # instead of silently compiling the stock path + raise ValueError("ext tiers require enable_r0") + if use_ext_cand and self.list_cap <= 0: + raise ValueError( + f"cand_cap={cand_cap} leaves no C segment past 2*accept_cap={2 * self.accept_cap}" + ) # ext_rungs (two-pass variant B): closed-loop rung THRESHOLDS come # from the host (previous-step xstep lines); the kernel counts them # itself via the stock R0 multi-count and admits the tightest rung @@ -1793,7 +1803,11 @@ def phase0_scan_bucket( # or the fallback (under) - exactly the v5 state machine # fed with n0 == n1 == n2 curT0 = s_seg[0] + # claims past segA were dropped by the walk: honor the + # "void==0 means nothing was dropped" contract s_seg[3] = cutlass.Int32(0) + if curT0 > cutlass.Int32(segA): + s_seg[3] = cutlass.Int32(1) s_seg[4] = curT0 s_seg[5] = curT0 s_seg[6] = curT0 @@ -5399,7 +5413,7 @@ def gvr_topk_kernel( xstate: cute.Tensor, # [numRows, 8] fp32 closed-loop state (or None) cand_vals: cute.Tensor, # [numRows, CAP] fp32 scores (or None) cand_idx: cute.Tensor, # [numRows, CAP] int32 positions (or None) - cand_ctl: cute.Tensor, # [numRows, 2] int32 {claimed, void} (or None) + cand_ctl: cute.Tensor, # [numRows, 4] int32 {n0, void, n1, n2} (or None) ): """Thin entry: bidx → row_idx → run_one_row. @@ -5475,7 +5489,7 @@ def run_one_row( xstate: cute.Tensor = None, # [numRows, 8] fp32 (emit_xstate) cand_vals: cute.Tensor = None, # [numRows, CAP] fp32 (ext cand) cand_idx: cute.Tensor = None, # [numRows, CAP] int32 (ext cand) - cand_ctl: cute.Tensor = None, # [numRows, 2] int32 (ext cand) + cand_ctl: cute.Tensor = None, # [numRows, 4] int32 (ext cand) ): """Dispatch: compute per-row slice + cluster sync mode, call _run_phases. @@ -5532,6 +5546,18 @@ def run_one_row( # Slice per-row views. input_row = input_data[row_idx, None] pre_idx_row = pre_idx[pre_idx_row_idx, None] + # trace-time contract checks: a feature flag without its tensor + # must fail HERE, not as a NoneType subscript deep in the phases + if cutlass.const_expr((self.use_ext_counts or self.ext_rungs) and seed_thr is None): + raise ValueError("use_ext_counts/ext_rungs kernels require seed_thr") + if cutlass.const_expr(self.emit_xstate and xstate is None): + raise ValueError("emit_xstate kernels require xstate") + if cutlass.const_expr( + self.use_ext_cand and (cand_vals is None or cand_idx is None or cand_ctl is None) + ): + raise ValueError("use_ext_cand kernels require cand_vals/cand_idx/cand_ctl") + if cutlass.const_expr(self.enable_block_skip and block_max is None): + raise ValueError("enable_block_skip kernels require block_max") if cutlass.const_expr(self.enable_block_skip and block_max is not None): block_max_row = block_max[row_idx, None] else: @@ -6635,6 +6661,7 @@ def _run_phases( list_used = cutlass.Int32(1) if tidx == cutlass.Int32(0): s_iscalars[0] = cutlass.Int32(0) + s_iscalars[2] = cutlass.Int32(0) # non-sentinel count s_thr[0] = anch_t s_iscalars[1] = cutlass.Int32(1) # done cute.arch.barrier() @@ -6654,6 +6681,7 @@ def _run_phases( # no atomics - every read is a winner candidate. if tidx == cutlass.Int32(0): s_iscalars[0] = cut_n + nreal_c = cutlass.Int32(0) i_c = tidx while i_c < cut_n: for _ju in cutlass.range_constexpr(4): @@ -6683,14 +6711,32 @@ def _run_phases( cute.AddressSpace.gmem, assumed_align=4, ) - smem_vals[j_c] = cute.make_tensor(ip_c, cute.make_layout((1,)))[ - 0 - ] + iv_c = cute.make_tensor(ip_c, cute.make_layout((1,)))[0] + smem_vals[j_c] = iv_c + # sentinel pads carry idx -1; cut_n + # (= claimed n0) counts them, so the + # REAL candidate count must be + # re-measured during the copy + if iv_c >= cutlass.Int32(0): + nreal_c = nreal_c + cutlass.Int32(1) i_c = i_c + cutlass.Int32(4 * num_threads) + wsum_c = self.warp_reduce_sum_i32(nreal_c) + if lane_c == cutlass.Int32(0): + atomicAdd(s_iscalars.iterator + cutlass.Int32(2), wsum_c) wmax_w = self.warp_reduce_max_f32(wmax_acc) if lane_c == cutlass.Int32(0): smem_wcnt[tidx // cutlass.Int32(32)] = float_as_uint32(wmax_w) cute.arch.barrier() + # demote when the pad-inflated claim admitted a + # list that holds fewer than K real candidates; + # the stock path below recovers exactly + real_c = s_iscalars[2] + if real_c < cutlass.Int32(self.top_k): + take_cand = cutlass.Int32(0) + list_used = cutlass.Int32(0) + if tidx == cutlass.Int32(0): + s_iscalars[1] = cutlass.Int32(0) + cute.arch.barrier() if line_cut == cutlass.Int32(0): # ---- histogram-edge cut: value-filtered mapped # walk with merged-ballot claims (float edges diff --git a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py index 95ccff38ee87..5c557c5a032a 100644 --- a/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py +++ b/tests/unittest/_torch/attention/sparse/test_cute_dsl_gvr_topk_decode.py @@ -1283,7 +1283,7 @@ def test_cute_dsl_gvr_topk_decode_tie_flood_beyond_capacity(): # --------------------------------------------------------------------------- # Emission-assisted (ext) tiers: packed seed row / candidate list / block max. # Inputs emulate the indexer epilogue host-side against the layout contracts -# in ``gvr_ext`` (segments at bases 0 / LIST_SEG_A / 2*LIST_SEG_A, packed row +# in ``gvr_emission`` (segments at bases 0 / LIST_SEG_A / 2*LIST_SEG_A, packed row # = lines at [0..2] + exact counts at [3..5] + skip pass count at [6]). # --------------------------------------------------------------------------- @@ -1365,7 +1365,7 @@ def test_cute_dsl_gvr_topk_decode_ext_counts(top_k, mode): @skip_not_sm100 -@pytest.mark.parametrize("mode", ["hit", "pads", "hist", "void", "bucketed"]) +@pytest.mark.parametrize("mode", ["hit", "pads", "hist", "void", "bucketed", "starved"]) def test_cute_dsl_gvr_topk_decode_ext_list(mode): """Candidate-list tier at the production geometry (accept_cap = LIST_SEG_A, width = LIST_WIDTH). @@ -1379,8 +1379,11 @@ def test_cute_dsl_gvr_topk_decode_ext_list(mode): void: collection overflows LIST_CAP_C -> void=1 -> full scan. bucketed: three live lines spread across segments A/B/C, cut at the tightest line inside the band. + starved: fewer than K real candidates, but sentinel pads lift the + claim into the admission band -> the line-cut copy must + re-measure and demote (exactness regression). """ - from tensorrt_llm._torch.attention_backend.sparse.gvr_ext import ( + from tensorrt_llm._torch.attention_backend.sparse.gvr_emission import ( LIST_CAP_C, LIST_PARK_LINE, LIST_SEG_A, @@ -1396,7 +1399,7 @@ def test_cute_dsl_gvr_topk_decode_ext_list(mode): if mode == "bucketed": lines = _lines_at_counts(logits, n_eff, (20000, 4000, 600)) else: - n0 = {"hit": 4096, "pads": 4096, "hist": 12000, "void": 30000}[mode] + n0 = {"hit": 4096, "pads": 4096, "hist": 12000, "void": 30000, "starved": 400}[mode] l0 = _lines_at_counts(logits, n_eff, (n0,)) lines = torch.cat( [l0, torch.full_like(l0, LIST_PARK_LINE), torch.full_like(l0, 2 * LIST_PARK_LINE)], 1 @@ -1405,7 +1408,7 @@ def test_cute_dsl_gvr_topk_decode_ext_list(mode): cand_vals = torch.full((batch, LIST_WIDTH), float("-inf"), dtype=torch.float32, device=dev) cand_idx = torch.full((batch, LIST_WIDTH), -1, dtype=torch.int32, device=dev) cand_ctl = torch.zeros((batch, 4), dtype=torch.int32, device=dev) - pads = 64 if mode == "pads" else 0 + pads = {"pads": 64, "starved": 200}.get(mode, 0) for r in range(batch): ne = int(n_eff[r]) row = logits[r, :ne] @@ -1501,3 +1504,81 @@ def test_cute_dsl_gvr_topk_decode_ext_block_max(tail_mode): ) torch.cuda.synchronize() _tie_aware_check(out_indices, logits, seq_lens, top_k, 1) + + +def _emulate_emission(logits, n_eff, st, tier, top_k): + """Host-side stand-in for the indexer epilogue: fill the packed-row + counts (and the candidate list on the list tier) against the CURRENT + seed lines, exactly as the production emitter would.""" + from tensorrt_llm._torch.attention_backend.sparse.gvr_emission import LIST_CAP_C, LIST_SEG_A + + batch, N = logits.shape + dev = logits.device + lines = st.seed_row[:batch, 0:3] + pos = torch.arange(N, device=dev)[None, :] + valid = pos < n_eff[:, None] + finite = torch.isfinite(lines[:, 0]) + counts = torch.stack( + [((logits >= lines[:, j : j + 1]) & valid).sum(-1) for j in range(3)], 1 + ).float() + st.seed_row[:batch, 3:6] = torch.where(finite[:, None], counts, torch.zeros_like(counts)) + if tier == "list" and st.cand_vals is not None: + st.cand_vals[:batch].fill_(float("-inf")) + st.cand_idx[:batch].fill_(-1) + st.cand_ctl[:batch].zero_() + for r in range(batch): + if not bool(finite[r]): + continue + ne = int(n_eff[r]) + row = logits[r, :ne] + hits = torch.nonzero(row >= lines[r, 0], as_tuple=False).flatten() + cnt = int(hits.numel()) + nwr = min(cnt, LIST_CAP_C) + base = 2 * LIST_SEG_A + st.cand_idx[r, base : base + nwr] = hits[:nwr].int() + st.cand_vals[r, base : base + nwr] = row[hits[:nwr]] + st.cand_ctl[r, 0] = cnt + st.cand_ctl[r, 1] = 1 if cnt > LIST_CAP_C else 0 + + +@skip_not_sm100 +@pytest.mark.parametrize( + "tier_shape", [("list", 2, 131072), ("counts", 8, 131072), ("rungs", 1, 32768)] +) +def test_cute_dsl_gvr_topk_decode_ext_closed_loop(tier_shape): + """Chained multi-step closed loop: kernel xstate publish -> + update_seed_rows -> next step's emission and admission. + + Step 2's logits shift the k-th value far beyond any fixed guard + width, so line placement must come from the fitted slope; every step + must stay exact regardless of which internal path admission picks. + """ + from tensorrt_llm._torch.attention_backend.sparse.gvr_emission import GvrEmissionState + + want_tier, batch, N = tier_shape + top_k = 512 + num_sms = torch.cuda.get_device_properties(0).multi_processor_count + logits0, pre_idx, seq_lens = _make_inputs( + batch, N, top_k, torch.float32, 1, seed=17, varlen=False + ) + dev = logits0.device + n_eff = seq_lens.to(device=dev, dtype=torch.long) + st = GvrEmissionState(max_rows=batch, top_k=top_k, device=dev, enable_list_tier=True) + # k-th drift per step: step1 -> step2 rises by ~0.05 (>> any fixed + # guard), step3 falls back near step1's level + shifts = (0.0, 0.05, -0.03) + for step, shift in enumerate(shifts): + logits = (logits0 + shift).contiguous() + tier, route = st.plan(batch, N, num_sms, compress_ratio=1) + assert tier == want_tier + st.update_seed_rows(batch, tier) + _emulate_emission(logits, n_eff, st, tier, top_k) + pre = torch.topk(logits.float(), top_k, dim=-1).indices.int().contiguous() + out_indices = torch.empty(batch, top_k, dtype=torch.int32, device="cuda") + kw = st.topk_ext_kwargs(route, batch, None) + torch.ops.trtllm.cute_dsl_gvr_topk_decode( + logits, pre, seq_lens, out_indices, top_k=top_k, **kw + ) + torch.cuda.synchronize() + _tie_aware_check(out_indices, logits, seq_lens, top_k, 1) + assert bool((st.xstate[:batch, 0] > 0).all().item()), f"step {step}: publish missing" From c48cacf424789fac2e43cb222f626f693fbfcc48 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 11 Aug 2026 04:10:43 -0700 Subject: [PATCH 102/117] [None][chore] commit the GVR per-step perf grid driver (provenance) Per review: the driver behind the PR's performance numbers, in-tree. Per-step paired protocol (one NVTX range per arm/model/isl/B/layer, cold reps cycle all decode steps), nsys wrapper, per-step extraction from the sqlite, pairing + production-routing aggregation into the B x N mean/min tables. Capture root and output dir parameterized via GVR_CAP_ROOT / GVR_BENCH_OUT. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../cute_dsl_kernels/top_k/perf/README.md | 19 + .../cute_dsl_kernels/top_k/perf/ab_steps.py | 403 ++++++++++++++++++ .../cute_dsl_kernels/top_k/perf/f58_an.py | 93 ++++ .../cute_dsl_kernels/top_k/perf/f58_grid.sh | 29 ++ .../cute_dsl_kernels/top_k/perf/perstep.py | 75 ++++ 5 files changed, 619 insertions(+) create mode 100644 tests/scripts/cute_dsl_kernels/top_k/perf/README.md create mode 100644 tests/scripts/cute_dsl_kernels/top_k/perf/ab_steps.py create mode 100644 tests/scripts/cute_dsl_kernels/top_k/perf/f58_an.py create mode 100755 tests/scripts/cute_dsl_kernels/top_k/perf/f58_grid.sh create mode 100644 tests/scripts/cute_dsl_kernels/top_k/perf/perstep.py diff --git a/tests/scripts/cute_dsl_kernels/top_k/perf/README.md b/tests/scripts/cute_dsl_kernels/top_k/perf/README.md new file mode 100644 index 000000000000..02b6fcc147bc --- /dev/null +++ b/tests/scripts/cute_dsl_kernels/top_k/perf/README.md @@ -0,0 +1,19 @@ +# GVR top-k per-step perf grid (provenance for the PR numbers) + +Per-step paired protocol: each (arm, model, ISL, B, layer) is one NVTX +range; the cold reps inside cycle every usable decode step (L2 evicted, +batch refilled before eviction), so the k-th kernel instance of a range +IS decode step k and arms pair index-by-index. + +- `ab_steps.py` - the driver (env: ARM = pr|st|va|vb|wf, UNITS = + "pro:64k,...", BS_LIST, OUT; GVR_CAP_ROOT points at the capture data, + HARNESS_ROOT at the repo). +- `f58_grid.sh ""` - nsys wrapper per arm x unit + (GVR_BENCH_OUT selects the output dir). +- `perstep.py` - per-step extraction from the nsys sqlite. +- `f58_an.py` - pairing + production routing (plan_emission) + the B x N + mean/min tables. `f58_regr.py`-style regression stats derive from the + same pickle. + +Requirements: B200 (SM100), cutlass-dsl 4.5.x on PYTHONPATH, exclusive +GPU (no concurrent process - it pollutes event timing), nsys >= 2025.3. diff --git a/tests/scripts/cute_dsl_kernels/top_k/perf/ab_steps.py b/tests/scripts/cute_dsl_kernels/top_k/perf/ab_steps.py new file mode 100644 index 000000000000..ba1c1ec5f9c7 --- /dev/null +++ b/tests/scripts/cute_dsl_kernels/top_k/perf/ab_steps.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# ruff: noqa +# Measurement harness committed verbatim for provenance; bench idioms +# (loop-scoped buffers, del/rebind) trip static analysis. +# Layer x step complete grid. One NVTX range per (arm, model, isl, B, +# layer); the COLD reps cycle through ALL usable decode steps (batch +# refilled from the step's row BEFORE the eviction, so the row itself +# stays cold) -> the per-range cold mean IS the all-steps mean for that +# layer. Tables aggregate mean-over-layers; per-layer means are the +# per-range values (saved separately). +# env: ARM, UNITS ("flash:4k,pro:512k,..."), OUT, BS_LIST +import json +import os +import sys +from pathlib import Path + +import torch + +CAP = Path( + os.environ.get( + "GVR_CAP_ROOT", + "/home/scratch.loncheng_gpu/workspace/perf/workloads/DSV4/" + "E2E_exp/indexer_decode_capture/data", + ) +) +LAYERS = {"flash": list(range(2, 43, 2)), "pro": list(range(2, 61, 2)), "v32": list(range(61))} +KCFG = {"flash": (512, 4), "pro": (1024, 4), "v32": (2048, 1)} +ALIGN = 64 +FMIN = torch.finfo(torch.float32).min + +ARM = os.environ["ARM"] +OUT = os.environ["OUT"] +_LSUB = os.environ.get("LAYER_SUBSET") +if _LSUB: + _ls = [int(x) for x in _LSUB.split(",")] + LAYERS = {k: [x for x in v if x in _ls] for k, v in LAYERS.items()} +UNITS = [u.split(":") for u in os.environ["UNITS"].split(",")] +BS_LIST = [ + int(x) for x in os.environ.get("BS_LIST", "1,2,4,8,16,32,64,128,256,512,1024").split(",") +] +_fb = os.environ.get("FORCE_BM") +FORCE_BM = None if _fb is None else int(_fb) +FORCE_CS = int(os.environ.get("FORCE_CS", "0")) +CONLY = os.environ.get("CONLY", "0") == "1" # codespell:ignore +CONLY_RANK = int(os.environ.get("CONLY_RANK", "0")) +_pw = os.environ.get("FORCE_PWR") +FORCE_PWR = None if _pw is None else (_pw == "1") +_tf = os.environ.get("FORCE_TAILFAST") +FORCE_TAILFAST = None if not _tf else (_tf == "1") +_nf = os.environ.get("FORCE_NOFINE") +FORCE_NOFINE = None if not _nf else (_nf == "1") +_nb = os.environ.get("FORCE_BINS") +FORCE_BINS = int(_nb) if _nb else None +_frt = os.environ.get("FORCE_FINERT") +FORCE_FINERT = None if not _frt else (_frt == "1") +_srt = os.environ.get("FORCE_SCATRT") +FORCE_SCATRT = None if not _srt else (_srt == "1") +_tm = os.environ.get("TIGHT_MULT") +TIGHT_MULT = float(_tm) if _tm else None +_nt = os.environ.get("FORCE_THREADS") +FORCE_THREADS = int(_nt) if _nt else None +_v3 = os.environ.get("FORCE_TAILV3") +FORCE_TAILV3 = None if not _v3 else (_v3 == "1") +_et = os.environ.get("FORCE_EXACTTAIL") +FORCE_EXACTTAIL = None if not _et else (_et == "1") +XSTATE = os.environ.get("XSTATE", "0") == "1" +_ft = os.environ.get("FORCE_TIGHTEN") +FORCE_TIGHTEN = None if _ft is None else bool(int(_ft)) +sms = torch.cuda.get_device_properties(0).multi_processor_count +_EVICT = torch.empty(512 * 1024 * 1024 // 4, dtype=torch.float32, device="cuda") + +if ARM == "pr": + sys.path.insert(0, "/home/scratch.siyid_coreai/workspace") + from prpkg16457.top_k.gvr_topk_decode import GvrTopKKernel # noqa: E402 +elif ARM == "bx": + # loncheng PR #16877 head (BSX multi-tier): guard -> bsx dispatch, + # guard-fail -> that branch's own in-tree kernel (its production path) + sys.path.insert(0, "/home/scratch.siyid_coreai/wt-bsx/tensorrt_llm/_torch/cute_dsl_kernels") + from blackwell.top_k.gvr_topk_decode import GvrTopKKernel # noqa: E402 + from blackwell.top_k.gvr_topk_decode_bsx_dispatch import ( # noqa: E402 + bsx_topk, + is_bsx_supported, + ) +else: + _root = os.environ.get("HARNESS_ROOT", str(Path(__file__).resolve().parents[5])) + sys.path.insert(0, _root + "/tests/scripts/cute_dsl_kernels/top_k") + sys.path.insert(0, _root) + import run_gvr_topk as rg # noqa: E402 + from run_gvr_topk import GvrTopKKernel # noqa: E402 + + if ARM != "st": + from run_gvr_topk import ( + emu_block_max, # noqa: E402 + emu_cand_bucketed, + emu_seed_counts, + pack_seed, + ) + + +def wf_targets(model, K, N): + if model == "flash": + T = (4096, 2048, 1024) + elif model == "v32": + T = (16384, 8192, 3072) + else: + T = (4096, 3584, 1536) if N <= 40960 else (12288, 5120, 2048) + return [min(t, N) for t in T] + + +def main(): + f = open(OUT, "w") + for model, isl in UNITS: + K, cr = KCFG[model] + Ls = LAYERS[model] + # ---- load per-layer step dicts (logits + captured topk) ---- + lgs, pks = {}, {} + for L in Ls: + d = CAP / model / f"ISL_{isl}" / f"layer_{L:02d}" + lgs[L] = torch.load(d / "decode.logits.in.pt", map_location="cpu", weights_only=False) + pks[L] = torch.load(d / "decode.topk.out.pt", map_location="cpu", weights_only=False) + steps_all = sorted(pks[Ls[0]].keys()) + # warmup steps carry -1 sentinel topk (and even wrong-width + # logits buffers): valid = captured topk present; a measured + # step also needs the PREVIOUS step valid (its topk is the + # preIdx seed) + valid = [s for s in steps_all if int(pks[Ls[0]][s].max()) >= 0] + vset = set(valid) + usable = [s for s in valid if (s - 1) in vset] + assert usable, f"no usable steps for {model}:{isl}" + NS = {} + for s in valid: + NS[s] = max(int(pks[L][s].max()) + 1 for L in Ls) + N_label = NS[valid[-1]] + # unit-constant pad width: block_max is validated against the + # BATCH buffer width (logits.shape[1]), so per-step bm must be + # built on rows already padded to the unit max + Npad_u = max((NS[s] + ALIGN - 1) // ALIGN * ALIGN for s in usable) + # ---- per (layer, step) prep on GPU ---- + # row (padded), preIdx, lines, ref values, arm extras (1-row) + prep = {} + for L in Ls: + for s in usable: + Ns = NS[s] + Npad = Npad_u + row = torch.full((1, Npad), FMIN, dtype=torch.float32, device="cuda") + src = lgs[L][s][0] + row[0, :Ns] = src[:Ns].float().cuda() + pre = pks[L][s - 1].flatten().to(torch.int32).view(1, K) + pre = pre.cuda().contiguous() + srt = torch.sort(row[0, :Ns], descending=True).values + ref = srt[:K].clone() + ks = wf_targets(model, K, Ns) + t = torch.empty((1, 3), dtype=torch.float32, device="cuda") + for j, kc in enumerate(ks): + t[0, j] = srt[kc - 1] + if TIGHT_MULT: + # the closed loop derives its tight line from the + # PREVIOUS step's k-th value, i.e. it sits just above + # rank K - not at the fixed rank wf_targets uses. Model + # that here so the tiers are measured on the contract + # they actually ship with. + t[0, 2] = srt[min(int(TIGHT_MULT * K), Ns) - 1] + for j in (1, 2): + t[0, j] = torch.maximum(t[0, j], t[0, j - 1] + 1e-6) + if CONLY: # codespell:ignore + # push both tight lines above the row max so every + # admitted entry lands in the loosest segment (the + # one that already uses the cheap claim window). + # CONLY_RANK tightens the collection line so the + # claimed count stays under the consumer's line-cut + # limit (and less volume is emitted). + if CONLY_RANK: + t[0, 0] = srt[min(CONLY_RANK * K, Ns) - 1] + hi = float(row[0, :Ns].max()) + 1e4 + t[0, 1] = hi + t[0, 2] = hi + 1.0 + e = dict(Ns=Ns, Npad=Npad, row=row, pre=pre, ref=ref, sthr=t) + if ARM not in ("pr", "st", "bx"): + sl1 = torch.tensor([Ns * cr], dtype=torch.int32, device="cuda") + scnt = emu_seed_counts(row, sl1, t, compress_ratio=cr) + if ARM == "wf": + cv, ci, ctl = emu_cand_bucketed( + row, sl1, t, 24576, seg_cap=8192, compress_ratio=cr, sentinel_pad=64 + ) + e.update(cv=cv, ci=ci, ctl=ctl) + # thresholds mirror gvr_routing (real-capture A/B) + nb = (ARM == "va" and Ns >= 65536) or ( + ARM == "vb" and model == "flash" and Ns >= 131072 + ) + if FORCE_BM is not None: + nb = bool(FORCE_BM) + if nb: + e["bm"] = emu_block_max(row, sl1, compress_ratio=cr, tail_mode="exact") + # col 6 = adaptive-skip pass count when bm attached + e["spk"] = pack_seed(t, scnt, block_max=e.get("bm")) + prep[(L, s)] = e + Npad_max = max(e["Npad"] for e in prep.values()) + for B in BS_LIST: + # reusable batch buffers (filled per rep BEFORE eviction) + lg_b = torch.full((B, Npad_max), FMIN, dtype=torch.float32, device="cuda") + pre_b = torch.zeros((B, K), dtype=torch.int32, device="cuda") + sl_b = torch.zeros((B,), dtype=torch.int32, device="cuda") + thr_b = torch.zeros((B, 3), dtype=torch.float32, device="cuda") + xs_b = torch.zeros((B, 8), dtype=torch.float32, device="cuda") if XSTATE else None + spk_b = torch.zeros((B, 8), dtype=torch.float32, device="cuda") + bufs = {} + if ARM in ("pr", "st", "bx"): + bufs["outb"] = torch.empty(B, K, dtype=torch.int32, device="cuda") + if ARM == "wf": + # widths from the emu contract (2*seg_cap+cap for the + # bucketed list), NOT the nominal cap + e0 = next(iter(prep.values())) + CW = e0["cv"].shape[1] + bufs["cv"] = torch.zeros((B, CW), dtype=torch.float32, device="cuda") + bufs["ci"] = torch.zeros((B, CW), dtype=torch.int32, device="cuda") + bufs["ctl"] = torch.zeros((B, e0["ctl"].shape[1]), dtype=torch.int32, device="cuda") + bm_b = None + if ARM in ("va", "vb"): + nbp = max((e["bm"].shape[1] for e in prep.values() if "bm" in e), default=0) + if nbp: + bm_b = torch.zeros((B, nbp), dtype=torch.float32, device="cuda") + state = {} + + def fill(L, s): + e = prep[(L, s)] + state.update(e=e) + lg_b[:, : e["Npad"]].copy_(e["row"].expand(B, e["Npad"])) + if e["Npad"] < Npad_max: + lg_b[:, e["Npad"] :].fill_(FMIN) + pre_b.copy_(e["pre"].expand(B, K)) + sl_b.fill_(e["Ns"] * cr) + thr_b.copy_(e["sthr"].expand(B, 3)) + if ARM not in ("pr", "st", "bx"): + spk_b.copy_(e["spk"].expand(B, 8)) + if ARM == "wf": + bufs["cv"].copy_(e["cv"].expand_as(bufs["cv"])) + bufs["ci"].copy_(e["ci"].expand_as(bufs["ci"])) + bufs["ctl"].copy_(e["ctl"].expand_as(bufs["ctl"])) + if bm_b is not None and "bm" in e: + # unit-constant Npad -> bm width == bm_b width + bm_b.copy_(e["bm"].expand_as(bm_b)) + state["bmc"] = bm_b + torch.cuda.synchronize() + + def akw(): + e = state["e"] + Ns = e["Ns"] + kw = dict(next_n=1, compress_ratio=cr, num_sms=sms, return_output_values=False) + if xs_b is not None: + kw["xstate"] = xs_b + if FORCE_TAILFAST is not None: + kw["p4_tail_fast"] = FORCE_TAILFAST + if FORCE_EXACTTAIL is not None: + kw["p4_exact_tail"] = FORCE_EXACTTAIL + if FORCE_TAILV3 is not None: + kw["p4_tail_v3"] = FORCE_TAILV3 + if FORCE_NOFINE is not None: + kw["p4_no_fine"] = FORCE_NOFINE + if FORCE_BINS is not None: + kw["num_bins"] = FORCE_BINS + if FORCE_FINERT is not None: + kw["p4_fine_rangetest"] = FORCE_FINERT + if FORCE_SCATRT is not None: + kw["p4_scat_rangetest"] = FORCE_SCATRT + if FORCE_THREADS is not None: + kw["num_threads_per_block"] = FORCE_THREADS + if ARM == "sw": + return dict(kw) # wrapper stock: no seed, no xstate + if ARM == "wf": + a = dict( + seed_thr=spk_b, + cand_vals=bufs["cv"], + cand_idx=bufs["ci"], + cand_ctl=bufs["ctl"], + cluster_size=1, + **kw, + ) + if FORCE_PWR is not None: + a["p4_warp_redundant"] = FORCE_PWR + return a + if ARM == "va": + a = dict(seed_thr=spk_b, cluster_size=FORCE_CS or 1, **kw) + if "bm" in e: + a.update(block_max=state["bmc"], skip_min_n=None) + if FORCE_PWR is not None: + a["p4_warp_redundant"] = FORCE_PWR + return a + # vb + cs = 1 + if Ns >= 196608: + if B * 8 <= sms // 2: + cs = 8 + elif B * 4 <= (sms * 9) // 10: + cs = 4 + elif B * 2 <= (sms * 9) // 10: + cs = 2 + a = dict(seed_thr=thr_b, cluster_size=cs, **kw) + if "bm" in e: + a.update(block_max=state["bmc"], skip_min_n=None) + if FORCE_CS: + a["cluster_size"] = FORCE_CS + if FORCE_PWR is not None: + a["p4_warp_redundant"] = FORCE_PWR + return a + + def call(): + if ARM == "bx": + if "bx_ok" not in state: + state["bx_ok"] = is_bsx_supported( + lg_b, pre_b, sl_b, bufs["outb"], K, 1, cr, None, None + ) + if state["bx_ok"]: + bsx_topk(lg_b, pre_b, sl_b, bufs["outb"], K, 1, cr) + else: + GvrTopKKernel.launch(lg_b, pre_b, sl_b, bufs["outb"], K, compress_ratio=cr) + return + if ARM in ("pr", "st"): + ov = {} + if ARM == "st" and FORCE_PWR is not None: + ov["p4_warp_redundant"] = FORCE_PWR + GvrTopKKernel.launch( + lg_b, pre_b, sl_b, bufs["outb"], K, compress_ratio=cr, **ov + ) + else: + rg.gvr_topk_decode(lg_b, pre_b, sl_b, K, **akw()) + + def out_idx(): + if ARM in ("pr", "st", "bx"): + call() + torch.cuda.synchronize() + return bufs["outb"] + _, o = rg.gvr_topk_decode(lg_b, pre_b, sl_b, K, **akw()) + torch.cuda.synchronize() + return o + + for L in Ls: + base = f"{ARM}|{model}|{isl}|N{N_label}|B{B}|L{L:02d}" + rec = dict( + model=model, isl=isl, N=N_label, K=K, B=B, arm=ARM, layer=L, steps=len(usable) + ) + try: + # exactness: every step at B==min; spot at larger B + chk = usable if B == BS_LIST[0] else usable[:1] + ok = True + for s in chk: + fill(L, s) + o = out_idx() + e = prep[(L, s)] + for i in (0, B - 1) if B > 1 else (0,): + gid = o[i, :K].long() + # gather with an out-of-range index raises a + # device-side assert that poisons the context and + # kills the whole worker -> screen first + if int(gid.min()) < 0 or int(gid.max()) >= e["Ns"]: + ok = False + rec["oob"] = rec.get("oob", 0) + 1 + continue + got = lg_b[i, gid].sort(descending=True).values + if gid.unique().numel() != K or not torch.equal(got, e["ref"]): + ok = False + if not ok: + break + rec["exact"] = ok + # warmup + for s in usable[:4]: + fill(L, s) + call() + torch.cuda.synchronize() + # COLD: one rep per decode step -> range mean = all- + # steps mean for this layer + xrec = [] + for s in usable: + fill(L, s) + if xs_b is not None: + xs_b.zero_() + _EVICT.uniform_(0, 1) + torch.cuda.synchronize() + torch.cuda.nvtx.range_push(f"c|{base}") + call() + torch.cuda.synchronize() + torch.cuda.nvtx.range_pop() + if xs_b is not None: + xrec.append([round(v, 1) for v in xs_b[0].tolist()]) + if xs_b is not None: + rec["x"] = xrec + torch.cuda.synchronize() + except Exception as ex: # noqa: BLE001 + rec["error"] = f"{type(ex).__name__}: {str(ex)[:120]}" + f.write(json.dumps(rec) + "\n") + f.flush() + del lg_b, pre_b, sl_b, thr_b, spk_b, bufs, bm_b, xs_b + torch.cuda.empty_cache() + prep.clear() + lgs.clear() + pks.clear() + torch.cuda.empty_cache() + print(f"[{ARM}] {model} {isl} done", flush=True) + f.close() + print(f"{ARM}_STEPS_DONE") + + +if __name__ == "__main__": + main() diff --git a/tests/scripts/cute_dsl_kernels/top_k/perf/f58_an.py b/tests/scripts/cute_dsl_kernels/top_k/perf/f58_an.py new file mode 100644 index 000000000000..a4b21a0f683b --- /dev/null +++ b/tests/scripts/cute_dsl_kernels/top_k/perf/f58_an.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# ruff: noqa +# Measurement harness committed verbatim for provenance; bench idioms +# (loop-scoped buffers, del/rebind) trip static analysis. +# f58: B×N 四张表(flash/pro × 算数均值/最小值),逐步配对 vs PR16457, +# 我们的臂按线上路由(plan_emission)选取。 +import glob +import os +import pickle +import sys + +sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent)) +from perstep import per_step # noqa: E402 + +D = os.environ.get("GVR_BENCH_OUT", "./bench_results") +ARMS = { + "pr": "f58_*_pr.sqlite", + "st": "f58_*_st.sqlite", + "va": "f58_*_va.sqlite", + "vb": "f58_*_vb.sqlite", + "wf": "f58w_*.sqlite", +} +PKL = "./f58.pkl" +if os.path.exists(PKL) and os.environ.get("REUSE", "1") == "1": + data = pickle.load(open(PKL, "rb")) +else: + data = {} + for a, pat in ARMS.items(): + agg = {} + for f in sorted(glob.glob(os.path.join(D, pat))): + for k, v in per_step(f).items(): + agg.setdefault(k, []).extend(v) + data[a] = agg + print(f" {a}: {len(agg)} 个(格,层) {sum(len(v) for v in agg.values())} 步", flush=True) + pickle.dump(data, open(PKL, "wb")) + +sys.path.insert( + 0, + str( + __import__("pathlib").Path(__file__).parents[5] + / "tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k" + ), +) +from gvr_routing import plan_emission # noqa: E402 + +KC = {"flash": 512, "pro": 1024, "v32": 2048} +CR = {"flash": 4, "pro": 4, "v32": 1} +TIER2ARM = {"list": "wf", "counts": "va", "rungs": "vb", "none": "st"} +NS = ["8k", "16k", "32k", "64k", "128k", "256k"] +BS = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] + +cell = {} +for m, isl, b in sorted({(m, i, b) for (m, i, b, l) in data["pr"]}): + n = int(isl[:-1]) * 1024 // CR[m] + tier = plan_emission(b, n, KC[m], True) + arm = TIER2ARM[tier] + ratios = [] + for (mm, ii, bb, l), pr in data["pr"].items(): + if (mm, ii, bb) != (m, isl, b): + continue + ou = data[arm].get((mm, ii, bb, l), []) + for j in range(min(len(pr), len(ou))): + ratios.append(pr[j] / ou[j]) + if ratios: + cell[(m, isl, b)] = dict( + tier=tier, n=len(ratios), mean=sum(ratios) / len(ratios), mn=min(ratios) + ) + + +def table(m, key): + print(f"\n### {m} — {'算数均值' if key == 'mean' else '最小值'}(对 PR16457,全层×全步)") + print("| B \\ N | " + " | ".join(NS) + " |") + print("|---" * (len(NS) + 1) + "|") + for b in BS: + row = [f"**{b}**"] + for isl in NS: + c = cell.get((m, isl, b)) + row.append(f"{c[key]:.3f}" if c else "—") + print("| " + " | ".join(row) + " |") + + +for m in ("flash", "pro", "v32"): + for key in ("mean", "mn"): + table(m, key) + +tot = sum(c["n"] for c in cell.values()) +print( + f"\n共 {len(cell)} 格 / {tot} 逐步配对;各格档位:", + { + t: sum(1 for c in cell.values() if c["tier"] == t) + for t in ("list", "counts", "rungs", "none") + }, +) diff --git a/tests/scripts/cute_dsl_kernels/top_k/perf/f58_grid.sh b/tests/scripts/cute_dsl_kernels/top_k/perf/f58_grid.sh new file mode 100755 index 000000000000..26ca3b196945 --- /dev/null +++ b/tests/scripts/cute_dsl_kernels/top_k/perf/f58_grid.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Full-grid re-run on the current tree (post P4 fold work), fresh f58 prefix. +BR=${GVR_BENCH_OUT:-./bench_results} +cd "$(dirname "$0")" +# PYTHONPATH must carry cutlass-dsl 4.5.x and the repo root; see README.md +export TMPDIR=/tmp/nsys_g$1; mkdir -p $TMPDIR +for UNIT in $2; do + TAG=$(echo $UNIT | tr ':' '_') + for ARM in ${ARMS:-pr st va vb}; do + rm -f $BR/f58_${TAG}_${ARM}*.csv $BR/f58_${TAG}_${ARM}.sqlite + CUDA_VISIBLE_DEVICES=$1 env ARM=$ARM UNITS=$UNIT \ + GVR_BSTAR=8192 GVR_KC=8192 GVR_CAPC=16384 OUT=$BR/f58_${TAG}_${ARM}.jsonl \ + nsys profile --trace=cuda,nvtx --force-overwrite true -o $BR/f58_${TAG}_${ARM} \ + python3 ab_steps.py >> $BR/f58_${TAG}.log 2>&1 + nsys stats -r nvtx_kern_sum --format csv -o $BR/f58_${TAG}_${ARM} \ + $BR/f58_${TAG}_${ARM}.nsys-rep >> $BR/f58_${TAG}.log 2>&1 + echo "f58 done ${TAG}_${ARM}" >> $BR/f58.log + done + # list tier (wf) under the C-only contract, same dataset + rm -f $BR/f58w_${TAG}*.csv $BR/f58w_${TAG}.sqlite + CUDA_VISIBLE_DEVICES=$1 env ARM=wf UNITS=$UNIT CONLY=1 CONLY_RANK=2 \ # codespell:ignore + GVR_BSTAR=8192 GVR_KC=8192 GVR_CAPC=16384 OUT=$BR/f58w_${TAG}.jsonl \ + nsys profile --trace=cuda,nvtx --force-overwrite true -o $BR/f58w_${TAG} \ + python3 ab_steps.py >> $BR/f58_${TAG}.log 2>&1 + nsys stats -r nvtx_kern_sum --format csv -o $BR/f58w_${TAG} \ + $BR/f58w_${TAG}.nsys-rep >> $BR/f58_${TAG}.log 2>&1 + echo "f58 done ${TAG}_wf" >> $BR/f58.log +done +echo "f58 GPU$1 ALL DONE" >> $BR/f58.log diff --git a/tests/scripts/cute_dsl_kernels/top_k/perf/perstep.py b/tests/scripts/cute_dsl_kernels/top_k/perf/perstep.py new file mode 100644 index 000000000000..6f950dc07624 --- /dev/null +++ b/tests/scripts/cute_dsl_kernels/top_k/perf/perstep.py @@ -0,0 +1,75 @@ +# ruff: noqa +# Measurement harness committed verbatim for provenance; bench idioms +# (loop-scoped buffers, del/rebind) trip static analysis. +# Per-(layer, step) pairing straight out of the nsys sqlite. +# +# One NVTX range = one (arm, model, isl, N, B, layer); inside it the +# harness runs one kernel per decode step, in step order. So the k-th +# kernel of a range IS step k, and the arms can be paired index by +# index. This is the only way to get a true per-step ratio - the CSV +# summary only carries Avg/Min/Max per range, which cannot be paired. +import glob +import os +import re +import sqlite3 +import sys +from collections import defaultdict + +D = os.environ.get("GVR_BENCH_OUT", "./bench_results") + + +def per_step(path): + """{(model, isl, B, layer): [step0_us, step1_us, ...]} for one file.""" + out = {} + con = sqlite3.connect(path) + rng = con.execute( + "SELECT start, end, text FROM NVTX_EVENTS WHERE text LIKE 'c|%' ORDER BY start" + ).fetchall() + ker = con.execute( + "SELECT start, end - start FROM CUPTI_ACTIVITY_KIND_KERNEL ORDER BY start" + ).fetchall() + con.close() + # ranges do not overlap (one layer at a time), so a single merge walk + # assigns every kernel to the range containing it + i = 0 + for rs, re_, text in rng: + while i < len(ker) and ker[i][0] < rs: + i += 1 + p = text.lstrip(":").split("|") + if len(p) != 7: + while i < len(ker) and ker[i][0] < re_: + i += 1 + continue + _, arm, m, isl, _n, b, l = p + acc = out.setdefault((m, isl, int(b[1:]), l), []) + while i < len(ker) and ker[i][0] < re_: + acc.append(ker[i][1] / 1000.0) + i += 1 + return out + + +def collect(pattern): + agg = {} + for f in sorted(glob.glob(os.path.join(D, pattern))): + for k, v in per_step(f).items(): + agg.setdefault(k, []).extend(v) if k in agg else agg.setdefault(k, v) + return agg + + +if __name__ == "__main__": + arms = { + "pr": "f22_*_pr.sqlite", + "st": "f22_*_st.sqlite", + "va": "f22_*_va.sqlite", + "vb": "f22_*_vb.sqlite", + "wf": "f30_*.sqlite", + } + data = {a: collect(p) for a, p in arms.items()} + for a, d in data.items(): + n = sum(len(v) for v in d.values()) + print(f" {a}: {len(d)} 个(格,层) {n} 个步", flush=True) + import pickle + + with open("/home/scratch.siyid_coreai/workspace/perstep.pkl", "wb") as f: + pickle.dump(data, f) + print("saved perstep.pkl") From c4de505ac74258e89df698b4e092be1c51a37829 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:39:13 -0700 Subject: [PATCH 103/117] [None][chore] size list-tier candidate buffers by the routing cap Review touch-ups (approved round): the wide candidate buffers only serve the list tier, which plan_emission picks at batch <= LIST_EMIT_MAX_B - allocate that many rows instead of max_rows (~0.33 MB/row/layer, ~5 GB -> ~80 MB at max_rows=256 x 61 layers); clamp the per-step control-word zeroing accordingly. Also state the engine-static routing-N assumption at the plan call site. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../attention_backend/sparse/dsa/indexer.py | 5 +++++ .../attention_backend/sparse/gvr_emission.py | 20 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py b/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py index 34c04ef3dcc2..686c0ddd37b9 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/dsa/indexer.py @@ -1761,6 +1761,11 @@ def sparse_attn_indexer( st = self._ensure_gvr_emission(metadata, q_fp8.device) # indexer_max_seq_len is already the compressed length # (get_indexer_max_seq_len divides); do not divide again. + # routing N is the ENGINE-STATIC max (graph capture + # bakes the tier in); short live rows in a long-max + # engine run assist machinery the planner would refuse + # at their true length - exactness holds via the + # in-kernel guards, the tax is routed pessimistically emit_tier, self._gvr_route = st.plan( batch_size, indexer_max_seq_len, diff --git a/tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py b/tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py index 770ce37647dd..1aa77f3cc1a5 100644 --- a/tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py +++ b/tensorrt_llm/_torch/attention_backend/sparse/gvr_emission.py @@ -33,6 +33,7 @@ import torch from ...cute_dsl_kernels.blackwell.top_k.gvr_routing import ( + LIST_EMIT_MAX_B, LIST_EMIT_MIN_N, TopkRoute, pick_config, @@ -90,10 +91,16 @@ def __init__( self.cand_ctl: Optional[torch.Tensor] = None self.cand_cur: Optional[torch.Tensor] = None if enable_list_tier: - self.cand_vals = torch.zeros((max_rows, LIST_WIDTH), dtype=torch.float32, device=device) - self.cand_idx = torch.zeros((max_rows, LIST_WIDTH), dtype=torch.int32, device=device) - self.cand_ctl = torch.zeros((max_rows, 4), dtype=torch.int32, device=device) - self.cand_cur = torch.zeros((max_rows, 4), dtype=torch.int32, device=device) + # the routing only ever picks the list tier at + # batch <= LIST_EMIT_MAX_B, so the wide candidate buffers + # need that many rows, not max_rows (~0.33 MB/row/layer) + cand_rows = min(max_rows, LIST_EMIT_MAX_B) + self.cand_vals = torch.zeros( + (cand_rows, LIST_WIDTH), dtype=torch.float32, device=device + ) + self.cand_idx = torch.zeros((cand_rows, LIST_WIDTH), dtype=torch.int32, device=device) + self.cand_ctl = torch.zeros((cand_rows, 4), dtype=torch.int32, device=device) + self.cand_cur = torch.zeros((cand_rows, 4), dtype=torch.int32, device=device) # previous-step top-k feedback (address-stable; zero-init -> # first step's pre_idx points at index 0, a benign candidate) self.prev_topk = torch.zeros((max_rows, top_k), dtype=torch.int32, device=device) @@ -204,8 +211,9 @@ def update_seed_rows(self, num_rows: int, emit_tier: str = "counts") -> None: rungs[:, 1] = new1 rungs[:, 2] = new2 if self.cand_ctl is not None: - self.cand_ctl[:num_rows].zero_() - self.cand_cur[:num_rows].zero_() + nc = min(num_rows, self.cand_ctl.shape[0]) + self.cand_ctl[:nc].zero_() + self.cand_cur[:nc].zero_() def indexer_emit_kwargs(self, emit_tier: str, num_rows: int) -> dict: """kwargs for CuteDSLFP4PagedMQALogitsRunner.forward covering the From ba1d698f814048ed947422515987e36dc010c550 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:46:14 -0700 Subject: [PATCH 104/117] [None][fix] restore the plateau terminal dropped in the main merge The merge of main (which brought in #16877) resolved the shared kernel file toward this branch's emission work and unintentionally dropped the plateau-terminal feature added there (4642d92643, e382f9814d): when a bitwise-equal tie plateau wider than the candidate buffer straddles the K boundary, the bracket admits no threshold and the row previously fell through to the legacy give-up, leaving -1 pads (CI: 10/10 plateau_terminal params failed at the merge head). Port both commits onto the current drivers: - leader driver: keep this branch's slope-fit retry loop and append the budget-exhausted bisection collapse behind it (coherent undershoot-overflow guard; the retry's bracket widening marks a side stale with -1 and fails the guard) -> adjacent-float bracket sets done = 3 and a recount at the terminal threshold feeds Phase 3; - phase2_secant_search: same post-loop collapse ahead of the legacy give-up; - register-resident redundant driver: adjacent-float terminal inside the refine loop plus the post-loop collapse, warp-uniform by replay; - Phase 4: plateau fill from the tie class (ticket in the dedicated s_iscalars[7], seeded from the pre-P4 cand_count_p4 snapshot - the s_iscalars[0] slot is radix scratch by then), pad guard keyed on the captured s_iscalars[6] flag, for both the cs=1 and cs>1 leader paths. B200: plateau_terminal 10/10; full gvr decode file 744 passed / 1 xpassed / 0 failed; tiers file 95 passed / 0 failed. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../blackwell/top_k/gvr_topk_decode.py | 384 +++++++++++++++++- 1 file changed, 363 insertions(+), 21 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py index 904c56eac1f1..e15cb0be6fbe 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/top_k/gvr_topk_decode.py @@ -2613,8 +2613,18 @@ def phase2_secant_search( if nv == vlo_r or nv == vhi_r: nv = (vlo_r + vhi_r) * cutlass.Float32(0.5) if nv == vlo_r or nv == vhi_r: - thr_r = vlo_r - done_r = cutlass.Int32(2) + # ADJACENT-FLOAT bracket, same terminal as the + # leader path: a low side over the candidate + # buffer plus a high side under K means the + # boundary sits inside a bitwise-equal plateau + # wider than kC. Keep the sure-winner threshold + # and let Phase 4's plateau fill finish the row. + if clo_r > cutlass.Int32(kCC) and chi_r < cutlass.Int32(kK): + thr_r = vhi_r + done_r = cutlass.Int32(3) + else: + thr_r = vlo_r + done_r = cutlass.Int32(2) if done_r == cutlass.Int32(0): thr_r = nv par_r = par_r ^ cutlass.Int32(1) @@ -2644,6 +2654,75 @@ def phase2_secant_search( vhi_r = thr_r chi_r = cnt_r it = it + cutlass.Int32(1) + # ---- Budget-exhausted plateau collapse (mirrors the leader + # path): the refine budget can run out while the bracket is + # still wide because a tie plateau wider than kC admits no + # threshold. On exactly that signature, bisect to adjacent + # floats so the plateau terminal is exact. Every thread + # replays this from identical registers, so the branch stays + # warp-uniform and block_count_ge keeps its barrier cadence. + if ( + done_r == cutlass.Int32(0) + and clo_r > cutlass.Int32(kCC) + and chi_r >= cutlass.Int32(0) + and chi_r < cutlass.Int32(kK) + ): + itc = cutlass.Int32(0) + while itc < cutlass.Int32(64) and done_r == cutlass.Int32(0): + mid_c = (vlo_r + vhi_r) * cutlass.Float32(0.5) + if mid_c == vlo_r or mid_c == vhi_r: + thr_r = vhi_r + done_r = cutlass.Int32(3) + else: + thr_r = mid_c + par_r = par_r ^ cutlass.Int32(1) + cnt_r = self.block_count_ge( + input_row, + slice_start, + slice_end, + thr_r, + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + cutlass.Boolean(False), # do_cluster_sync (cs==1) + smem_input=smem_input, + redundant=True, + wcnt_off=par_r * cutlass.Int32(nwp2), + ) + if cnt_r >= cutlass.Int32(kK) and cnt_r <= cutlass.Int32(kCC): + done_r = cutlass.Int32(1) + elif cnt_r > cutlass.Int32(kCC): + vlo_r = thr_r + clo_r = cnt_r + else: + vhi_r = thr_r + chi_r = cnt_r + itc = itc + cutlass.Int32(1) + if done_r == cutlass.Int32(3): + # recount at the terminal threshold so Phase 3 sees + # per-thread counts for the sure-winner set. + par_r = par_r ^ cutlass.Int32(1) + cnt_r = self.block_count_ge( + input_row, + slice_start, + slice_end, + thr_r, + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + cutlass.Boolean(False), # do_cluster_sync (cs==1) + smem_input=smem_input, + redundant=True, + wcnt_off=par_r * cutlass.Int32(nwp2), + ) if done_r == cutlass.Int32(0): if clo_r <= cutlass.Int32(kCC * 2): thr_r = vlo_r @@ -2729,11 +2808,22 @@ def phase2_secant_search( nv = vhi - rng * cutlass.Float32(0.05) if nv == vlo or nv == vhi: - # Bracket exhausted — try midpoint, else give up. + # Bracket exhausted — try midpoint, else terminal. nv = (vlo + vhi) * cutlass.Float32(0.5) if nv == vlo or nv == vhi: - s_thr[0] = vlo - s_iscalars[1] = cutlass.Int32(2) # done = 2 (give up) + # ADJACENT-FLOAT bracket: every value in + # [vlo, vhi) is bitwise-equal to vlo. Low side + # overflowing the candidate buffer AND high side + # undershooting K means the boundary sits inside + # a bitwise-equal plateau wider than kC — record + # the plateau terminal (done = 3) and keep the + # sure-winner threshold vhi. + if clo > cutlass.Int32(kCC) and chi < cutlass.Int32(kK): + s_thr[0] = vhi + s_iscalars[1] = cutlass.Int32(3) # done = 3 (plateau) + else: + s_thr[0] = vlo + s_iscalars[1] = cutlass.Int32(2) # done = 2 (give up) else: s_thr[0] = nv else: @@ -2773,6 +2863,84 @@ def phase2_secant_search( cute.arch.barrier() it = it + cutlass.Int32(1) + # ---- Budget-exhausted plateau collapse ---- + # The refine budget can run out while the bracket is still wide: the + # secant step keeps making progress (the bracket shrinks every + # iteration) but a tie plateau wider than kC admits no threshold, so + # the count never lands in [kK, kCC]. In exactly that signature - + # count(>= v_lo) > kCC AND count(>= v_hi) < kK, both counts current - + # collapse the bracket by pure bisection until the ends are ADJACENT + # floats; every value in [v_lo, v_hi) is then bitwise-equal, so the + # plateau terminal (done = 3) is exact and Phase 4 completes the row + # from that tie class. A count landing in [kK, kCC] mid-collapse + # converges normally. Anything else keeps the legacy give-up below. + if ( + s_iscalars[1] == cutlass.Int32(0) + and s_iscalars[2] > cutlass.Int32(kCC) + and s_iscalars[3] >= cutlass.Int32(0) + and s_iscalars[3] < cutlass.Int32(kK) + ): + itc = cutlass.Int32(0) + while itc < cutlass.Int32(64) and s_iscalars[1] == cutlass.Int32(0): + if tidx == 0: + vlo_c = s_thr[1] + vhi_c = s_thr[2] + mid_c = (vlo_c + vhi_c) * cutlass.Float32(0.5) + if mid_c == vlo_c or mid_c == vhi_c: + s_thr[0] = vhi_c + s_iscalars[1] = cutlass.Int32(3) # plateau terminal + else: + s_thr[0] = mid_c + cute.arch.barrier() + if s_iscalars[1] == cutlass.Int32(0): + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[0], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + smem_input=smem_input, + do_cluster_sync=do_cluster_sync, + ) + if tidx == 0: + c_c = s_iscalars[0] + t_c = s_thr[0] + if c_c >= cutlass.Int32(kK) and c_c <= cutlass.Int32(kCC): + s_iscalars[1] = cutlass.Int32(1) + elif c_c > cutlass.Int32(kCC): + s_thr[1] = t_c + s_iscalars[2] = c_c + else: + s_thr[2] = t_c + s_iscalars[3] = c_c + cute.arch.barrier() + itc = itc + cutlass.Int32(1) + if s_iscalars[1] == cutlass.Int32(3): + # recount at the terminal threshold so Phase 3's cached + # per-thread counts describe the sure-winner set. + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[0], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + smem_input=smem_input, + do_cluster_sync=do_cluster_sync, + ) + cute.arch.barrier() + # ---- Post-loop fallback: if still not done, force threshold ---- if tidx == 0: if s_iscalars[1] == cutlass.Int32(0): @@ -4876,12 +5044,13 @@ def phase4_rank_scatter( output_values_row[i10] = self.dtype(smem_keys[i10]) output_indices_row[i10] = smem_vals[i10] i10 = i10 + cutlass.Int32(num_threads) - i11 = cand_count + tidx - while i11 < cutlass.Int32(kK): - if cutlass.const_expr(self.return_output_values): - output_values_row[i11] = self.dtype(self.NEG_FLT_MAX) - output_indices_row[i11] = cutlass.Int32(-1) - i11 = i11 + cutlass.Int32(num_threads) + if s_iscalars[6] == cutlass.Int32(0): # plateau fill completes done=3 + i11 = cand_count + tidx + while i11 < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[i11] = self.dtype(self.NEG_FLT_MAX) + output_indices_row[i11] = cutlass.Int32(-1) + i11 = i11 + cutlass.Int32(num_threads) # ------------------------------------------------------------------ # Phase 4: Histogram-based k-th selection + two-pass writeback @@ -5387,12 +5556,13 @@ def phase4_histogram_snap( ) output_indices_row[i10] = self._smem_ld(cutlass.Int32, vals_base, i10) i10 = i10 + cutlass.Int32(num_threads) - i11 = cand_count + tidx - while i11 < cutlass.Int32(kK): - if cutlass.const_expr(self.return_output_values): - output_values_row[i11] = self.dtype(self.NEG_FLT_MAX) - output_indices_row[i11] = cutlass.Int32(-1) - i11 = i11 + cutlass.Int32(num_threads) + if s_iscalars[6] == cutlass.Int32(0): # plateau fill completes done=3 + i11 = cand_count + tidx + while i11 < cutlass.Int32(kK): + if cutlass.const_expr(self.return_output_values): + output_values_row[i11] = self.dtype(self.NEG_FLT_MAX) + output_indices_row[i11] = cutlass.Int32(-1) + i11 = i11 + cutlass.Int32(num_threads) # ------------------------------------------------------------------ # Main kernel — one CTA per row @@ -5724,9 +5894,13 @@ def run_one_row( # [4] out_count # [5] local cand_count (per-CTA snapshot before cluster all-reduce; # consumed by the kernel-level cluster handoff) + # [6] plateau terminal flag, captured from [1] BEFORE Phase 4 + # (Phase 4 REUSES [1] as radix scratch, so the terminal must + # never be re-read from it afterwards) + # [7] plateau fill ticket (done == 3 only) s_iscalars = smem.allocate_tensor( element_type=cutlass.Int32, - layout=cute.make_ordered_layout((6,), order=(0,)), + layout=cute.make_ordered_layout((8,), order=(0,)), byte_alignment=16, ) # Per-CTA DSMEM scratch for the cluster all-reduce of cand_count: @@ -7391,9 +7565,96 @@ def _run_phases( s_iscalars[2] = cutlass.Int32(-1) cute.arch.barrier() rs = rs + cutlass.Int32(1) - if s_iscalars[1] != cutlass.Int32(1): - # tie-plateau fail-soft: land on the measured - # undershoot side (count <= kC => no overflow). + if s_iscalars[1] != cutlass.Int32(1): + # The retry budget could not land in [K, kC]. + # ONLY the coherent undershoot-overflow corner + # (count(>= lo) > kC AND 0 <= count(>= hi) < K, + # both counts CURRENT — the retry's bracket + # widening marks a side stale with -1 and thus + # fails this guard) collapses the bracket by + # pure bisection to ADJACENT floats, where the + # plateau terminal (done = 3, threshold = hi) + # is exact: Phase 4 emits the sure winners and + # the plateau fill completes the row from the + # tie class. A mid-collapse count landing in + # [K, kC] converges normally; anything else + # (incl. an exhausted collapse budget) falls + # through to the fail-soft terminal below. + it4 = cutlass.Int32(0) + if ( + s_iscalars[2] <= cutlass.Int32(self.kC) + or s_iscalars[3] < cutlass.Int32(0) + or s_iscalars[3] >= cutlass.Int32(self.top_k) + ): + it4 = cutlass.Int32(40) # guard: skip collapse + while it4 < cutlass.Int32(40) and s_iscalars[1] == cutlass.Int32(0): + if tidx == cutlass.Int32(0): + lo4 = s_thr[1] + hi4 = s_thr[2] + mid4 = (lo4 + hi4) * cutlass.Float32(0.5) + if mid4 == lo4 or mid4 == hi4: + s_thr[0] = hi4 + s_iscalars[1] = cutlass.Int32(3) + else: + s_thr[0] = mid4 + cute.arch.barrier() + if s_iscalars[1] == cutlass.Int32(0): + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[0], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + do_cluster_sync=do_cluster_sync, + smem_input=smem_input, + ) + cute.arch.barrier() + if tidx == cutlass.Int32(0): + c4 = s_iscalars[0] + t4 = s_thr[0] + if c4 >= cutlass.Int32(self.top_k) and c4 <= cutlass.Int32( + self.kC + ): + s_iscalars[1] = cutlass.Int32(1) + elif c4 > cutlass.Int32(self.kC): + s_thr[1] = t4 + s_iscalars[2] = c4 + else: + s_thr[2] = t4 + s_iscalars[3] = c4 + cute.arch.barrier() + it4 = it4 + cutlass.Int32(1) + if s_iscalars[1] == cutlass.Int32(3): + # recount at the terminal threshold so P3's + # cached per-thread counts describe the + # sure-winner set the fill completes. + self.block_count_ge( + input_row, + slice_start, + slice_end, + s_thr[0], + smem_ptcnt, + smem_wcnt, + s_iscalars, + s_cluster_partial, + tidx, + warp_id, + lane, + do_cluster_sync=do_cluster_sync, + smem_input=smem_input, + ) + cute.arch.barrier() + elif s_iscalars[1] != cutlass.Int32(1): + # fail-soft (non-plateau): land on the + # measured undershoot side (count <= kC => + # no overflow; -1 pad stays the documented + # non-convergence encoding). self.block_count_ge( input_row, slice_start, @@ -7497,6 +7758,13 @@ def _run_phases( cand_count_p4 = cutlass.Int32(0) if cutlass.const_expr(cluster_size == 1): # cs=1: the single CTA per row IS the leader. + # Capture the P2 terminal BEFORE Phase 4: P4 reuses + # s_iscalars[1] as radix scratch. + if tidx == cutlass.Int32(0): + s_iscalars[6] = cutlass.Int32(0) + if s_iscalars[1] == cutlass.Int32(3): + s_iscalars[6] = cutlass.Int32(1) + cute.arch.barrier() cand_count_p4 = min(s_iscalars[0], cutlass.Int32(self.kC)) if cutlass.const_expr(self.enable_p4_rank_scatter): if cutlass.const_expr( @@ -7551,6 +7819,39 @@ def _run_phases( warp_id, lane, ) + # ---- plateau fill (done == 3): complete the row from the + # bitwise-equal plateau class. The terminal is only set on an + # ADJACENT-FLOAT bracket, so every value in [s_thr[1], s_thr[0]) + # is bitwise-equal; Phase 4 has already emitted the + # cnt(>= s_thr[0]) sure winners, and ANY (K - count)-subset of + # the tie class is a valid tie-aware completion. Ticket counter + # lives in the DEDICATED s_iscalars[7]. + if s_iscalars[6] == cutlass.Int32(1): + pv_lo = s_thr[1] + pv_hi = s_thr[0] + if tidx == cutlass.Int32(0): + # cand_count_p4 was captured BEFORE Phase 4; + # s_iscalars[0] is radix scratch by now (same + # hazard as the flag). + s_iscalars[7] = cand_count_p4 + cute.arch.barrier() + ifp = tidx + while ifp < N: + vfp = cutlass.Float32(0.0) + if cutlass.const_expr(self.dtype == cutlass.Float32): + vfp = input_row[ifp] + else: + vfp = cutlass.Float32(input_row[ifp]) + if vfp >= pv_lo and vfp < pv_hi: + pfill = atomicAdd( + s_iscalars.iterator + cutlass.Int32(7), cutlass.Int32(1) + ) + if pfill < cutlass.Int32(self.top_k): + if cutlass.const_expr(self.return_output_values): + output_values_row[pfill] = self.dtype(vfp) + output_indices_row[pfill] = ifp + ifp = ifp + cutlass.Int32(self.num_threads) + cute.arch.barrier() ck_sw0 = cutlass.Int64(0) ck_sw1 = cutlass.Int64(0) if cutlass.const_expr(_P4_SUB_DBG): @@ -7712,6 +8013,13 @@ def _run_phases( # smem_keys/smem_vals (no peers to gather from). # ---- Phase 4: histogram snap + writeback ---- + # Capture the P2 terminal BEFORE Phase 4: P4 + # reuses s_iscalars[1] as radix scratch. + if tidx == cutlass.Int32(0): + s_iscalars[6] = cutlass.Int32(0) + if s_iscalars[1] == cutlass.Int32(3): + s_iscalars[6] = cutlass.Int32(1) + cute.arch.barrier() cand_count_p4 = min(s_iscalars[0], cutlass.Int32(self.kC)) if cutlass.const_expr(self.enable_p4_rank_scatter): self.phase4_rank_scatter( @@ -7761,6 +8069,40 @@ def _run_phases( xstate_row[5] = cutlass.Float32(s_mt_cnt[1]) xstate_row[6] = cutlass.Float32(s_mt_cnt[2]) + # ---- plateau fill (done == 3): complete the row from the + # bitwise-equal plateau class. The terminal is only set on an + # ADJACENT-FLOAT bracket, so every value in [s_thr[1], s_thr[0]) + # is bitwise-equal; Phase 4 has already emitted the + # cnt(>= s_thr[0]) sure winners, and ANY (K - count)-subset of + # the tie class is a valid tie-aware completion. Ticket counter + # lives in the DEDICATED s_iscalars[7]. + if s_iscalars[6] == cutlass.Int32(1): + pv_lo = s_thr[1] + pv_hi = s_thr[0] + if tidx == cutlass.Int32(0): + # cand_count_p4 was captured BEFORE Phase 4; + # s_iscalars[0] is radix scratch by now (same + # hazard as the flag). + s_iscalars[7] = cand_count_p4 + cute.arch.barrier() + ifp = tidx + while ifp < N: + vfp = cutlass.Float32(0.0) + if cutlass.const_expr(self.dtype == cutlass.Float32): + vfp = input_row[ifp] + else: + vfp = cutlass.Float32(input_row[ifp]) + if vfp >= pv_lo and vfp < pv_hi: + pfill = atomicAdd( + s_iscalars.iterator + cutlass.Int32(7), cutlass.Int32(1) + ) + if pfill < cutlass.Int32(self.top_k): + if cutlass.const_expr(self.return_output_values): + output_values_row[pfill] = self.dtype(vfp) + output_indices_row[pfill] = ifp + ifp = ifp + cutlass.Int32(self.num_threads) + cute.arch.barrier() + # Final cluster barrier: keep peer CTAs (and their SMEM) alive # until the leader's gather + Phase 4 finish. Skipped at # do_cluster_sync=False (no peers; short-row degrade non-leaders From 6c581a71008d80fb2605a10b06fe34ba1c699b89 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:15:52 -0700 Subject: [PATCH 105/117] [None][fix] fp4 paged MQA logits: make_fragment -> make_rmem_tensor The B300 lane installs a CuTe DSL without make_fragment; the emission unittests this PR adds run there and hit the removed API (AttributeError across test_cute_dsl_fp4_paged_mqa_logits_block_meta/ seed_counts/cand). Renamed all uses in the file to make_rmem_tensor, matching the DSL 4.6.1 API main already uses in gvr_topk_decode. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .../paged_mqa_logits/fp4_paged_mqa_logits.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py index 37434d104a7c..89f32856d77c 100644 --- a/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py +++ b/tensorrt_llm/_torch/cute_dsl_kernels/blackwell/paged_mqa_logits/fp4_paged_mqa_logits.py @@ -2211,17 +2211,17 @@ def kernel( meta_warp = local_tidx // 32 meta_lane = local_tidx % 32 if cutlass.const_expr(self.emit_seed_counts): - sthr = cute.make_fragment(next_n * 3, cutlass.Float32) - scnt = cute.make_fragment(next_n * 3, cutlass.Int32) - spass = cute.make_fragment(next_n, cutlass.Int32) + sthr = cute.make_rmem_tensor(next_n * 3, cutlass.Float32) + scnt = cute.make_rmem_tensor(next_n * 3, cutlass.Int32) + spass = cute.make_rmem_tensor(next_n, cutlass.Int32) for _i in cutlass.range_constexpr(next_n): spass[_i] = cutlass.Int32(0) for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_cand or self.emit_cand_bucketed): - cwbase = cute.make_fragment(next_n, cutlass.Int32) - cwleft = cute.make_fragment(next_n, cutlass.Int32) + cwbase = cute.make_rmem_tensor(next_n, cutlass.Int32) + cwleft = cute.make_rmem_tensor(next_n, cutlass.Int32) for _i in cutlass.range_constexpr(next_n): cwbase[_i] = cutlass.Int32(0) cwleft[_i] = cutlass.Int32(0) @@ -2229,10 +2229,10 @@ def kernel( # Per-lane hit accumulators, carried across all # tiles of the same q and flushed once per # q-transition — no warp-wide ops per tile. - hacc_min = cute.make_fragment(next_n, cutlass.Float32) - hacc_max = cute.make_fragment(next_n, cutlass.Float32) - hacc_sum = cute.make_fragment(next_n, cutlass.Float32) - hacc_cnt = cute.make_fragment(next_n, cutlass.Int32) + hacc_min = cute.make_rmem_tensor(next_n, cutlass.Float32) + hacc_max = cute.make_rmem_tensor(next_n, cutlass.Float32) + hacc_sum = cute.make_rmem_tensor(next_n, cutlass.Float32) + hacc_cnt = cute.make_rmem_tensor(next_n, cutlass.Int32) for _t in cutlass.range_constexpr(next_n): hacc_min[_t] = cutlass.Float32(_META_FLT_MAX) hacc_max[_t] = cutlass.Float32(_META_NEG_FLT_MAX) @@ -2985,17 +2985,17 @@ def kernel( meta_warp = local_tidx // 32 meta_lane = local_tidx % 32 if cutlass.const_expr(self.emit_seed_counts): - sthr = cute.make_fragment(next_n * 3, cutlass.Float32) - scnt = cute.make_fragment(next_n * 3, cutlass.Int32) - spass = cute.make_fragment(next_n, cutlass.Int32) + sthr = cute.make_rmem_tensor(next_n * 3, cutlass.Float32) + scnt = cute.make_rmem_tensor(next_n * 3, cutlass.Int32) + spass = cute.make_rmem_tensor(next_n, cutlass.Int32) for _i in cutlass.range_constexpr(next_n): spass[_i] = cutlass.Int32(0) for _i in cutlass.range_constexpr(next_n * 3): sthr[_i] = cutlass.Float32(_META_FLT_MAX) scnt[_i] = cutlass.Int32(0) if cutlass.const_expr(self.emit_cand or self.emit_cand_bucketed): - cwbase = cute.make_fragment(next_n, cutlass.Int32) - cwleft = cute.make_fragment(next_n, cutlass.Int32) + cwbase = cute.make_rmem_tensor(next_n, cutlass.Int32) + cwleft = cute.make_rmem_tensor(next_n, cutlass.Int32) for _i in cutlass.range_constexpr(next_n): cwbase[_i] = cutlass.Int32(0) cwleft[_i] = cutlass.Int32(0) @@ -3003,10 +3003,10 @@ def kernel( # Per-lane hit accumulators, carried across all # tiles of the same q and flushed once per # q-transition — no warp-wide ops per tile. - hacc_min = cute.make_fragment(next_n, cutlass.Float32) - hacc_max = cute.make_fragment(next_n, cutlass.Float32) - hacc_sum = cute.make_fragment(next_n, cutlass.Float32) - hacc_cnt = cute.make_fragment(next_n, cutlass.Int32) + hacc_min = cute.make_rmem_tensor(next_n, cutlass.Float32) + hacc_max = cute.make_rmem_tensor(next_n, cutlass.Float32) + hacc_sum = cute.make_rmem_tensor(next_n, cutlass.Float32) + hacc_cnt = cute.make_rmem_tensor(next_n, cutlass.Int32) for _t in cutlass.range_constexpr(next_n): hacc_min[_t] = cutlass.Float32(_META_FLT_MAX) hacc_max[_t] = cutlass.Float32(_META_NEG_FLT_MAX) From f24be00b1997a5db1fb82bda0ed5bd244aab6276 Mon Sep 17 00:00:00 2001 From: siyidNV <297196620+siyidNV@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:17:51 -0700 Subject: [PATCH 106/117] [None][chore] restore the .claude subtree clobbered in the main merge A staged-file cleanup during the 2a0c11f044 merge reset the .claude index entries to the pre-merge state, silently reverting main's updates for that subtree (85 files). Restore it to the merged main's (c30f71365c) content; no code paths affected. Signed-off-by: siyidNV <297196620+siyidNV@users.noreply.github.com> --- .claude/agents/exec-local-slurm.md | 31 + .claude/agents/exec-remote-slurm.md | 40 + .claude/agents/trtllm-test-specialist.md | 18 + .claude/skills/exec-env-check/SKILL.md | 142 ++ .claude/skills/exec-local-docker/SKILL.md | 121 ++ .claude/skills/exec-local-slurm/SKILL.md | 443 ++++++ .claude/skills/exec-remote-slurm/SKILL.md | 970 ++++++++++++++ .../skills/exec-remote-slurm/scripts/build.sh | 42 + .../exec-remote-slurm/scripts/build.slurm | 41 + .claude/skills/exec-slurm-compile/SKILL.md | 14 +- .../exec-slurm-compile/scripts/compile.slurm | 6 +- .../scripts/submit_compile.sh | 6 +- .../references/api-core.md | 3 +- .../references/concepts-tensors.md | 3 +- .claude/skills/perf-analysis/SKILL.md | 6 + .../perf-optimization-casebook/SKILL.md | 293 ++++ .../data/aliases.yaml | 53 + .../data/patterns.yaml | 406 ++++++ .../perf-optimization-casebook/data/tags.yaml | 116 ++ .../references/case-template.md | 211 +++ .../references/communication/deepep.md | 37 + .../references/communication/index.md | 74 ++ .../communication/low-precision-dispatch.md | 38 + .../communication/mnnvl-twoshot-allreduce.md | 35 + .../shape-aware-allreduce-autotune.md | 33 + .../userbuffers-symmetric-memory.md | 37 + .../fold-scale-swizzle-into-kernel.md | 33 + .../kernel-and-fusion/fp8-mla-kv-cache.md | 38 + .../kernel-and-fusion/fuse-add-norm-quant.md | 39 + .../kernel-and-fusion/fuse-ar-epilogue.md | 38 + .../fuse-datamovement-into-quantize.md | 85 ++ .../fuse-moe-routing-kernel.md | 115 ++ .../kernel-and-fusion/fuse-qk-norm-rope.md | 36 + .../hw-matched-lowprec-moe-gemm.md | 36 + .../references/kernel-and-fusion/index.md | 158 +++ .../mega-fuse-moe-deepgemm.md | 41 + .../ranking-only-precision-tf32.md | 87 ++ .../reevaluate-fusion-boundary-per-dtype.md | 87 ++ .../kernel-and-fusion/relax-tl-constexpr.md | 69 + .../sparse-mla-topk-attention.md | 117 ++ .../specialize-topk-selection-kernel.md | 103 ++ .../split-mla-reduction-kernel.md | 35 + .../kernel-and-fusion/triton-to-cpp-op.md | 81 ++ .../trtllm-gen-fp4-moe-backend.md | 36 + .../runtime-execution/attention-dp-padding.md | 33 + .../auxiliary-cache-in-kv-manager.md | 94 ++ .../cache-step-invariant-per-layer.md | 85 ++ .../chunked-prefill-aligned-auxiliary.md | 86 ++ .../runtime-execution/cuda-graph-padding.md | 33 + .../free-mla-intermediates.md | 33 + .../hoist-torch-compile-closures.md | 77 ++ .../references/runtime-execution/index.md | 165 +++ .../runtime-execution/mla-kv-cache-reuse.md | 35 + .../move-bookkeeping-into-cpp-op.md | 36 + .../multi-stream-shared-routed-expert.md | 37 + .../overlap-mla-rope-uk-bgemm.md | 35 + .../runtime-execution/overlap-online-eplb.md | 35 + .../runtime-execution/overlap-scheduler.md | 41 + .../references/runtime-execution/pdl.md | 35 + .../runtime-execution/piecewise-cuda-graph.md | 35 + .../pybind-wrapper-pure-python.md | 93 ++ .../relaxed-mtp-acceptance.md | 37 + .../skip-sparse-path-when-degenerate.md | 90 ++ .../split-custom-op-for-piecewise-capture.md | 93 ++ .../runtime-execution/two-model-mtp-eagle.md | 35 + .claude/skills/perf-optimization/SKILL.md | 61 +- .claude/skills/trtllm-case-executor/SKILL.md | 440 ++++++ .../scripts/detect_slurm_env.sh | 449 +++++++ .../trtllm-model-onboard-multimodal/SKILL.md | 14 +- .../trtllm-test-script-builder/SKILL.md | 385 ++++++ .../references/trtllm_test_template.md | 520 ++++++++ .../scripts/build_slurm_script.py | 643 +++++++++ .../scripts/slurm_run_custom.sh | 33 + .../skills/trtllm-test-specialist/SKILL.md | 681 ++++++++++ .../references/agg_config_template.yaml | 257 ++++ .../references/benchmark_config_template.yaml | 133 ++ .../references/disagg_config_template.yaml | 63 + .../references/smoke_test_config_template.yml | 19 + .../references/test_config_template.yaml | 227 ++++ .../trtllm_test_fix_recommendations.md | 14 + .../scripts/build_test_command.py | 768 +++++++++++ .../scripts/extract_test_markers.py | 200 +++ .../scripts/generate_benchmark_config.py | 1184 +++++++++++++++++ .../scripts/generate_report.py | 297 +++++ .../scripts/parse_config.py | 237 ++++ 85 files changed, 12247 insertions(+), 74 deletions(-) create mode 100644 .claude/agents/exec-local-slurm.md create mode 100644 .claude/agents/exec-remote-slurm.md create mode 100644 .claude/agents/trtllm-test-specialist.md create mode 100644 .claude/skills/exec-env-check/SKILL.md create mode 100644 .claude/skills/exec-local-docker/SKILL.md create mode 100644 .claude/skills/exec-local-slurm/SKILL.md create mode 100644 .claude/skills/exec-remote-slurm/SKILL.md create mode 100644 .claude/skills/exec-remote-slurm/scripts/build.sh create mode 100644 .claude/skills/exec-remote-slurm/scripts/build.slurm create mode 100644 .claude/skills/perf-optimization-casebook/SKILL.md create mode 100644 .claude/skills/perf-optimization-casebook/data/aliases.yaml create mode 100644 .claude/skills/perf-optimization-casebook/data/patterns.yaml create mode 100644 .claude/skills/perf-optimization-casebook/data/tags.yaml create mode 100644 .claude/skills/perf-optimization-casebook/references/case-template.md create mode 100644 .claude/skills/perf-optimization-casebook/references/communication/deepep.md create mode 100644 .claude/skills/perf-optimization-casebook/references/communication/index.md create mode 100644 .claude/skills/perf-optimization-casebook/references/communication/low-precision-dispatch.md create mode 100644 .claude/skills/perf-optimization-casebook/references/communication/mnnvl-twoshot-allreduce.md create mode 100644 .claude/skills/perf-optimization-casebook/references/communication/shape-aware-allreduce-autotune.md create mode 100644 .claude/skills/perf-optimization-casebook/references/communication/userbuffers-symmetric-memory.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fold-scale-swizzle-into-kernel.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fp8-mla-kv-cache.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fuse-add-norm-quant.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fuse-ar-epilogue.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fuse-datamovement-into-quantize.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fuse-moe-routing-kernel.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/fuse-qk-norm-rope.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/hw-matched-lowprec-moe-gemm.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/index.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/mega-fuse-moe-deepgemm.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/ranking-only-precision-tf32.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/reevaluate-fusion-boundary-per-dtype.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/relax-tl-constexpr.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/sparse-mla-topk-attention.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/specialize-topk-selection-kernel.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/split-mla-reduction-kernel.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/triton-to-cpp-op.md create mode 100644 .claude/skills/perf-optimization-casebook/references/kernel-and-fusion/trtllm-gen-fp4-moe-backend.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/attention-dp-padding.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/auxiliary-cache-in-kv-manager.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/cache-step-invariant-per-layer.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/chunked-prefill-aligned-auxiliary.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/cuda-graph-padding.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/free-mla-intermediates.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/hoist-torch-compile-closures.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/index.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/mla-kv-cache-reuse.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/move-bookkeeping-into-cpp-op.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/multi-stream-shared-routed-expert.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/overlap-mla-rope-uk-bgemm.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/overlap-online-eplb.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/overlap-scheduler.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/pdl.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/piecewise-cuda-graph.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/pybind-wrapper-pure-python.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/relaxed-mtp-acceptance.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/skip-sparse-path-when-degenerate.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/split-custom-op-for-piecewise-capture.md create mode 100644 .claude/skills/perf-optimization-casebook/references/runtime-execution/two-model-mtp-eagle.md create mode 100644 .claude/skills/trtllm-case-executor/SKILL.md create mode 100755 .claude/skills/trtllm-case-executor/scripts/detect_slurm_env.sh create mode 100644 .claude/skills/trtllm-test-script-builder/SKILL.md create mode 100644 .claude/skills/trtllm-test-script-builder/references/trtllm_test_template.md create mode 100755 .claude/skills/trtllm-test-script-builder/scripts/build_slurm_script.py create mode 100755 .claude/skills/trtllm-test-script-builder/scripts/slurm_run_custom.sh create mode 100644 .claude/skills/trtllm-test-specialist/SKILL.md create mode 100644 .claude/skills/trtllm-test-specialist/references/agg_config_template.yaml create mode 100644 .claude/skills/trtllm-test-specialist/references/benchmark_config_template.yaml create mode 100644 .claude/skills/trtllm-test-specialist/references/disagg_config_template.yaml create mode 100644 .claude/skills/trtllm-test-specialist/references/smoke_test_config_template.yml create mode 100644 .claude/skills/trtllm-test-specialist/references/test_config_template.yaml create mode 100644 .claude/skills/trtllm-test-specialist/references/trtllm_test_fix_recommendations.md create mode 100644 .claude/skills/trtllm-test-specialist/scripts/build_test_command.py create mode 100644 .claude/skills/trtllm-test-specialist/scripts/extract_test_markers.py create mode 100644 .claude/skills/trtllm-test-specialist/scripts/generate_benchmark_config.py create mode 100755 .claude/skills/trtllm-test-specialist/scripts/generate_report.py create mode 100644 .claude/skills/trtllm-test-specialist/scripts/parse_config.py diff --git a/.claude/agents/exec-local-slurm.md b/.claude/agents/exec-local-slurm.md new file mode 100644 index 000000000000..795f5751441c --- /dev/null +++ b/.claude/agents/exec-local-slurm.md @@ -0,0 +1,31 @@ +--- +name: exec-local-slurm +description: > + Execute a TensorRT-LLM workload on a local Slurm cluster. Supports persistent + allocation (allocate once via nohup salloc, reuse across runs) and one-shot + sbatch. Workflow-agnostic — handles pytest, eval, benchmark, and custom + scripts identically. The orchestrator (typically trtllm-case-executor) writes + a job spec to /job_spec.json and invokes this agent to run it. +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +model: sonnet +license: Apache-2.0 +--- + +You are the local Slurm executor agent. Load the `exec-local-slurm` skill (`trtllm-agent-toolkit:exec-local-slurm`) and follow its procedure exactly. The skill is the single source of truth for the execution flow; this agent file only adds the contract with the caller and the invariants that must hold across every run. + +## Input + +The caller passes a path to a job spec — typically `/job_spec.json` — plus a short summary of the fields used in this run. **Read `job_spec.json` first.** The skill's "Input (from orchestrator prompt)" section enumerates every field it consumes (`script_path`, `work_dir`, `model_name`, `workflow_type`, `success_patterns`, `failure_patterns`, `log_file_pattern`, `monitor_timeout_seconds`, `persistent_mode`, `release_allocation`, `alloc_time_limit`, `docker_image`, `container_name`, `container_mounts`, `repo_root`, `slurm_params`). + +Do not re-derive any field that `trtllm-case-executor` already wrote into the spec. + +## Invariants + +- **Hang detection.** Poll the log periodically; on a case-insensitive `hang detected` match, kill the process group and report `HANG_DETECTED`. Implementation lives in the skill. +- **Wall-clock limit.** Honor `monitor_timeout_seconds` (default `3600`). On timeout, kill and report `TIMEOUT`. +- **Persistent allocation lifecycle.** Default `persistent_mode=true`; reuse an existing allocation when present and valid. Only release when `release_allocation=true` is explicitly set — never auto-release. +- **Single source of truth.** `node_count`, `job_name`, `container_image`, and `slurm_params` come from `job_spec.json`. Never recompute them or re-grep `current_image_tags.properties`. + +## Output + +Return a single report to the caller with: task type, status (`PASSED` / `FAILED` / `TIMEOUT` / `HANG_DETECTED` / `OUT_OF_MEMORY` / `CANCELLED` / `ERROR` / `BUILD_FAILED`), Slurm job id (and allocation id when persistent), log file path, summary, and any error excerpts (last ~100 lines on build/job failure). Do not perform follow-up actions beyond what the skill prescribes. diff --git a/.claude/agents/exec-remote-slurm.md b/.claude/agents/exec-remote-slurm.md new file mode 100644 index 000000000000..496493d0aa56 --- /dev/null +++ b/.claude/agents/exec-remote-slurm.md @@ -0,0 +1,40 @@ +--- +name: exec-remote-slurm +description: > + Execute a TensorRT-LLM workload on a remote Slurm cluster via SSH. Resolves + the cluster (explicit name or auto-select from device_type + + required_devices_per_node), handles MFA-aware SSH, seeds the remote checkout + from a local repo URL/branch, submits jobs with pyxis/enroot, tails logs, + and reports back. The orchestrator (typically trtllm-case-executor) writes a + job spec to /job_spec.json and invokes this agent to run it. +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +model: sonnet +license: Apache-2.0 +--- + +You are the remote Slurm executor agent. Load the `exec-remote-slurm` skill (`trtllm-agent-toolkit:exec-remote-slurm`) and follow its procedure exactly. The skill is the single source of truth for the execution flow; this agent file only adds the contract with the caller and the invariants that must hold across every run. + +## Input + +The caller passes a path to a job spec — typically `/job_spec.json` — plus a short summary of the fields used in this run. **Read `job_spec.json` first.** The skill's "Input" section enumerates every field it consumes (`script_path`, `script_name`, `work_dir`, `model_name`, `workflow_type`, `success_patterns`, `failure_patterns`, `log_file_pattern`, `slurm_cluster`, `ssh_host`, `slurm_user`, `remote_cwd`, `remote_work_dir`, `slurm_password`, `extra_files`, `repo_url`, `repo_branch`, `device_type`, `total_required_devices`, `required_devices_per_node`, `container_image`, `node_count`, `job_name`, `monitor_timeout_seconds`). + +Do not re-derive any field that `trtllm-case-executor` already wrote into the spec. + +## Cluster resolution + +**Precondition — optional dependency.** Before either bullet below, check whether the `skills/internal-env-info/` directory exists in the toolkit. If it does **not**, do not attempt to load any reference file or invoke the skill. Proceed using only the cluster fields the orchestrator wrote into `job_spec.json` (`ssh_host`, `slurm_user`, `remote_cwd`, `partition`, `account`, `container_image`, `mounts`, `gpus_per_node`, `mfa_style`). If a required field is also absent, stop and ask the user to supply it — do **not** report the missing skill as an error. + +- **Explicit cluster** (`slurm_cluster` is set) → invoke the `internal-env-info` skill in single-cluster mode to fetch per-cluster info (`mfa_style`, `default_models_repo`, `default_user_root_dir`, `gpus_per_node`). Connection fields (`ssh_host`, `slurm_user`, `remote_cwd`, `account`, `partition`, `mounts`, `container_image`) come from `job_spec.json` — there is no per-cluster connection-config file to parse. +- **Auto-select** (only hardware constraints are present) → invoke the `internal-env-info` skill in constraint-based mode, passing `device_type` and `required_devices_per_node` (and optionally `total_required_devices`). Never re-implement the constraint filter. + +## Invariants + +- **`mfa_style` decides the SSH path.** `false` → direct; `true` → MFA flow; `null` → probe direct first, then fall back, and ask the user to update the cluster reference. Do **not** probe-and-fall-back on the SSH error string when `mfa_style` is known. +- **Hang detection + `monitor_timeout_seconds`.** Same semantics as local execution; implementation lives in the skill. +- **Pass-through fields.** `container_image`, `node_count`, and `job_name` come from `job_spec.json` and are used verbatim. Apply transport-specific URL rewrites (e.g., enroot `/` → `#`) at use-time. Never re-grep `current_image_tags.properties` or reconstruct `job_name` from `account` / `subproject` / `detail`. +- **Remote repo bootstrap.** Use `repo_url` and `repo_branch` from `job_spec.json` to ensure the remote checkout matches the local one before submission. +- **`node_count` is authoritative.** Use it directly as `--nodes`; never recompute from totals. + +## Output + +Return a single report to the caller with: task type, status (`PASSED` / `FAILED` / `TIMEOUT` / `HANG_DETECTED` / `OUT_OF_MEMORY` / `CANCELLED` / `ERROR` / `BUILD_FAILED`), remote Slurm job id, remote log path (and a local copy when synced back), summary, and any error excerpts (last ~100 lines on build/job failure). Do not perform follow-up actions beyond what the skill prescribes. diff --git a/.claude/agents/trtllm-test-specialist.md b/.claude/agents/trtllm-test-specialist.md new file mode 100644 index 000000000000..0952a24f01c2 --- /dev/null +++ b/.claude/agents/trtllm-test-specialist.md @@ -0,0 +1,18 @@ +--- +name: trtllm-test-specialist +description: > + Runs model-level and module-level tests for TensorRT-LLM. Classifies the test + scope (module test or model test), builds the appropriate test commands, and + delegates execution to trtllm-case-executor. Supports functionality/smoke + tests, benchmarks, and evaluations. Writes structured test reports to a + caller-specified path. +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +license: Apache-2.0 +--- + +Role: dispatch TRT-LLM model-level and module-level test requests. + +Load the `trtllm-agent-toolkit:trtllm-test-specialist` skill, pass the caller's parameters through verbatim, and return its result. + +- If the caller supplies `report_file`, write the report to that exact path — do not substitute the skill's default (`./-auto-test-report.md`) or invent your own. +- Return the skill's status and final report path verbatim; do not re-summarize or rename. diff --git a/.claude/skills/exec-env-check/SKILL.md b/.claude/skills/exec-env-check/SKILL.md new file mode 100644 index 000000000000..3b91216a74b1 --- /dev/null +++ b/.claude/skills/exec-env-check/SKILL.md @@ -0,0 +1,142 @@ +--- +name: exec-env-check +description: >- + Check the local execution environment for GPU availability, Docker support, + and Slurm access. Returns the execution scenario (`satisfied, local, docker`, + `satisfied, local, direct`, `satisfied, slurm, local`, or `not_satisfied`), + the number of available GPUs, and the GPU type. On Slurm login nodes + without local GPUs, the cluster is identified by delegating the hostname + to internal-env-info (hostname-based mode), which owns the + hostname → cluster_name patterns; GPU type and gpus_per_node then come + from that skill's reference files. If internal-env-info is not + installed, the scenario falls back to `not_satisfied` without probing + compute nodes via srun. +tags: [infrastructure, slurm, environment] +license: Apache-2.0 +metadata: + author: NVIDIA Corporation +--- + +# TensorRT-LLM Environment Check + +Detect whether the current machine can run a GPU workload locally (Docker) or via Slurm, and report hardware details. + +## Input + +| Field | Description | Required | +|-------|-------------|----------| +| `required_devices` | Minimum number of GPUs needed | Yes | +| `account` | Slurm account (unused for GPU probing, kept for compatibility) | No | + +## Procedure + +### 1. Check local GPUs + +```bash +timeout 5 nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null +``` + +If this succeeds: +- Count the number of GPU lines → `available_gpus` +- Extract the GPU name from the first line → `device_type` (e.g., `NVIDIA B200`, `NVIDIA H100 80GB HBM3`) +- Normalize `device_type`: strip `NVIDIA ` prefix and trailing memory info to get the short name (e.g., `B200`, `H100`, `A100`, `L40S`, `RTX 6000`) + +If `nvidia-smi` fails or returns no GPUs → `available_gpus = 0`, `device_type = null`. + +### 2. Check if local GPUs are sufficient + +If `available_gpus >= required_devices`, continue to step 2a to determine whether Docker is available on this host. + +### 2a. Check Docker availability + +```bash +command -v docker >/dev/null 2>&1 && timeout 3 docker info >/dev/null 2>&1 +``` + +- If the command succeeds (Docker CLI exists **and** the daemon responds) → **Result**: `satisfied, local, docker` +- Otherwise (Docker CLI missing, daemon not running, or permission denied) → **Result**: `satisfied, local, direct` + +Include `available_gpus` in the result. Do NOT include `device_type` — local execution does not need it. + +### 3. Check Slurm availability + +If local GPUs are insufficient (or as additional detection), check for Slurm: + +```bash +which squeue 2>/dev/null && squeue --version 2>/dev/null +``` + +If Slurm is NOT available → go to step 5. + +### 4. Resolve GPU type + +**Optional dependency.** Before doing anything else in this step, check whether `skills/internal-env-info/` exists in the toolkit. If it does **not**, skip this step entirely and return `scenario: not_satisfied` with `available_gpus = 0`, `device_type = null`, `gpus_per_node = null`, `cluster_name = null`, `default_models_repo = null`, and `default_user_root_dir = null`. Do **not** report this as an error — the skill is an internal-only dependency. + +When Slurm is available but local `nvidia-smi` returned no GPUs or `device_type` is null (login nodes typically have no GPUs), capture the hostname and delegate cluster identification to `internal-env-info`: + +```bash +hostname -f 2>/dev/null || hostname +``` + +- Pass the captured hostname to `internal-env-info` in **hostname-based mode**. That skill owns the NVIDIA-internal login-host patterns and the hostname → `` mapping; do **not** parse the hostname or hard-code any cluster identifier here. +- It returns the standard output template (`cluster_name`, `device_type`, `gpus_per_node`, `default_models_repo`, `default_user_root_dir`) plus supplementary field (`mfa_style`). +- Set `available_gpus` = `gpus_per_node` (if resolved). +- Set `cluster_name` = the value returned (a placeholder `` token in this skill). The orchestrator uses this to fetch per-cluster info (`mfa_style`, `default_models_repo`, `default_user_root_dir`, `gpus_per_node`) from `internal-env-info`; connection fields (`mounts`, `ssh_host`, `partition`, etc.) come from caller-supplied inputs in `job_spec.json` (with `internal-env-info` default values / ask-the-user fallbacks). +- Set `default_user_root_dir` = the user root directory returned (with `` substituted with the actual SLURM username). Set to `null` if not found. +- If `internal-env-info` returns `null` for `cluster_name` (no pattern matched), set `device_type = null`, `cluster_name = null`, `default_user_root_dir = null` and fall through to Step 5 (`not_satisfied`). + +**Result**: `satisfied, slurm, local` with `device_type`, `gpus_per_node`, `cluster_name`, and `default_user_root_dir`. + +### 5. Not satisfied + +If neither local GPUs nor Slurm is available: +- **Result**: `not_satisfied` + +## Output + +Return a single structured result: + +``` +scenario: +available_gpus: +device_type: +gpus_per_node: +cluster_name: +default_models_repo: +default_user_root_dir: +``` + +**Examples:** + +``` +scenario: satisfied, local, docker +available_gpus: 4 +``` + +``` +scenario: satisfied, local, direct +available_gpus: 4 +``` + +``` +scenario: satisfied, slurm, local +available_gpus: 4 +device_type: B200 +gpus_per_node: 4 +cluster_name: +default_user_root_dir: / +``` + +``` +scenario: not_satisfied +available_gpus: 0 +device_type: null +cluster_name: null +``` + +## Rules + +- Never install drivers or modify the system +- If `nvidia-smi` hangs, use a 5-second timeout: `timeout 5 nvidia-smi ...` +- GPU type is derived from the hostname by matching the cluster name via the `internal-env-info` skill when it is installed — no `srun` allocation needed. If that skill is absent, the scenario falls back to `not_satisfied` (see Step 4); do not error. +- Report the GPU type exactly as found in the mapping (do not guess or fabricate) diff --git a/.claude/skills/exec-local-docker/SKILL.md b/.claude/skills/exec-local-docker/SKILL.md new file mode 100644 index 000000000000..cd73eed6e043 --- /dev/null +++ b/.claude/skills/exec-local-docker/SKILL.md @@ -0,0 +1,121 @@ +--- +name: exec-local-docker +description: >- + Execute a TensorRT-LLM workload locally in Docker. Runs a fully-resolved + Docker command in background, monitors completion, reads logs, and reports + results. Workflow-agnostic — does not need to know if the workload is pytest, + eval, benchmark, or a custom script. +tags: [docker, execution, infrastructure] +license: Apache-2.0 +metadata: + author: NVIDIA Corporation +--- + +# Local Docker Executor + +Run a Docker command locally, monitor it, and report results. + +## Input (from orchestrator prompt) + +The orchestrator passes these fields in the skill prompt: + +| Field | Description | +|-------|-------------| +| `docker_cmd` | Complete `docker run` command string, ready to execute | +| `work_dir` | Local work directory for logs and artifacts | +| `log_file` | Full path to the log file (output redirected here) | +| `model_name` | Short model name for reporting | +| `workflow_type` | `pytest`, `eval`, `custom`, or `benchmark` — for output parsing hints | +| `success_patterns` | Comma-separated patterns indicating success (e.g., `passed,accuracy:`) | +| `failure_patterns` | Comma-separated patterns indicating failure (e.g., `FAILED,Error,AssertionError`) | + +## Procedure + +### Step 0: Resolve Image and Build (when `build_project=true`) + +This executor owns image selection and the build for the local Docker target. Skip the entire step when `build_project=false`. + +1. **Detect the target GPU type.** Use `gpu_type` from `job_spec.json` if upstream env-check resolved it; otherwise probe locally with `nvidia-smi --query-gpu=name --format=csv,noheader | head -1`. +2. **Detect the host CPU arch** with `uname -m` (`x86_64` or `aarch64`). +3. **Resolve the container image.** Read `/jenkins/current_image_tags.properties` and pick the tag whose CPU-arch flavor matches the host. If the orchestrator already passed a `container_image` field in the job spec, use that and skip the lookup. +4. **Map GPU → build arch (`-a` flag):** `H100`/`H200` → `90-real`; `B200`/`GB200`/`B300`/`GB300` → `100-real`; `A100` → `80-real`; `L40S` → `89-real`. Default `100-real` when the GPU is unknown. +5. **Compile.** Invoke the `exec-local-compile` skill with `repo_dir=`, `image=`, `arch=`. Wait for completion. +6. **On failure**, do not launch the workload. Report `BUILD_FAILED` with the last 100 lines of the compile log. + +`build_project`, `gpu_type`, `repo_root`, and (optionally) `container_image` come from `job_spec.json`. + +### Step 1: Launch + +Run the Docker command in background using `run_in_background`: + +```bash + 2>&1 | tee +``` + +Report to the orchestrator: "Launched locally, log at ``" + +### Step 2: Monitor for Hangs + +While waiting for the background process to complete, actively monitor the log +file for hang indicators. Launch a monitoring loop using `run_in_background`: + +```bash +while true; do + sleep 60 + if [ -f "" ] && grep -qi "hang detected" ""; then + echo "HANG_DETECTED: Found 'hang detected' in log file" + docker ps --filter "ancestor=" -q | xargs -r docker kill 2>/dev/null + exit 1 + fi +done +``` + +- If the monitor detects a hang, it kills the Docker container and exits with + code 1. The main background process will also terminate. +- When the main process completes normally (background notification received), + kill the monitoring loop (it is no longer needed). +- If a hang is detected, skip to Step 4 and report `HANG_DETECTED` status + instead of proceeding to normal result collection. + +### Step 3: Wait for Completion + +The Bash tool's `run_in_background` will notify when the process finishes +(either normally or because the container was killed by the hang monitor). + +### Step 4: Read Results + +On completion: + +1. **Read exit code** from the background command result. +2. **Read the last 100 lines** of `` using the Read tool. +3. **If exit code != 0**, also read the first 50 lines to catch early errors (import failures, setup crashes). +4. **Search for patterns**: + - Grep `` for each `success_patterns` entry + - Grep `` for each `failure_patterns` entry + +### Step 5: Report + +Return a structured result: + +``` +Status: PASSED | FAILED | ERROR | HANG_DETECTED +Exit code: +Log file: +Work directory: +Summary: +Errors: +``` + +### Output Parsing by Workflow Type + +- **pytest**: Look for `X passed, Y failed in Zs` summary line +- **eval**: Look for `accuracy:` or `score:` lines; check for `Expected accuracy >= X, but got Y` assertion +- **custom**: No specific patterns — report last 10 lines of output +- **benchmark**: Look for throughput/latency numbers + +## Rules + +- Never run the Docker command in foreground — always use `run_in_background` +- Never `cat` the full log file — use Read with offset/limit or tail +- If the Docker command fails immediately (exit code within seconds), check if the image exists locally +- Report results even if the log file is empty (container may have failed to start) diff --git a/.claude/skills/exec-local-slurm/SKILL.md b/.claude/skills/exec-local-slurm/SKILL.md new file mode 100644 index 000000000000..ecfd3a860fbc --- /dev/null +++ b/.claude/skills/exec-local-slurm/SKILL.md @@ -0,0 +1,443 @@ +--- +name: exec-local-slurm +description: >- + Submit and monitor a Slurm job on a local cluster. Supports two modes: + (1) Persistent allocation (default) — allocates nodes once via nohup salloc, + imports the container once, installs once, and reuses across runs by setting + SLURM env vars and running the sbatch script via bash. (2) One-shot sbatch — + submits a fully-generated Slurm script via sbatch, polls job status, reads + logs on completion, and reports results. Workflow-agnostic — handles pytest, + eval, benchmark, and custom scripts identically. +tags: [slurm, execution, infrastructure] +license: Apache-2.0 +metadata: + author: NVIDIA Corporation +--- + +# Local Slurm Executor + +Submit a Slurm job locally, monitor it, and report results. Uses persistent +allocation by default to eliminate queue wait, container import, and install +overhead on repeated runs. + +## Input (from orchestrator prompt) + +The orchestrator passes these fields in the skill prompt: + +| Field | Description | +|-------|-------------| +| `script_path` | Full local path to the generated `.slurm` script | +| `work_dir` | Local work directory for logs and artifacts | +| `model_name` | Short model name for reporting | +| `workflow_type` | `pytest`, `eval`, `custom`, or `benchmark` — for output parsing hints | +| `success_patterns` | Comma-separated patterns indicating success | +| `failure_patterns` | Comma-separated patterns indicating failure | +| `log_file_pattern` | Log filename pattern with `%j` placeholder (e.g., `llama_auto_test_%j.out`) | +| `persistent_mode` | Default: `true` for all local slurm workflows. Set to `false` to force one-shot sbatch (opt-out). | +| `release_allocation` | `true` to release the current allocation and stop. Only set when the user explicitly says no more jobs are needed. Default: `false`. Never auto-release. | +| `alloc_time_limit` | Walltime for the persistent allocation. Default: `04:00:00`. | +| `docker_image` | Container image for persistent container import. From `job_spec.json`. | +| `container_name` | Container name used in the `.slurm` script (e.g., `llama_auto_test`). Must match exactly. From `job_spec.json`. | +| `container_mounts` | Comma-separated mount mappings. From `job_spec.json`. | +| `repo_root` | Repo root path (for locating state file and project path). | +| `slurm_params` | Slurm parameters object: `partition`, `account`, `nodes`, `ntasks`, `ntasks_per_node`, `gpus_per_node`. From `job_spec.json`. | + +## Procedure + +### Pre-step: Resolve Image and Build (when `build_project=true`) + +This executor owns image selection and the build for the local SLURM cluster. Skip the entire pre-step when `build_project=false`. + +1. **Read the cluster GPU type** from `device_type` in `job_spec.json` (resolved upstream by env-check / internal-env-info). If absent, probe from any compute node via `srun -p --ntasks-per-node=1 nvidia-smi --query-gpu=name --format=csv,noheader | head -1`. If `skills/internal-env-info/` is not installed, skip the upstream lookup silently and rely on the srun probe — do not report the missing skill as an error. +2. **Determine the partition's CPU arch.** Look up `job_spec.slurm_env.partitions[]` for the entry where `name == partition` and read its `arch` (`x86_64` / `aarch64`). Case-executor's Step 2.5 already detected this — do **not** run `scontrol show node` here. If `slurm_env` is absent or the entry's `arch` is null (case-executor ran without SLURM tools), fall back to inferring from `device_type`: Grace-based parts (`GB*` / `GH*`) → `aarch64`; everything else → `x86_64`. +3. **Resolve the container image.** If `docker_image` is already set in `job_spec.json`, use it directly. Otherwise read `/jenkins/current_image_tags.properties` and pick the tag matching the partition's CPU arch. +4. **Map GPU → build arch (`-a` flag):** `H100`/`H200` → `90-real`; `B200`/`GB200`/`B300`/`GB300` → `100-real`; `A100` → `80-real`; `L40S` → `89-real`. Default `100-real`. +5. **Compile.** Invoke the `exec-slurm-compile` skill with `repo_dir=`, `partition`, `account`, `container_image=`, `user_root_dir`, `arch`. Wait for completion. +6. **On failure**, do not allocate or run the workload. Report `BUILD_FAILED` with the last 100 lines of the build log. + +`build_project`, `device_type`, `repo_root`, `partition`, `account`, `user_root_dir`, and (optionally) `docker_image` come from `job_spec.json`. + +### Step 0: Allocation Management + +This step runs before any execution. It determines whether to reuse an existing +persistent allocation, create a new one, or fall through to one-shot sbatch. + +#### Step 0A — Release mode + +If `release_allocation=true`: + +1. Read `/work_dirs/.slurm_alloc.json` +2. If file exists and `job_id` is present: + ```bash + scancel + ``` +3. Delete the state file and `.salloc.log` +4. Report: "Allocation released." Stop. + +#### Step 0B — One-shot mode + +If `persistent_mode=false`, skip entirely to Step 1 (One-Shot Path). + +#### Step 0C — Validate existing allocation + +Read `/work_dirs/.slurm_alloc.json`. + +- If state file **does not exist** → go to Step 0D. +- If state file exists, validate the allocation: + ```bash + squeue -j -h -o "%T %L" + ``` + - **RUNNING + remaining > 5 min + params compatible** → **reuse** (skip to Step 0F) + - **RUNNING + remaining <= 5 min** → warn user ("Allocation expiring soon"), `scancel `, delete state file, go to Step 0D + - **RUNNING + params incompatible** → `scancel `, delete state file, go to Step 0D + - **Empty output / error** (job gone) → stale state file, delete it, go to Step 0D + +**Params compatibility check:** +- `partition` must match +- `nodes` in state must be **>=** requested nodes (a 2-node allocation can serve 1-node jobs) +- `docker_image` must match + +#### Step 0D — Justify and prepare allocation + +Before allocating, ensure no orphaned allocations exist and log the reason: + +1. Check for existing persistent jobs: + ```bash + squeue -u $(whoami) -h -o "%i %T %j" --name=-trtllm.persistent + ``` +2. If found → orphan (state file was missing/corrupt). Cancel it: + ```bash + scancel + ``` + Log: "Released orphaned allocation — state file was missing." +3. Log justification: "No reusable allocation found. Allocating node(s) on partition for ." + +#### Step 0E — Allocate new + +Convert `alloc_time_limit` from `HH:MM:SS` to seconds (e.g., `04:00:00` → `14400`). + +```bash +mkdir -p /work_dirs +nohup salloc --partition= --account= \ + --nodes= --time= \ + --job-name=-trtllm.persistent \ + sleep \ + > /work_dirs/.salloc.log 2>&1 & +``` + +Retrieve job ID: +```bash +squeue -u $(whoami) -h -o "%i" --name=-trtllm.persistent +``` + +If no job appears after 10 seconds, read `/work_dirs/.salloc.log` +for errors (bad partition, invalid account, etc.) and report to user. + +Otherwise, poll until RUNNING (every 10s, max 60 polls). Once RUNNING, get +the nodelist: +```bash +squeue -j -h -o "%N" +``` + +**Import container** on all nodes (using the **same container name** as the +`.slurm` script — critical for pyxis reuse): +```bash +srun --jobid= -N --ntasks-per-node=1 \ + --container-image= \ + --container-name= true +``` + +**Warm up filesystem mounts** on all nodes — `ls` each mounted path so +Lustre/NFS metadata is cached for later use: +```bash +srun --jobid= -N --ntasks-per-node=1 \ + --container-name= \ + --container-mounts= \ + bash -c 'for p in ...; do ls "$p" > /dev/null 2>&1; done' +``` +Parse mount targets from `container_mounts` — the right-hand side of each +`host:container` pair. + +**Check GPU status** on all nodes (no container needed — `nvidia-smi` is on +the host): +```bash +srun --jobid= -N --ntasks-per-node=1 \ + bash -c 'echo "=== $(hostname) ===" && nvidia-smi --query-compute-apps=pid,name,used_memory --format=csv,noheader' +``` +- If output shows no processes → GPUs are clean, proceed +- If unexpected processes found → warn user: "GPU processes found on + : . These may interfere with the job." + +**Run install** on all nodes with `--container-writable` (so packages persist +in the named container across srun calls). Install all common requirements +upfront so that subsequent jobs of any workflow type can skip install: +```bash +srun --jobid= -N --ntasks-per-node=1 \ + --container-name= --container-writable \ + --container-mounts= \ + bash -c 'cd && pip install -e . && pip install -r requirements-dev.txt && \ + if [ -f examples/trtllm-eval/requirements.txt ]; then pip install -r examples/trtllm-eval/requirements.txt; fi' +``` +For custom workflow with `skip_install=true`, skip this step entirely. + +**Write state file** `/work_dirs/.slurm_alloc.json`: +```json +{ + "job_id": "", + "container_name": "", + "nodelist": "", + "partition": "", + "account": "", + "nodes": , + "gpus_per_node": , + "docker_image": "", + "container_mounts": "", + "allocated_at": "", + "time_limit": "", + "installed": true +} +``` + +Proceed to Step 1 (Persistent Path). + +#### Step 0F — Reuse path validation + +Allocation is valid. Check if container name matches current request: + +- If `container_name` in state file **matches** `container_name` from + `job_spec.json` → proceed to Step 1 (Persistent Path). Container and install + are already set up. +- If **different** (model changed) → import the new container, warm up mounts, + and install: + ```bash + srun --jobid= -N --ntasks-per-node=1 \ + --container-image= \ + --container-name= true + srun --jobid= -N --ntasks-per-node=1 \ + --container-name= \ + --container-mounts= \ + bash -c 'for p in ; do ls "$p" > /dev/null 2>&1; done' + srun --jobid= -N --ntasks-per-node=1 \ + --container-name= --container-writable \ + --container-mounts= \ + bash -c 'cd && pip install -e . && pip install -r requirements-dev.txt' + ``` + Update `container_name` in state file. Proceed to Step 1 (Persistent Path). + +--- + +### Step 1: Execute + +Two execution paths depending on `persistent_mode`. + +#### Persistent Path (persistent_mode=true) + +**Pre-flight time check** — verify the allocation has enough remaining time: +```bash +squeue -j -h -o "%L" +``` +If remaining time < job's `time_limit` from `slurm_params`, warn user: +"Allocation has left but job expects . The job may +be killed early." + +**Pre-flight GPU check** — verify GPUs are not occupied by leftover processes: +```bash +srun --jobid= -N --ntasks-per-node=1 \ + bash -c 'procs=$(nvidia-smi --query-compute-apps=pid,name,used_memory --format=csv,noheader 2>/dev/null); [ -n "$procs" ] && echo "WARNING: GPU processes on $(hostname): $procs"' +``` +If unexpected processes found, warn user before proceeding. + +**Run the sbatch script** with SLURM env vars — the `#SBATCH` directives are +comments when run via `bash`; the inner `srun` inherits the env vars and uses +the persistent allocation: +```bash +export SLURM_JOB_ID= +export SLURM_JOB_NUM_NODES= +export SLURM_NNODES= +export SLURM_NTASKS= +export SLURM_NTASKS_PER_NODE= +export SLURM_NODELIST= +export SLURM_JOB_NODELIST= +bash 2>&1 | tee /_.log +``` + +Run with `run_in_background`. Container reuse is automatic (pyxis skips +re-import when `--container-name` already exists on the node). The install +step (Step 1 srun) always runs to keep the container up to date. + +**Step cancellation:** If a job hangs or the user wants to abort, cancel just +the srun step without killing the allocation: +```bash +# List active steps: +squeue -s -j +# Cancel a specific step (e.g., step 0): +scancel .0 +``` +The allocation stays RUNNING — new jobs can run immediately. The job name +`-trtllm.persistent` uniquely identifies allocations created by this +skill via `squeue --name=-trtllm.persistent`. + +#### One-Shot Path (persistent_mode=false) + +```bash +sbatch +``` + +Parse the job ID from output: `Submitted batch job `. + +If sbatch fails, report the error immediately and stop. + +### Step 2: Report Submission + +**One-shot mode:** +``` +Job ID: +Script: +Work directory: +Log files: / +Monitor: squeue -j +``` + +**Persistent mode:** +``` +Allocation: (persistent, remaining: