Skip to content

[FlyDSL] [Feature] Tiered persistent radix-select decode Top-K + arch/shape dispatcher - #4355

Open
JH-Leon-KIM-AMD wants to merge 112 commits into
mainfrom
jeongkim/silotiger-699-gfx950-port
Open

JH-Leon-KIM-AMD wants to merge 112 commits into
mainfrom
jeongkim/silotiger-699-gfx950-port

Conversation

@JH-Leon-KIM-AMD

@JH-Leon-KIM-AMD JH-Leon-KIM-AMD commented Jul 23, 2026

Copy link
Copy Markdown

Tiered persistent radix-select decode Top-K + arch/shape dispatcher

Tickets: SILOTIGER-699 (gfx950 decode Top-K) ·
SILOTIGER-629 /
SILOTIGER-683 (the kernel this builds on) —
epic SILOTIGER-606 (GLM-5 MXFP4, MI355X perf uplift)

What this is

  1. The FlyDSL decode Top-K kernel — context-length-independent tiered persistent radix-select
    for the sparse-attention indexer, which calls it ~78×/step. The HIP path it replaces
    (radix_topk_one_block<12,1024>) is an O(N_blocks) radix sort: 369 µs @ 8K → 1,165 µs @ 120K,
    ~15× slower than B200's persistent_topk_kernel. This one is O(topK·log topK).
  2. A dispatcher inside aiter.top_k_per_row_decode, routing there only where it is measured to
    win. Without it the kernel ships unreachable: the public op still calls HIP.
Best cell vs HIP 1.42× gfx950, 2.19× gfx942
Worst in-window cell 0.61× (gfx942, k=512 rows=1 width=163840 seq=16384)
gfx942 shipped window +1371 µs over 360 cells, median 0.976×, 169 won
Same grid, ungated −2998 µs over 928 cells, median 0.861×
Correctness set-equivalence with torch.topk, both arch sweeps

Ungated, this kernel loses on that grid; the gate is what turns it into a win — a routing result as
much as a kernel result.

Lineage

Sami's and Robin's commits ride along because that base is not in main yet. Base branch
RElbers/aiter@samremes/topk-per-row.

who what commits
@samremes the initial FlyDSL kernel (SILOTIGER-683): K=512 graph-capture-friendly tiered persistent radix-select, runtime row_len tiers, row-barrier optimization, long-row cap 48f0a93 555569b 72c7745 5152d4d
@RElbers made it the sole decode path for every K (256/512/1024/2048) by lifting top_k to a compile-time parameter; workspace API, batch-aware tier config, deadlock guard, gfx950 params, non-pow2 K, tier_mode refactor, AOT entry b3a0d97 c0b6909 01741ab 755df3b c06b8db 135ebca c278797 f42bb80 4abc985 c6c30d8 ← base
@JH-Leon-KIM-AMD gfx950 tuning (below), the dispatcher, the workspace cache, dispatch counters, the gfx942 window, the correctness harness, and the move onto aiter's buffer_ops/vector shims after #4501 below

The generic tiering serialized wide decode batches into sequential co-resident waves. The switches
that fix it are gfx950-only, since gfx942 runs the frozen configuration: short_max +
dead_block_trim + midbatch cap
(d2d2ad6 5383b16 d783300),
blocks_per_row floor 1 for the barrier-free short tier (2fc5ddc),
row_proportional_parts (a87146d),
batch_coresident_cap (197eda0),
early_stop (d839939), and a rows>63 / L≤65536
fold back to bpr=1 (e7dde59).

The gate

Five checks, cheapest first, all on host values. Per-row lengths live in seqLens on the device,
and reading them would sync every decode step.

check rejects to
1 AITER_DISABLE_FLYDSL_TOPK_DECODE HIP
2 arch has a row in the gate table HIP
3 k in ks, and numRows inside the row cap that logits.shape[1] earns HIP
4 stride1 == 1, stride0 == logits.stride(0), next_n >= 1, logits.ndim == 2 HIP
5 flydsl importable HIP

Step 4 is a contract check, not a filter: FlyDSL raises where HIP accepts.

# (min padded width, max rows) bands, widest first.
"gfx950": _DecodeGate(((163840, 15), (131072, 9)), ks={256, 512, 1024, 2048}),
"gfx942": _DecodeGate(((163840, 18), (131072, 11)), ks={256, 512, 1024, 2048}),

The first band whose width a call reaches sets its cap, and a call reaching none is refused. Each
cap sits two rows below its measured crossing.

graph-replay sign change width 163840 width 131072
gfx950 rows 17 → cap 15 rows 11 → cap 9
gfx942 rows 20 → cap 18 rows 13 → cap 11

A wider buffer is more work per row, so cooperation keeps paying for more rows. One number would
refuse a wide band it wins, or admit a narrow band it loses.

Width asks "long-context model", not "long request". Callers size the buffer to the model's max
context — vLLM's indexer builds logits as (batch * next_n, max_model_len) — and under graph
capture the request length does not exist yet. Sweeping width at fixed real length moves the margin
under 2.2 µs, so the check works only because a request cannot outrun its buffer.

Thresholds come from graph replay only. Eager charges HIP 11.7 µs of host work per call against
FlyDSL's 2.6 µs, mostly an un-memoized topk_ob_workspace_size query; its kinder column (in-window
median 1.135×, 349/360 won) is that artifact.

env effect
AITER_DISABLE_FLYDSL_TOPK_DECODE=1 every shape back to HIP
AITER_FLYDSL_TOPK_ARCHS narrows which archs the table applies to
AITER_FLYDSL_TOPK_MAX_ROWS replaces the cap in every band
AITER_FLYDSL_TOPK_MIN_WIDTH also flattens the table to one band
AITER_FLYDSL_TOPK_COUNT=1 tallies flydsl vs hip per worker; an earlier PR-image A/B measured ~0% with HIP on both arms

Why the gate lives in AITER. The alternative duplicates arch detection, availability checks,
shape thresholds and the HIP fallback in every calling framework. gemm_op_a8w8.py does the same.

Host cost is the same order as the kernel win, so two things came along:

  • workspace is a parameter, with a buffer cached on (device, stream, size rounded to pow2)
    worth ~2.4 µs, and returning nothing under graph capture.
  • The stream context manager around workspace.zero_() is skipped when no stream was asked for.

Measurements

conditions
gfx950 rocprofv3 kernel-only min µs, k=2048, --boost-ms 500 --warmup 10 --iters 40
gfx942 928 cells (k × rows × width × seq), eager and graph replay, both arms through this public entry point so each pays the gate and the workspace sizing; dispatch counters checked per cell
regimes A = uniform padded length, B = per-row random causal length, fixed seed

gfx950 vs HIP. hip/ours > 1.00 is faster. B tracks A, so the win is not a padding artifact.

rows L ours (µs) hip (µs) hip/ours (A) hip/ours (B)
1 8K 10.0 10.1 1.01× 1.04×
4 8K 10.5 10.3 0.98× 1.04×
1 65K 21.1 30.1 1.42× 1.42×
4 65K 27.0 30.0 1.11× 1.10×
8 65K 30.0 30.6 1.02× 1.08×
1 256K 31.2 33.4 1.07× 1.05×
4 256K 42.1 55.6 1.32× 1.24×
8 256K 48.3 56.7 1.17× 1.16×

gfx950 through the dispatcher, width 163840, 200 back-to-back calls with one sync at the end.
hip / dispatch:

k rows seq 4,096 65,536 163,840
2048 1 0.97 1.16 1.05
2048 4 1.16 1.32 1.25
2048 8 1.19 1.22 1.17
2048 16 1.37 1.11 1.03

10 win, 1 tie, 1 marginal (0.97 is ~0.5 µs, inside run-to-run noise). The rows=16 line predates
the staircase; the cap here is 15, so it routes to HIP now.

gfx942 length curve, graph replay, median over k × rows, in the window that shipped when this
was measured (width ≥ 131072, rows ≤ 16). The advantage is a function of the real request length,
which the gate cannot see.

seq_len 4K 16K 32K 48K 64K 96K 128K 160K
HIP / FlyDSL (higher = we win) 0.80× 0.73× 0.86× 0.91× 1.09× 1.28× 1.54× 1.64×
cells won 0/48 0/48 3/48 6/48 40/48 48/48 48/48 24/24
  • Profitable because the wins are larger than the losses (mean +13.3 against −4.6 µs), not because
    most cells win. Break-even needs long requests to be 25.6% of traffic.
  • The staircase came later: it drops rows 12–16 at width 131072 and admits rows 17–18 at 163840,
    both measured as gains (+2.41 and +1.99 µs per cell). The shipped window scores no worse, but this
    table has not been re-run.
  • gfx942 is an SR-IOV VF with no clock locking, so band noise is 2–6 µs against gfx950's 0.3 µs.
    Near-threshold cells were repeated in three independent runs.

vs Robin's gfx950 base, informational only — that base was not tuned for wide batch and this
path trails HIP there anyway: rows=1 1.15–1.19×, rows 4–8 parity, rows ≥ 16 2.0×–10.5×. Full sweep
in the SILOTIGER-699 results comment.

Correctness

Buffers are padded past the real length with the tail poisoned, so a kernel ignoring seqLens fails
loudly instead of passing by accident.

arch result grid
gfx950 504/504 A 252/252 + B 252/252, over K{512, 2048} × rows{1, 4, 16, 32, 64, 128, 256} × L{8K, 16K, 32K, 64K, 120K, 256K} × dist{random, ties, 10LSB}. No out-of-bounds reads
gfx942 all 928 cells, both arms, two independent runs plus k{256, 1024} × rows{1, 4, 8} × L{131072, 163840} for the newly admitted k, and rows{12, 16} × k{256, 512, 1024, 2048} × L{4096, 70000, 131072} for the row band above the sweep's samples

Unit tests on gfx950: 195/195 (dispatcher + kernel, including the stable exact-order cases). Two of the 33 pin the staircase —
the bands descend with width, and one row count is refused at 131072 and admitted at 163840.

Run: pytest op_tests/test_topk_decode_dispatch.py,
pytest op_tests/flydsl_tests/test_flydsl_topk_per_row_decode.py,
python op_tests/topk_decode_correctness.py, and
python op_tests/benchmark_topk_per_row_decode.py under rocprofv3.

stable=True (ordered emit)

Previously stable=True forced HIP: FlyDSL returned an unordered set. This commit
adds an ordered emit to the tiered kernel and routes stable=True through
the same gate bands as the unordered path (ordered=stable forwarded to
FlyDSL).

Implementation highlights:

  • One contiguous vec-block run per workgroup; run counts from pass histograms
    (s_own_hist) — no extra row read for a count pass.
  • ordered=True disables the short-tier atomic scatter and early_stop
    (both assume unordered output); persistent path covers all tiers.
  • Launcher cache key includes ordered (different kernel).

Correctness contract: exact index list equality, ascending with
smallest-index tie-break on the kth value — not set equivalence. Tie-dense
inputs in tests.

check gfx950 result
pytest op_tests/flydsl_tests/test_flydsl_topk_per_row_decode.py + op_tests/test_topk_decode_dispatch.py 195/195
python op_tests/test_topk_per_row_stable.py ALL PASS
Final bench (3 rep, all shapes) exact failures 0

Performance (stable, k=2048, 3-rep median µs, vs #5011 ordered / HIP):

rows × L ours 5011 ours/5011 HIP ours/HIP
26 × 700K 126 121 1.04 933 0.14
26 × 1M 157 149 1.05 1335 0.12
28 × 700K 128 127 1.01 933 0.14
28 × 1M 159 160 0.99 1335 0.12
32 × 700K 134 128 1.05 936 0.14
32 × 1M 165 166 1.00 1336 0.12

Gate region (26–32 × 700K–1M): 5011 parity (0.99–1.05×), HIP 7–8×.
Stable overhead vs unordered: 1.12–1.18× in that region. Only regression:
rows=8 × 700K/1M ~1.11× vs 5011 (gate-outside; HIP ~92/118 µs there).

Raw: JHK_TASK/.../results_this_node/final/stable_fused_rep{1,2,3}.jsonl.

Follow-ups

why it is left blocker
Kernel self-reset the barrier-counter zero_() is 3.7 µs of launch latency per call, the largest item left HIP's multi-block kernel already self-resets; a side stream measured slower
gfx942 kernel tuning the four gfx950 switches are frozen there; enabling them flipped rows=32 from 0.79× to 1.06× and widens the window needs a full correctness pass
HIP multi-block for decode the C++ decode entry is one-block only, though its own crossover says multi-block should win where FlyDSL does graph capture freezes stride0, this gate's information problem

See also

  • SILOTIGER-629 — topk algorithm optimization (Robin, Implemented)
  • SILOTIGER-683 — K=2048 tiered persistent decode path (Sami/Robin, Implemented)
  • SILOTIGER-636 — gfx942 indexer original (fuse 4 indexer kernels)
  • vllm-project/vllm#50470 — the caller-side change that routes the sparse indexer through this op

samremes and others added 27 commits June 25, 2026 02:50
Introduce FlyDSL decode TopK implementations and benchmarking coverage so K=512 graph-captured paths can choose measured runtime tiers while preserving correctness against reference TopK behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…pace API. Make the tiered persistent radix-select the sole decode TopK path for every supported K (256/512/1024/2048) by lifting top_k to a compile-time parameter, and drop the single-CTA fallback. Callers now manage scratch through flydsl_top_k_per_row_decode_workspace_size and an optional workspace= argument, with a batch-aware tier config and a deadlock guard that clamps cooperating workgroups (or forces the barrier-free short tier) to keep the non-cooperative row barrier safe.
…uce the number of compilations. Empirically confirmed perf neutral. Rename _compile_launcher -> _build_launcher, because the function doesn't actually do the compilation.
… (bpp==11)

Prerequisite for batch_coresident_cap (grid=1 fold). Kernel previously
rejected blocks_per_row < 2; now allows 1 when the barrier-free single-
workgroup short tier exists (tier_mode auto/short + bits_per_pass==11),
keeping the >=2 floor for mid/long forced modes and bpp==10.

Also parameterize the vec-block ceil-div shift on LOAD_VEC
(LOAD_VEC_LOG2) instead of a hardcoded 2 (no-op at LOAD_VEC=4).

Dispatch output unchanged (_kernel_config identical to Phase A); this
only opens the path for Phase B2. gfx942 unaffected.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cap each row's cooperating workgroups by its actual coverage need
(ceil(row_len / items_per_block), floored at 2) in the mid/long tiers,
undoing the pow2 round-up of blocks_per_row for rows whose coverage is
not a power of two. Only reduces active parts (a min), so results are
identical.

Kernel: add row_proportional_parts param + _rpp name tag + the row_cover
cap after mid/long_parts. Dispatch: emit row_proportional_parts in the
config (gfx950 default on, env FLYDSL_TOPK_TIERED_RPP override, gfx942
frozen), passed through the existing **kernel_config spread.

Verified: correctness lossless (random/ties/10LSBits x rows{8,32} x
L{49k,120k,256k} all PASS); perf rows=1 L=49152 22.44->22.20us (-1.1%,
parts 16->12), L=98304 28.64->27.84us (-2.8%, 32->24); null cell
unchanged. blocks_per_row unchanged; gfx942 frozen.

Co-authored-by: Cursor <cursoragent@cursor.com>
Cap the launch grid width so blocks_per_row*num_rows stays within one
co-resident wave (envelope = CU*occ), so the persistent barrier does not
serialize rows into sequential waves. When even a width-2 grid overflows
(budget<2), collapse to grid=(1, num_rows): every row runs the
barrier-free single-workgroup short tier (needs the B0 bpr==1 kernel
path). Also fold the all-short case (max_model_len<=short_max) to grid=1.

- Add _CDNA_OCCUPANCY / _COCAP_OCC2_MAX_ROWS (occ=2 up to 32 rows, else 1).
- Make tiered_mid_max a local so the fold can raise it with short_max
  (kernel requires mid_max >= short_max when force_single_wg lifts
  short_max to L).
- gfx950 only (env FLYDSL_TOPK_TIERED_BATCH_CAP / _OCC override);
  gfx942 frozen.

Verified: correctness lossless (rows{64,128,256} x L{8k,65k,120k} x
{random,ties} all PASS); perf rows=256 L=120000 364.11->101.52us (-72%),
rows=128 L=65536 117.56->78.56us (-33%); folded config builds; gfx942
frozen (arch-patch check).

Co-authored-by: Cursor <cursoragent@cursor.com>
Bring the enhanced correctness harness from df2c666 onto the robin-based
port branch (production kernel/dispatch already ported in A/B):
- regime B (--seq-rand-min-frac): per-row random causal seq_len with
  per-row poison tail, compared against torch.topk over each row's valid
  region only (decode causal geometry, next_n slots)
- _has_built_flydsl() guard so a source-only FlyDSL checkout cannot shadow
  the installed runtime
- --k accepts multiple values; total/regime-labelled output

Verified on gfx950: regime A 54/54 and regime B 54/54 across
random/ties/10LSBits x rows{1,4,16} x L{8192,65536,131072} x k{512,2048}.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reapply ddaeffa on the robin-based port branch: time_kernel allocated
single-element start/end event lists but indexed them per iteration, so
any --iters > 1 raised IndexError and recorded every cell as an error.
Size the event lists to iters.

Verified on gfx950: --kernels aiter_hip --iters 5 completes the
CUDA-event path (41.8+/-3.8 us over 5 samples), no IndexError.

Co-authored-by: Cursor <cursoragent@cursor.com>
Kernel (topk_per_row_decode_tiered.py):
- add early_stop param + _es kernel-name tag (cache separation)
- add early-stop write helpers (process_loaded_early_vec /
  early_write_vec_block / early_write_all): take the boundary bucket whole
  via prefix_for_key(key, prev_start_bit) <= kth_bits (HIP-mb
  previous_bits <= kth_value_bits), same 4x-staged unroll as the normal
  last-pass write
- persistent loop: when early_stop, run passes[0..n-2], and if the
  boundary bucket is taken whole (local_len == local_k) skip the final
  radix pass and write directly; else run it. The early flag is computed
  from the merged histogram so it is identical across a row's blocks ->
  the final row_barrier is entered/skipped by all blocks together (no
  deadlock).

Dispatch (topk_per_row_decode.py):
- es_on = _env_int(FLYDSL_TOPK_TIERED_ES, 1 if gfx950 else 0)
- early_stop = bool(es_on) and num_rows <= 1 (gated: pays off only in the
  single-sequence long tier; neutral/loss at rows>=2)

Verified gfx950: correctness 768/768 vs torch.topk (ES on/off x regime
A+B x rows{1,4,16,32} x L{8k,65k,120k,256k} x k{256..2048} x
random/ties/10LSBits, poison over-scan checked). Perf (rocprofv3 min):
rows=1 -14..17% (L120k k2048 29.76->25.68us), rows=4 (gated off) 0
regression.

Co-authored-by: Cursor <cursoragent@cursor.com>
C3 lossless A/B (port vs our original topk-robin branch) surfaced a
regression at rows=128 L=65536: port 78.6us vs ours 49.6us. Root cause:
the A3 midbatch_coord_cap port omitted the first rule (rows>63, L<=65536
-> cap=1) and floored the cap at 2, because the kernel had no bpr=1
single-workgroup path at the time. Phase B (B0 bpr=1 support + B2 fold)
added that path, so restore the rule and lower the floor to 1.

The rule caps blocks_per_row to 1 WITHOUT force_single_wg (short_max
stays < L), so the row runs the mid tier with a single cooperating block
(barrier-free, HIP one-block shape) instead of a width-2 cooperative grid
that serializes a wide batch into extra co-resident waves.

Verified gfx950:
- config parity vs ours across rows{1..256} x L{8k..256k}: identical
  except the known benign pow2 rounding at rows=1 L=120000 (30->32,
  perf-neutral per robin's pow2 commit).
- correctness 24/24 (regime A+B, random/ties/10LSBits) on the newly
  folded rows{64,128,256} L=65536.
- perf: rows=128 L=65536 78.6->49.8us (== ours 49.9), rows=64 47.4us
  (== ours 47.5); ~5x faster than robin (251us).

Co-authored-by: Cursor <cursoragent@cursor.com>
…ocstring

Co-authored-by: Cursor <cursoragent@cursor.com>
…699-gfx950-port

# Conflicts:
#	aiter/aot/flydsl/common.py
#	aiter/ops/flydsl/__init__.py
@github-actions

Copy link
Copy Markdown
Contributor

🏷️ CI Guide

Runs automatically on every PR:

  • ✅ Pre-checks (submodule verification, code formatting)
  • ✅ Aiter op tests (gfx942 + gfx950)
  • ✅ Triton tests on MI35X (only when aiter/ops/triton/** or related paths are changed)

Extended tests (opt-in via labels):

Label Tests
ci:triton-300x Run an additional Triton test job on MI300X in PRs; main branch always runs both MI35X and MI300X
ci:sglang SGLang integration tests: DeepSeek-R1-MXFP4 accuracy, Qwen 3.5 accuracy
ci:atom ATOM benchmark: DeepSeek-R1-0528, GPT-OSS-120B
ci:atom_full ATOM accuracy suite for PR and main models from ATOM models_accuracy.json
ci:vllm vLLM benchmark: GPT-OSS-120B, DeepSeek-R1-0528, Kimi-K2.5
ci:all All standard extended tests (excludes ci:atom_full)

Only add ci:atom_full for FlyDSL or Triton upgrades.
Add labels via the sidebar or gh pr edit 4355 --add-label <label>

@JH-Leon-KIM-AMD
JH-Leon-KIM-AMD marked this pull request as ready for review July 23, 2026 14:20
@JH-Leon-KIM-AMD
JH-Leon-KIM-AMD requested a review from a team July 23, 2026 14:20
The shipped gfx950 window was read off a sweep that called the HIP arm through
its raw binding while FlyDSL went through the public wrapper, so HIP carried
host cost the comparison then credited to us. Re-running both arms through
top_k_per_row_decode over 1276 cells, twice independently, moves every field
except min_width.

Under graph replay the row bands decay monotonically and cross zero at 14, so
the cap drops from 16 to 12 rather than sitting on the crossing. All four
AOT-precompiled k values land within +286 to +463us of each other and none is
separable, so the window widens from {2048} to all four. The rows==2 carve-out
goes with them: sampling rows 3 shows 1-2-3-4 decaying monotonically with rows 2
above the line, so the dip the old table encoded was an artifact of the eager
column that host cost had inflated. excluded_rows had no other user, so the
field is dropped from _DecodeGate.

The gate comment loses its measurement narrative. Per-cell figures, noise floors
and cell counts go stale on the next re-run and cannot be re-verified from the
source file, so they live in the investigation instead; what stays is why the
window exists, why width has to stand in for request length, and where to read
the numbers.
These files accumulated block comments that retold how each tuning knob was
arrived at: what the grid looked like before the trim, which batch sizes were
measured, the worked example behind the deadlock guard's threshold. None of it
is checkable from the source file, and the figures in it go stale silently the
next time the kernel is re-measured on different hardware.

What each comment keeps is the part a maintainer gets wrong without it: that a
cap is a min and therefore cannot change results, that blocks_per_row == 1 needs
the kernel's single-workgroup launch path, that mid_max must stay above
short_max, that the early-stop flag is derived from the merged histogram so
every block of a row agrees on it and the last barrier stays deadlock-free.

Comments only. The three kernel modules parse to an identical AST with
docstrings stripped, and the dispatch suite still passes 24/24.
Copilot AI review requested due to automatic review settings August 24, 2026 16:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

aiter/aot/flydsl/common.py:175

  • In _compile_one_config_for, OpKind.GROUPED_MOE is handled twice: first by importing from .grouped_moe import compile_one_config, and then again by returning a stub lambda. The second branch is unreachable and the comment contradicts the actual behavior (grouped_moe AOT appears wired up). This should be de-duplicated (either remove the stub branch, or change it to the intended missing kind).
def _compile_one_config_for(kind: OpKind) -> Callable[..., dict[str, Any]]:
    if kind is OpKind.MOE:
        from .moe import compile_one_config
    elif kind is OpKind.MXFP4_MOE:
        from .mxfp4_moe import compile_one_config
    elif kind is OpKind.GEMM:
        from .gemm import compile_one_config
    elif kind is OpKind.GROUPED_MOE:
        from .grouped_moe import compile_one_config
    elif kind is OpKind.CHUNK_GDN_H:
        from .chunk_gdn_h import compile_one_config
    elif kind is OpKind.TOPK:
        from .topk import compile_one_config
    elif kind is OpKind.GROUPED_MOE:
        # grouped_moe AOT not wired up yet (no jobs are ever collected); keep a
        # trivial stub so the dispatch is total.
        return lambda **_kw: {}
    else:
        raise ValueError(f"unknown FlyDSL AOT kind: {kind!r}")

Copilot AI review requested due to automatic review settings August 24, 2026 16:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Comment thread aiter/ops/topk.py Outdated
Comment thread aiter/aot/flydsl/topk.py
Comment thread aiter/aot/flydsl/topk.py
The rows threshold is not one number: it moves with the width of the score
buffer. A wider buffer means more work per row, so the multi-block kernel's
cooperation keeps paying for more concurrent rows before the batch alone fills
the machine. A single cap is a compromise between two crossings and loses at
both ends -- it refuses rows the wide band still wins, and admits rows the
narrow band has already lost.

_DecodeGate therefore carries (min width, max rows) bands rather than a scalar
min_width and max_rows. A call is capped by the first band whose width it
reaches and one that reaches none is refused, so a single-band table reproduces
the old behaviour exactly.

Crossings read off graph replay at repeat 5, with an independent repeat-3 run
reproducing every sign:

           width 163840   width 131072
  gfx950         17 rows        11 rows
  gfx942         20 rows        13 rows

Each step sits two rows below its own crossing, as the scalar caps did, giving
gfx950 ((163840, 15), (131072, 9)) and gfx942 ((163840, 18), (131072, 11)).
Summed over every measured cell in the window that is +158us on gfx950
(2204.7 -> 2362.8) and +199us on gfx942 (2895.7 -> 3094.3), with the worst cell
moving -9.9 -> -12.9 and -11.7 -> -11.9 respectively.

The dispatch tests straddle all four steps and pin that one row count is refused
at 131072 and admitted at 163840, which a table flattened back to a single cap
would fail while every other case stayed green.
Copilot AI review requested due to automatic review settings August 25, 2026 08:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

aiter/ops/topk.py:598

  • _should_use_flydsl_decode() calls get_gfx_runtime() without guarding exceptions. If rocminfo probing fails or the live arch is not in the known map, get_gfx_runtime raises and the whole top_k_per_row_decode op will error out instead of safely falling back to the HIP kernel (this gate is an optimization, so it should be fail-open). Suggestion: wrap the get_gfx() call (and possibly the gate lookup) in try/except and return False on any exception so decode remains functional even when arch detection is unavailable.
    arch = get_gfx()
    if arch not in _FLYDSL_TOPK_DECODE_ARCHS:
        return False
    gate = _FLYDSL_TOPK_DECODE_GATES.get(arch)
    if gate is None:

aiter/ops/flydsl/kernels/topk_per_row_decode_tiered.py:3

  • This new kernel module is missing the standard SPDX license header / copyright header used throughout aiter’s FlyDSL kernel sources (e.g., other files in aiter/ops/flydsl/kernels start with # SPDX-License-Identifier: MIT). Please add the SPDX header at the top of the file for consistency and license compliance automation.
"""FlyDSL decode TopK-per-row kernel (tiered persistent multi-block radix-select)

Computes an unordered Top-K index set per decode row, fusing a single-workgroup and

…host's

The AOT CSV names its target twice: cu_num picks the arch string, and the same
number is the CU count that target runs. Only the first was used. Both
_kernel_config and _build_launcher were called without cu_count, so each
resolved it from torch.cuda.current_device() -- the machine running the build,
which for a cross-compiled row is the wrong device entirely.

The count is not cosmetic. It selects bits_per_pass, and it scales the
co-residency envelope that sets blocks_per_row and the tier caps.

On a 256 CU host the gfx942 rows (cu_num=304) enumerate 38 configs where a
304 CU runtime asks for 36, so 8 of 232 jobs compile something nothing loads.
Below 128 CU it is not waste but a miss: bits_per_pass drops to 10 and all 36
gfx942 configs differ from what runtime selects, so every AOT kernel for that
arch is dead and the first call of each shape pays a JIT compile.

Nothing in the shipped window changes. Within the gate's row cap the config sets
were already identical, which is why this never showed up in testing.
Copilot AI review requested due to automatic review settings August 25, 2026 13:05
Every other kernel module under aiter/ops/flydsl/kernels carries one, including
the non-tiered sibling this file was split from. The docstring stays the module
docstring: the header is a comment above it, not a replacement for it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread aiter/ops/topk.py
JH-Leon-KIM-AMD and others added 2 commits August 25, 2026 13:40
FlyDSL validates dtype and device; the HIP one-block kernel casts to float*
and int* and reads whatever is there. With the check left to the branches, one
caller bug raises on a gated shape and returns a wrong answer on the next
width over, so the diagnosis depends on which kernel the gate happened to
pick. Checking once up front makes it the same error either way.

The other contracts the review raised -- packed seqLens, indices stride
(k, 1), stride0 matching the tensor -- are already screened: the gate refuses
those shapes and they fall back to HIP exactly as they did before this branch.
Only dtype and device were unscreened. The two checks cost 0.35 us of host
time against the op's 41 us, measured at rows=4, width=163840, k=2048 over
40,000 calls; the full block would have cost 1.5 us, more than the per-cell
margin the row caps are set by.
JH-Leon-KIM-AMD and others added 4 commits August 29, 2026 07:37
stable=True now routes through the same gate bands and forwards ordered
emit to FlyDSL with histogram-fused placement, so tensor-parallel ranks
get identical ascending index lists without falling back to HIP.

Co-authored-by: Cursor <cursoragent@cursor.com>
The CI pre-check pins psf/black@stable, now 26.5.1, which wraps the
assert condition rather than its message. Formatting only.

Co-authored-by: Cursor <cursoragent@cursor.com>
The compact candidate-buffer path for the gfx950 decode top-k, measured over steps
17 to 20 of the port. Passes behind the fill read a buffer of survivors instead of
re-scanning the row, and histogram_certificate lets a pass settle its digit by
proving the merge finished rather than waiting at the row barrier.

Includes the workgroup barrier at the end of flush_local_histogram. Without it the
certified path runs the flush straight into load_global_histogram, which writes the
same s_hist the flush is still reading and barriers only at its end, so a slower
thread reads back the merged global count and adds that instead of its own
contribution. The fill pass then waits on a total the corrupt histogram cannot
reach and spins CERTIFICATE_MAX_SPINS, which is 4.28 s. Reading both histograms
back out of the workspace after a stalled launch and counting the same row on the
host is what identified it: pass 0 matches all 2048 bins exactly every time, and
the fill histogram is off in both directions, -28 to +15, which only an added
foreign count explains. 0 freezes in 200,000 launches with the barrier against 4
within the first 546 without, at no cost -- 257.7 us against 257.1 at 128x1M
unordered.

certificate_publish_total, certificate_fill_pass and certificate_trace default to
off or neutral. The first two were fixes for the freeze written before the cause
was known and are superseded by the barrier; publish_total is still worth measuring
on its own merits. certificate_trace is diagnostic only and costs nothing when off.
Copilot AI review requested due to automatic review settings September 8, 2026 17:22
@github-actions github-actions Bot added the FlyDSL label Sep 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The public dispatch path in aiter/ops/topk.py has two verified fallback/stride-handling bugs that can cause unexpected exceptions or incorrect results instead of clean HIP fallback.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 10/11 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread aiter/ops/topk.py
Comment on lines +786 to +789
if stride1 != 1 or logits.stride(1) != 1:
logits = logits.contiguous()
stride0, stride1 = logits.stride()

Comment thread aiter/ops/topk.py
Comment on lines +19 to +23
try:
from .flydsl.utils import is_flydsl_available as _is_flydsl_available
except ImportError:
return False
return _is_flydsl_available()
@samremes

samremes commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Agent review: AITER PR #4355 — tiered persistent decode Top-K

Reviewed: #4355
Title: [FlyDSL] [Feature] Tiered persistent radix-select decode Top-K + arch/shape dispatcher
Local worktree: /home/samremes/dev/aiter-pr4355-stageb-baseline
Base: origin/main @ f0321c0e8927d1d90a29385433f71e592b1c51f5
Merge base: 4ad99832823dde2315b361cbd3b54b1c5c12acd5
Head: f19d30a6113d848f32e894297ae0b8cca5bd6afe
Dirty files in this worktree: none
Review date: 2026-09-10
Skills applied: FlyDSL kernel authoring, FlyDSL kernel cleanup, FlyDSL layout algebra, AITER operation testing

This document is a read-only review. I inspected origin/main...HEAD, the pinned FlyDSL 0.3.2 package, the AOT path, the dispatcher, and both new test files. I ran Python bytecode compilation, Ruff, eight host-only dispatcher tests, and AOT job discovery. I did not run a GPU kernel test or benchmark.

Verdict

Do not merge this revision. The kernels can return indices outside the logical row when a device sequence length exceeds the logits width. The new compact kernel is also disconnected from runtime dispatch, AOT, and tests.

The public wrapper has useful dtype, device, stride, workspace, and stream checks. The host-only dispatcher boundary tests pass. The change still needs one correctness fix and material integration, layout, and test cleanup.

Review context

Item Value
Change class New kernel and operation; performance and tuning change; AITER dispatch and AOT integration
Targets gfx942 and gfx950; wave64; 1024 threads per workgroup
Dtypes fp32 logits; int32 sequence lengths, indices, histograms, and counters
Shape regime Decode rows; k in 256, 512, 1024, or 2048 through the public gate; long padded widths from 131072
Input layout Rank-2 logits with column stride 1; packed sequence lengths; packed (numRows, k) output; contiguous int32 workspace
Expected fallback Existing HIP one-block decode path outside the architecture, width, row-count, k, availability, or stride gate
Declared FlyDSL pin flydsl==0.3.2 in requirements.txt and pyproject.toml
Imported FlyDSL 0.3.2 from /opt/venv/lib/python3.14/site-packages/flydsl
Diff vs base 11 files, +7036 / -15

Changed files:

  • aiter/aot/flydsl/common.py (AOT operation registration)
  • aiter/aot/flydsl/topk.py (Top-K AOT job generation and compilation)
  • aiter/configs/topk_decode_aot.csv (AOT shape and architecture matrix)
  • aiter/ops/flydsl/__init__.py (FlyDSL exports)
  • aiter/ops/flydsl/kernels/topk_per_row_decode.py (standalone one-workgroup kernel)
  • aiter/ops/flydsl/kernels/topk_per_row_decode_tiered.py (runtime-selected tiered kernel)
  • aiter/ops/flydsl/kernels/topk_per_row_decode_tiered_compact.py (new compact tiered kernel and tuning rules)
  • aiter/ops/flydsl/topk_per_row_decode.py (configuration, validation, workspace, and launch wrapper)
  • aiter/ops/topk.py (public architecture and shape dispatcher)
  • op_tests/flydsl_tests/test_flydsl_topk_per_row_decode.py (direct FlyDSL pytest suite)
  • op_tests/test_topk_decode_dispatch.py (public dispatcher pytest suite)

FlyDSL 0.3.2 provides the APIs used in the layout recommendations below. I verified make_layout, make_view, idx2crd, crd2idx, flat_divide, zipped_divide, slice, make_tiled_copy_tv, copy, SharedAllocator, rocdl.make_buffer_tensor, rocdl.BufferCopy128b, and UniversalAtomicAdd in the installed package.

Findings

Findings are ordered by severity. Each finding has a location, a trigger, a reason, and a proposed fix.

P1 — Sequence lengths beyond the logical width select data from outside the row

Where: aiter/ops/flydsl/kernels/topk_per_row_decode.py lines 182-191, load_row_vec (linear row load), and lines 273-299, row_len and vec_blocks_i32 (lower clamp only); aiter/ops/flydsl/kernels/topk_per_row_decode_tiered.py lines 319-348, descriptor and row geometry, and lines 533-539, load_row_vec; aiter/ops/flydsl/kernels/topk_per_row_decode_tiered_compact.py lines 955-984, descriptor and row geometry, and lines 1225-1231, load_row_vec; aiter/ops/flydsl/topk_per_row_decode.py lines 562-575, _validate_inputs (sequence storage validation without a value bound).

Trigger: A seqLens entry produces row_len > logits.shape[1]. For row zero of a multi-row contiguous tensor, the scan reads the next row as part of row zero. For the final row, the bounded descriptor returns zero for out-of-range loads, but col_i32 < row_len still marks those zero values as valid. Negative valid logits can therefore lose to synthetic zeros. The kernel can emit a column index that is greater than or equal to the logical width.

Reason: Each kernel clamps row_len only at zero. The kernel does not receive the logical width. The descriptor byte count is the physical row-stride footprint, not the logical per-row bound. A descriptor bound therefore cannot prevent an inner-row overrun into the next physical row.

Proposed fix: Pass max_model_len = logits.shape[1] as an explicit runtime kernel argument. Clamp row_len to [0, max_model_len] before tier selection and before vec_blocks_i32 is computed. Apply the same contract to every retained kernel. Add a regression with two rows, negative logits, and one sequence length larger than the logical width. Verify that no output index reaches the next row or exceeds the width.

P2 — The new compact kernel is not part of the operation

Where: aiter/ops/flydsl/kernels/topk_per_row_decode_tiered_compact.py lines 311-552, compact host policy, and lines 597-2810, create_topk_per_row_decode_compact_kernel; aiter/ops/flydsl/topk_per_row_decode.py lines 452-490, _build_launcher (always calls create_topk_per_row_decode_tiered_kernel); aiter/aot/flydsl/topk.py lines 50-106 and 110-183, AOT job creation and compilation (both call the non-compact configuration and launcher); op_tests/flydsl_tests/test_flydsl_topk_per_row_decode.py lines 1-710 and op_tests/test_topk_decode_dispatch.py lines 1-326 (no compact builder or compact policy call).

Trigger: Every public call, direct FlyDSL call, AOT build, and new test in this revision.

Reason: Repository-wide reference search finds the compact symbols only in their defining file. The latest commit adds 2810 lines of kernel and tuning code, but no caller can select or compile it. The standalone builder in aiter/ops/flydsl/kernels/topk_per_row_decode.py lines 47-461 is also unreferenced. The change therefore carries three complete implementations while only the non-compact tiered implementation is reachable. Ruff also reports 17 errors in the disconnected compact file, including unused loop results and self-comparisons.

Proposed fix: Decide which implementation defines the current operation. If the compact kernel is the replacement, connect its shape policy to the public wrapper, workspace sizing, cache key, AOT jobs, and canonical behavior test. Prove correctness and matched performance first. Then delete the superseded tiered and standalone implementations, their selectors, and stale configuration. If the compact kernel is not ready, remove it from this PR and land it only with its integration and verification.

P2 — The new kernels bypass FlyDSL layout ownership for their core mappings

Where: aiter/ops/flydsl/kernels/topk_per_row_decode_tiered.py lines 275-283, thread-to-wave mapping; lines 319-348 and 533-539, raw buffer resources and row offsets; lines 388-418, workspace and pointer offsets; lines 560-569, thread-to-bin-pair mapping; lines 620-633, histogram reload; lines 967-980 and 984-1027, block/thread/vector ownership; aiter/ops/flydsl/kernels/topk_per_row_decode_tiered_compact.py lines 910-918, 955-984, 1027-1067, 1225-1236, 1252-1264, 1440-1459, and 2058-2103, equivalent mappings plus compact ownership; aiter/ops/flydsl/kernels/topk_per_row_decode.py lines 103-123, 151-191, 230-233, and 273-320, equivalent standalone mappings.

Trigger: All radix scans, histogram scans, output writes, and compact candidate scans.

Reason: These new kernels use fx.SharedAllocator, but they express most global and ownership mappings through buffer_ops, raw element formulas, fx.Index, raw LLVM pointers, and repeated division and remainder. FlyDSL 0.3.2 has native representations for these regular mappings. The current form makes units and ownership depend on repeated formulas across three implementations. It also makes changes to vector width, block size, workspace regions, or output packing unsafe.

Proposed fix: Rewrite one retained kernel around these exact mappings:

  • Use thread_layout = fx.make_layout((16, 64), (64, 1)) and wave, lane = fx.idx2crd(thread_x, thread_layout).unpack() for the 1024-thread wave64 decomposition.
  • Use bin_owner = fx.make_layout((1024, 2), (2, 1)). Derive each histogram bin with fx.crd2idx((tid, pair), bin_owner) for pair in 0 and 1. This mapping replaces first_bin = tid * 2 and documents exact coverage of 2048 bins. Use a one-value mode for the 1024-bin case.
  • Use vector_owner = fx.make_layout((blocks_per_row, 1024, 4), (4096, 4, 1)). Derive the first unordered scan column from fx.crd2idx((part, tid, value), vector_owner). Keep the persistent increment active_parts * 4096 explicit because active_parts is runtime state.
  • For ordered and compact tiles, use a static tile layout (ORDERED_STAGES, 1024, 4) with stride (4096, 4, 1). Add the dynamic run_first_vblk * 4 base after the layout maps (stage, tid, value). Keep run_first_vblk and the persistent loop explicit.
  • Build physical row-major views for logits with shape (grid_y, stride0) and stride (stride0, 1). Build logical output views with shape (grid_y, top_k) and stride (top_k, 1). Wrap them with fx.rocdl.make_buffer_tensor(..., max_size=False, num_records_bytes=...). Use BufferCopy128b plus fx.copy for vec4 row and histogram transfers. Use UniversalCopy32b or the matching buffer copy atom for scalar sequence and output transfers.
  • Model workspace as a row-major (grid_y, row_workspace_slots) view. Create named views for counter groups, pass histograms, and compact (column, key) records. Keep acquire/release loads and exchanges in one low-level row-barrier helper because FlyDSL 0.3.2 does not expose those exact ordered operations. Use UniversalAtomicAdd(fx.Int32, fx.rocdl.SyncScope.Workgroup) for LDS additions and a buffer atomic atom for monotonic global histogram additions where their lowering matches the current instructions.
  • Keep @fx.struct plus one fx.SharedAllocator. Change s_scan to a (2, 16):(16, 1) view for wave totals and exclusive offsets. Split s_meta and s_run into named fields or named views instead of numeric slot constants. Keep the histogram as a 1-D bin view because bucket selection is data-dependent.

Compile each mapping with FlyDSL 0.3.2. Compare frontend IR, post-layout IR, vector load/store ISA, LDS atomics, registers, LDS, scratch, occupancy, numerical results, and matched latency before removing the manual form.

P2 — Architecture selection is process-global instead of tensor-device-specific

Extra note: Using e.g. get_gfx_runtime is common practice in AITER, so parsing of the device name per-kernel should likely not be done as proposed in this comment.

Where: aiter/ops/topk.py lines 571-599, _should_use_flydsl_decode (calls the process-global get_gfx_runtime); aiter/ops/flydsl/topk_per_row_decode.py lines 440-448, _current_arch (one-entry process cache), and lines 626-633, flydsl_top_k_per_row_decode (reads that cache before entering the logits device context).

Trigger: A process has devices of different supported architectures, or the current device architecture differs from logits.device.

Reason: The selected architecture comes from process-global current-device detection. The wrapper then keys the launcher and target-specific options with that architecture. decode_cu_count correctly checks the tensor device, but it receives the already incorrect architecture and falls back to an architecture floor. The JIT launcher can then freeze a target that does not match the tensor device.

Proposed fix: Derive the architecture from torch.cuda.get_device_properties(logits.device).gcnArchName.split(":")[0]. Pass that value to the public gate, workspace sizing, and launcher cache. Cache by device identity and architecture. Enter the tensor device context before any target-sensitive FlyDSL call. Add a device-selection test where the current device differs from the logits device. Add a heterogeneous-architecture test when such CI hardware is available.

P2 — The tests do not follow the AITER operation-test contract

Note: Do verify first before reading this comment if extra op_tests/flydsl_tests/ are allowed in any documentation under AITER, or any AITER issue/RFC.

Where: op_tests/flydsl_tests/test_flydsl_topk_per_row_decode.py lines 1-18, pytest-only suite and module-level GPU skip; lines 116-143, local set comparison and torch.testing.assert_close; lines 166-529 and 637-710, large pytest matrices without the standard benchmark table; op_tests/test_topk_decode_dispatch.py lines 1-326, second pytest-only suite; lines 39-81 and 142-170, assertions coupled directly to the shipped tuning table; lines 40-45, external ticket and deployment-specific narrative.

Trigger: Normal operation-test maintenance, a tuning-table update, or the standard operation benchmark workflow.

Reason: The AITER test guidance requires one canonical op_tests/test_<op>.py suite with @benchmark, run_perftest, checkAllclose, per-candidate latency and throughput fields, an architecture allow-list in main(), an itertools.product sweep, and a final Markdown table. This PR adds two pytest-only suites. The direct kernel suite uses a local comparison instead of checkAllclose. The dispatcher suite pins mutable production thresholds rather than injecting a small synthetic gate. A threshold retune can therefore fail tests without changing the dispatch contract. The test narrative also includes an external identifier that does not describe a technical behavior.

Proposed fix: Create one canonical operation test at op_tests/test_topk_decode.py. Drive the public wrapper and add a direct launcher candidate only where implementation-specific coverage is required. Use an independent fp32 reference, checkAllclose, and the standard timing and summary flow. Keep focused pytest checks only for distinct failure behavior that does not fit the benchmark function. Test gate precedence and fallback with an injected synthetic gate. Remove the duplicate suite, mutable winner assertions, and external identifiers from test and production comments.

P3 — New-file metadata and comments do not meet repository guidance

Where: aiter/ops/flydsl/kernels/topk_per_row_decode.py lines 1-10; aiter/ops/flydsl/kernels/topk_per_row_decode_tiered.py lines 1-45; aiter/ops/flydsl/kernels/topk_per_row_decode_tiered_compact.py lines 1-45; aiter/ops/flydsl/topk_per_row_decode.py lines 1-22; op_tests/flydsl_tests/test_flydsl_topk_per_row_decode.py lines 1-7 (new files have SPDX text but omit the repository copyright line); aiter/ops/topk.py lines 480-483 (production comment refers to an external investigation identifier).

Trigger: Source distribution, generated notices, or reuse of these comments outside the original development context.

Reason: Nearby new AITER files use the current copyright convention. The cleanup guidance also requires comments to explain the technical property without external ticket or review-history references.

Proposed fix: Add the repository's current copyright line to retained new files. Replace external references with the measured width, row-count, architecture, and graph-replay facts that justify each threshold. Remove historical tuning narratives that do not define a maintained invariant.

Verification gaps

  • I did not run GPU correctness on gfx942 or gfx950.
  • I did not compile the kernel from an empty FlyDSL cache.
  • I did not test AOT compile-only output or a packaged run-only cache hit.
  • I did not test graph capture and replay, a caller-provided non-current stream, workspace reuse across streams, or heterogeneous devices.
  • I did not inspect generated IR, ISA, VGPR, SGPR, LDS, scratch, or occupancy for this revision.
  • I did not reproduce the claimed architecture and shape performance windows.
  • Python bytecode compilation passed for the changed Python files.
  • python3 -m pytest op_tests/test_topk_decode_dispatch.py -q -k 'shipped or staircase or routing_cases' passed 8 tests and deselected 53 tests.
  • AOT job discovery completed and produced 224 jobs: 36 per k for the 304-CU rows and 20 per k for the 256-CU rows.
  • git diff --check origin/main...HEAD passed.
  • Ruff failed with 17 findings, all in topk_per_row_decode_tiered_compact.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants