Skip to content

[gfx1201] Enable quantization kernels for gfx12xx - #6

Open
big-yellow-duck wants to merge 1 commit into
mainfrom
rdna4_quant-support
Open

[gfx1201] Enable quantization kernels for gfx12xx#6
big-yellow-duck wants to merge 1 commit into
mainfrom
rdna4_quant-support

Conversation

@big-yellow-duck

Copy link
Copy Markdown

Motivation

FP8 quantization operations fail on AMD gfx1201 (RDNA4) architecture due to three compatibility issues:

  1. FP8 dtype is not registered for gfx1201 in the dtype mapping
  2. v_pk_mul_f32 assembly instruction is not supported on gfx11/gfx12
  3. DPP broadcast operations (0x142, 0x143) used in hip reduce are not supported on gfx11/gfx12

This PR enables FP8 quantization support on gfx1201 by addressing these incompatibilities.

Technical Details

1. FP8 Dtype Registration (aiter/utility/dtypes.py)

Added gfx1201 to the default FP8 dtype mapping to enable torch.float8_e4m3fn support on RDNA4.

2. Scalar Multiplication Fallback (csrc/include/ck_tile/vec_convert.h)

The v_pk_mul_f32 assembly instruction is not supported on gfx11/gfx12. Added amd_scalar_mul_f32() function as a portable fallback:

CK_TILE_DEVICE fp32x2_v amd_scalar_mul_f32(fp32x2_v a, fp32x2_t b){
    fp32x2_v c;
    c[0] = a[0] * b[0];
    c[1] = a[1] * b[1];
    return c;
}

The conversion functions fp32x2_t_to_fp8x2_t and fp32x2_t_to_int8x2_t now conditionally use the scalar path:

#if defined(__gfx11__) || defined(__gfx12__)
    tmp = amd_scalar_mul_f32(x, fp32x2_t{inverted_scale, inverted_scale});
#else
    tmp = amd_assembly_pk_mul_f32(x, fp32x2_t{inverted_scale, inverted_scale});
#endif

3. DPP Broadcast Replacement (csrc/include/hip_reduce.h)

DPP broadcast operations are not supported on gfx11/gfx12. Replaced with rocprim::warp_shuffle() for cross-lane communication in:

  • wave_reduce() - for WarpSize > 16 and WarpSize > 32 reductions
  • multithread_reduce() - for 16-thread and 32-thread reduction paths

Example change:

#if defined(__gfx12__) || defined(__gfx11__)
    // Use shuffle for gfx12 instead of DPP broadcast
    T v_remote = rocprim::warp_shuffle(local, 15, WarpSize);
    local      = reduce_op(v_remote, local);
#else
    // row_bcast:15
    local = reduce_op(rocprim::detail::warp_move_dpp<T, 0x142>(local), local);
#endif

4. Naive load to LDS fallback (csrc/kernels/quant_kernels.cu)

gfx12x Fallback to naive loading from global memory to LDS in smooth_per_token_scaled_quant_kernel.

for(int i = 0; i < async_load_num; i++)
        {
            #if defined(__gfx12__)
                int idx = threadIdx.x + i * block_size;
                if(idx < smooth_scale_map_hash_size)
                {
                    // RDNA4 doesn't support buffer_load_* with LDS modifier
                    // Use standard global load to VGPR then write to LDS
                    smooth_scale_map_hash_shared[idx] = smooth_scale_map_hash[idx];
                }
            #else
                const int lds_ptr_sgpr = __builtin_amdgcn_readfirstlane((reinterpret_cast<uintptr_t>((smooth_scale_map_hash_shared + threadIdx.x / WARP_SIZE * WARP_SIZE + i * block_size))));
                uint32_t offset = threadIdx.x * sizeof(int) + i * block_size * sizeof(int);
                asm volatile( "s_mov_b32 m0 %0\n\t"
                "buffer_load_dword %1, %2, 0 offen offset:0 lds\n\t"
                ::"s"(lds_ptr_sgpr), "v"(offset), "s"(buffer_hash.cached_rsrc): "memory", "m0");
            #endif
        }

Test Plan

Run the quantization test suite with various tensor sizes:

python op_tests/test_quant.py -m 1 2 16 32 64 128 192 256 512 1024 16384

Test Result

All quantization tests pass successfully on gfx1201:

m n q_type q_dtype h_dtype triton dq triton dq err hip dq hip dq err
1 4096 4 torch.float8_e4m3fn torch.bfloat16 1.88473 0.00219727 2.24066 0
2 4096 4 torch.float8_e4m3fn torch.bfloat16 1.91516 0.000244141 2.24869 0
16 4096 4 torch.float8_e4m3fn torch.bfloat16 12.9245 0.00135803 2.34457 0
32 4096 4 torch.float8_e4m3fn torch.bfloat16 15.0222 0.00146484 2.55607 0
64 4096 4 torch.float8_e4m3fn torch.bfloat16 5.16941 0.00187302 2.96935 0
128 4096 4 torch.float8_e4m3fn torch.bfloat16 8.66827 0.00178909 9.95423 0
192 4096 4 torch.float8_e4m3fn torch.bfloat16 11.9403 0.00161235 5.87333 0
256 4096 4 torch.float8_e4m3fn torch.bfloat16 15.25 0.00166798 9.63588 0
512 4096 4 torch.float8_e4m3fn torch.bfloat16 29.3212 0.00158978 13.3969 0
1024 4096 4 torch.float8_e4m3fn torch.bfloat16 56.4606 0.00173402 27.7032 0
16384 4096 4 torch.float8_e4m3fn torch.bfloat16 815.536 0.00166075 336 8.9407e-08
1 8192 4 torch.float8_e4m3fn torch.bfloat16 1.90162 0.00354004 2.22905 0
2 8192 4 torch.float8_e4m3fn torch.bfloat16 1.9646 0.00140381 2.06146 0
16 8192 4 torch.float8_e4m3fn torch.bfloat16 14.8775 0.00149536 2.45573 0
32 8192 4 torch.float8_e4m3fn torch.bfloat16 5.19413 0.00179291 3.01099 0
64 8192 4 torch.float8_e4m3fn torch.bfloat16 8.6691 0.00170708 6.05578 0
128 8192 4 torch.float8_e4m3fn torch.bfloat16 15.2624 0.0015707 9.57887 0
192 8192 4 torch.float8_e4m3fn torch.bfloat16 22.3951 0.00170898 7.50138 0
256 8192 4 torch.float8_e4m3fn torch.bfloat16 29.3114 0.00168657 13.3278 0
512 8192 4 torch.float8_e4m3fn torch.bfloat16 56.4138 0.00164104 27.7895 0
1024 8192 4 torch.float8_e4m3fn torch.bfloat16 110.548 0.00167239 39.1202 1.19209e-07
16384 8192 4 torch.float8_e4m3fn torch.bfloat16 1602.16 0.00165743 670.129 7.45058e-08
The scalar multiplication fallback and warp shuffle replacements provide correct functionality while maintaining compatibility with the RDNA4 architecture.

Submission Checklist

@github-actions

Copy link
Copy Markdown

🏷️ CI Guide

Runs automatically on every PR:

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

Extended tests (opt-in via labels):

Label Tests
ci:sglang SGLang integration tests
ci:atom ATOM benchmark (DeepSeek-R1 + GPT-OSS)
ci:vllm vLLM benchmark
ci:all All of the above

Add labels via the sidebar or gh pr edit 6 --add-label <label>

@big-yellow-duck big-yellow-duck changed the title [gfx1201] Enable quantization kernels for gfx1201 [gfx1201] Enable quantization kernels for gfx12xx Mar 19, 2026
BadrBasowid pushed a commit that referenced this pull request Aug 19, 2026
…igs (ROCm#4397)

* [dev] Pr/a8w4 situv2 (#4)

* add Situv2 activation for a8w4 MoE stage1

* refactor(flydsl): scope situv2 helpers and fix lint

Move situ beta compile-time constants into situ_elem/situ_up_elem; apply
ruff/black fixes on the a8w4 situv2 test module.

* feat(moe): integrate SiTUv2 into fused_moe API, split-K, tuner/prebuild

* test(moe): add SiTUv2 host-ref + a4w4/a8w4 stage1 test (adapted to Situv2 naming)

Complements MHYang's aiter/ops/flydsl/test_flydsl_moe_a8w4.py by adding a
host-only (no-GPU) SiTUv2 reference sweep plus a4w4 stage1 coverage in
addition to a8w4. Adapted to this branch's API: ActivationType.Situv2 enum
and situ_beta / situ_linear_beta parameters on torch_moe_stage1 and
flydsl_moe_stage1.

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

* test(moe): add SiTUv2 default cases + --beta/--linear-beta to test_moe_2stage (Situv2 naming)

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

* test: consolidate a8w4 SiTUv2 vec4 cases into op_tests/flydsl_tests.

Move pytest tile/gate_mode sweep from aiter/ops/flydsl/test_flydsl_moe_a8w4.py
into test_flydsl_moe_situv2.py and remove the misplaced test file.

* style: black-format silu_and_mul_fq.py for CI pre-checks.

---------

Co-authored-by: MHYang <mengyang@amd.com>
Co-authored-by: Clement Lin <Clement.Lin@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* [dev] Pr/fix k dimension (#5)

* add Situv2 activation for a8w4 MoE stage1

* refactor(flydsl): scope situv2 helpers and fix lint

Move situ beta compile-time constants into situ_elem/situ_up_elem; apply
ruff/black fixes on the a8w4 situv2 test module.

* feat(moe): integrate SiTUv2 into fused_moe API, split-K, tuner/prebuild

* test(moe): add SiTUv2 host-ref + a4w4/a8w4 stage1 test (adapted to Situv2 naming)

Complements MHYang's aiter/ops/flydsl/test_flydsl_moe_a8w4.py by adding a
host-only (no-GPU) SiTUv2 reference sweep plus a4w4 stage1 coverage in
addition to a8w4. Adapted to this branch's API: ActivationType.Situv2 enum
and situ_beta / situ_linear_beta parameters on torch_moe_stage1 and
flydsl_moe_stage1.

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

* test(moe): add SiTUv2 default cases + --beta/--linear-beta to test_moe_2stage (Situv2 naming)

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

* test: consolidate a8w4 SiTUv2 vec4 cases into op_tests/flydsl_tests.

Move pytest tile/gate_mode sweep from aiter/ops/flydsl/test_flydsl_moe_a8w4.py
into test_flydsl_moe_situv2.py and remove the misplaced test file.

* style: black-format silu_and_mul_fq.py for CI pre-checks.

* Fix GUI shuffle_scale k_pad for non-256-aligned MoE K and auto stage2 tile_k.

Pad w2/w1 GUI e8m0 scales to k_groups multiple of 8 (DSV4 inter=640), unify
stage2 tile_k selection in flydsl_moe_stage2 and fused_moe, and add staged
a8w4 regression tests across inter/model K sweeps.

* test: integrate a8w4 K-dimension regressions into existing test suites.

Move shuffle pad coverage into test_quant_mxfp4 and FlyDSL GUI stage2/e2e
into test_flydsl_moe_a8w4; drop standalone op_tests files for repo convention.

* test: move flydsl a8w4 GUI regressions to op_tests/flydsl_tests.

Relocate test_flydsl_moe_a8w4 under op_tests/flydsl_tests and align with
pytest conventions used by other FlyDSL op tests.

---------

Co-authored-by: MHYang <mengyang@amd.com>
Co-authored-by: Clement Lin <Clement.Lin@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* [dev] Feat/flydsl moe a16wfp4 (#6)

* add Situv2 activation for a8w4 MoE stage1

* refactor(flydsl): scope situv2 helpers and fix lint

Move situ beta compile-time constants into situ_elem/situ_up_elem; apply
ruff/black fixes on the a8w4 situv2 test module.

* feat(moe): integrate SiTUv2 into fused_moe API, split-K, tuner/prebuild

* test(moe): add SiTUv2 host-ref + a4w4/a8w4 stage1 test (adapted to Situv2 naming)

Complements MHYang's aiter/ops/flydsl/test_flydsl_moe_a8w4.py by adding a
host-only (no-GPU) SiTUv2 reference sweep plus a4w4 stage1 coverage in
addition to a8w4. Adapted to this branch's API: ActivationType.Situv2 enum
and situ_beta / situ_linear_beta parameters on torch_moe_stage1 and
flydsl_moe_stage1.

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

* test(moe): add SiTUv2 default cases + --beta/--linear-beta to test_moe_2stage (Situv2 naming)

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

* test: consolidate a8w4 SiTUv2 vec4 cases into op_tests/flydsl_tests.

Move pytest tile/gate_mode sweep from aiter/ops/flydsl/test_flydsl_moe_a8w4.py
into test_flydsl_moe_situv2.py and remove the misplaced test file.

* style: black-format silu_and_mul_fq.py for CI pre-checks.

* Fix GUI shuffle_scale k_pad for non-256-aligned MoE K and auto stage2 tile_k.

Pad w2/w1 GUI e8m0 scales to k_groups multiple of 8 (DSV4 inter=640), unify
stage2 tile_k selection in flydsl_moe_stage2 and fused_moe, and add staged
a8w4 regression tests across inter/model K sweeps.

* test: integrate a8w4 K-dimension regressions into existing test suites.

Move shuffle pad coverage into test_quant_mxfp4 and FlyDSL GUI stage2/e2e
into test_flydsl_moe_a8w4; drop standalone op_tests files for repo convention.

* test: move flydsl a8w4 GUI regressions to op_tests/flydsl_tests.

Relocate test_flydsl_moe_a8w4 under op_tests/flydsl_tests and align with
pytest conventions used by other FlyDSL op tests.

* Add FlyDSL MoE a16wfp4 (bf16 x mxfp4) kernels on gfx950.

Port a16w4 stage1/stage2 into mixed_moe_gemm_2stage as dedicated _a16w4
builders while keeping HEAD fp8/fp4 compile paths unchanged. Wire dispatch
in moe_kernels and add op_tests/flydsl_tests regression for stage1, stage2,
and e2e against torch_moe references.

* Fix ruff/black lint issues in a16wfp4 PR files.

Remove unused _fly imports, move lds_space/types imports to module top,
add noqa for test sys.path bootstrap, and apply black formatting.

* Remove dead duplicate waves_per_eu None checks in a16w4 stage2.

The parameter defaults to int and callers always pass an int after dispatch.

* Fix a16w4 stage2: skip second compute when total_tiles==1.

The CK-style ping-pong pipeline assumed total_tiles>=2.  When
inter_dim==tile_k (e.g. inter_dim=256, tile_k=256) total_tiles=1,
the HEAD pre-fetched an out-of-bounds A tile into lds_ping, and the
TAIL then computed with that garbage data, producing cos~0.71 vs
reference.

Fix: const_expr(total_tiles==1) guard in the TAIL block--skip the
second compute_tile and the ping-buffer load entirely.

Also tighten test _check_result: add cosine+rel_L2 as primary gate
(cos>0.999) so future stage2 bugs cannot be masked by atol=1.0 on
small-magnitude outputs.

* Add pipeline guards for a16w4 stage1 total_tiles==1 and odd-tile validation.

The stage1 ping-pong pipeline has the same total_tiles>=2 assumption
as stage2.  Add:
- total_tiles==1 guard in stage1 TAIL (same pattern as stage2 fix)
- Validation: reject odd total_tiles and K < tile_k at compile time
  for both stage1 and stage2 a16w4 paths

These configs cannot occur with real LLM shapes (model_dim and
inter_dim are always multiples of 256), but the guards prevent
silent corruption on contrived inputs.

* Add SiTUv2 activation to a16w4 (bf16 x mxfp4) stage1.

Port the SiTUv2 activation from pr/a8w4-situv2 into the a16w4 stage1
kernel (both direct and cshuffle epilogues):
  situ_g   = beta * tanh(gate/beta) * sigmoid(gate)
  up_scaled = linear_beta * tanh(up/linear_beta)
with gate/up clamped to <=7 / [-7,7] before activation, matching the
a8w4 situv2 numerics exactly.

Thread situ_beta/situ_linear_beta through compile_flydsl_moe_stage1 and
flydsl_moe_stage1; the per-beta cache tag keeps distinct binaries.

Verified vs an inline torch SiTUv2 reference (mxfp4 dequant GEMM +
clamp + situv2): cos>=0.99996 across beta in {(1,1),(0.5,2),(1.5,0.8)},
shapes (512/256, 3072/256), tile_n {128,256}. silu/swiglu unchanged.

* Strip dead generic pipeline from a16w4 stage1 kernel (~2260 lines).

compile_mixed_moe_gemm1_a16w4 was created by copying the generic fp8/fp4
builder and gating with is_a16w4_stage1.  The live a16w4 path is fully
contained in `if const_expr(is_a16w4_stage1): ... return`; everything
after that return was an unreachable copy of the generic f8f6f4 kernel
body (referenced generic-only vars like _lds_tid_offset_pong / _pipe_*
that are never defined on the a16w4 path -- proven dead since a16w4
tests pass without them).

Remove the dead generic kernel body, the generic-only setup blocks
(postlude pipeline schedule, not-is_a16w4 guards), and the now-unused
setup vars.  Purely dead-code deletion: a16w4 stage1/stage2/e2e/situv2
+ a4w4 all still pass with identical cosine.

* Strip dead generic pipeline from a16w4 stage2 kernel (~1140 lines).

Same shape as the stage1 cleanup: compile_mixed_moe_gemm2_a16w4's live
a16w4 path is fully contained in `if const_expr(is_a16w4): ... return`
inside _moe_gemm2_then_body; everything after that return was an
unreachable copy of the generic f8f6f4 stage2 body (f8f6f4 MFMA,
generic-only scale layouts). Proven dead: a16w4 stage2/e2e/situv2 +
a4w4 all pass unchanged after removal.

Remove the dead body plus the now-unused generic setup vars
(pack_N/pack_K, cbsz/blgp, generic scale layouts, sx_rsrc sentinel,
etc.). Purely dead-code deletion.

* feat(moe): route a16w4 (bf16 x mxfp4) SiTUv2 through fused_moe

Wire the mixed_moe a16w4 kernel into the fused_moe 2-stage path for SiTUv2:
get_2stage_cfgs now matches bf16/fp16 x fp4 when activation is SiTUv2 (which
uniquely identifies the a16w4 kernel, so GPT-OSS / legacy bf16-Swiglu keep
their CK-Tile routing), maps _a_type to bf16/fp16, and infers q_dtype_a=bf16
for SiTUv2+separated so the activation stays bf16 (no fp4 quant). The stage1/
stage2 activation-quant gates also accept SiTUv2.

Tests: a16w4 SiTUv2 via fused_moe (cos=1.0 vs torch ref) and an a8w4 SiTUv2
inter=640 vec4 case exercising the fix-k non-256 K-tiling.

* feat(moe): support non-256 inter_dim for a16w4 (a8w4 parity)

Bring the FlyDSL a16w4 (bf16 x mxfp4) MoE kernels to a8w4 parity so
inter_dim need not be a multiple of 256 (e.g. DSV4 inter=640, and
arbitrary values like 384 via inter_dim_pad), across stage1/stage2/E2E.

Kernel (mixed_moe_gemm_2stage.py):
- stage2: allow inter_dim % tile_k == 0 (drop even-only rule), add
  odd_k_tiles 1-tile tail path, 256-pad scale reads (ROCm#3476).
- stage1: same odd_k_tiles K-loop + 256-padded w1 scale layout; fix gx
  launch grid (tile2_pad + full-N when inter_dim_pad > 0).

Caller (moe_kernels.py):
- pick/resolve_flydsl_stage1_tile_n: use tile_n=128 when inter%256!=0
  (fixes stage1 error on cols 0..inter%256 under tile_n=256).
- zero-init stage1 out when inter_dim_pad > 0.

Tests (test_flydsl_moe_a16wfp4.py):
- non-256 regressions (256/384/640) for stage1/stage2/E2E + pick_* tests.
- _generate_a16wfp4_data gains activation/situ_beta/situ_linear_beta.
- --perf sweep mode (correctness + latency/TFLOPs).

* test(moe): fold situv2 tests into dtype files, drop standalone situv2 file

Consolidate test_flydsl_moe_situv2.py into the per-dtype test files (matching
how test_flydsl_moe_a16wfp4.py already embeds its situv2 tests):

- test_flydsl_moe_a8w4.py: add the a8w4 SiTUv2 vec4 stage1 sweep + the
  host-only situv2 reference test; reuse the file's existing _check_close
  helper (fp32 cast for the bf16-ref vs f16-out case). Add shuffle_weight /
  e8m0_shuffle imports.
- Delete test_flydsl_moe_situv2.py. Its broken-under-pytest a4w4/a8w4
  main()-runner is dropped; a8w4 situv2 is now real parametrized coverage.

a16wfp4 situv2 tests already lived in their dtype file and are unchanged.

* fix conflict

* add timing args

---------

Co-authored-by: MHYang <mengyang@amd.com>
Co-authored-by: Clement Lin <Clement.Lin@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mh <mh@local>

* fix(moe): a8w4 non-256 inter_dim stage1 tile_n + situv2 E2E regression tests

Squashed fix + tests (PR#3 commits 50729303, 2f33bd40, 1f0c6485, 46e3e056).

flydsl_moe_stage1 only downgraded tile_n (256->128) for non-256-aligned
inter_dim on a16w4 (bf16 x mxfp4); a8w4 (fp8 x mxfp4) kept tile_n=256, which
in separated gate_mode over-runs the gate/up (N) axis for non-256 inter_dim:
~30% wrong E2E output or GPU memfault at inter=384/640. Extend the resolve to
a8w4 (b_dtype fp4/mxfp4, a_dtype in {bf16,fp8}); a4w4 untouched. Callers keep
passing tile_n=256; the kernel resolves internally.

Tests (numeric vs torch reference, no-pad path, 128-multiple inter_dim):
- test_flydsl_e2e_a8w4_situv2 (separated, the production/customer path)
- test_flydsl_e2e_a16wfp4_situv2 (separated + interleave)
- test_flydsl_e2e_a8w4_gui extended to inter=256/384/640 (interleave, swiglu)

Verified on gfx950 (full rebuild): a8w4 20 / a16wfp4 27 / a4w4 4 passed;
ruff + black clean.

* refactor(moe): observable non-256 tile downgrade + a8w4 interleave situv2 E2E

Squashed (PR#3 c827ddbb + d381aae7).

- resolve_flydsl_stage1_tile_n / resolve_flydsl_stage2_tile_k: keep the silent
  auto-downgrade of a non-dividing tile (256->128) for non-256 inter_dim, but
  make it observable -- full docstrings noting tile=256 is NOT tunable for such
  shapes, plus a one-time (deduped) logger.warning on override.
- test_flydsl_e2e_a8w4_situv2: enable interleave (a16w4-style shuffle_weight_a16w4
  recipe) alongside separated, over 128-multiple inter_dim. Verified all
  activations (silu/swiglu/situv2) x both gate_modes x non-256 give E2E 0.0000.

* Add interleave_gate_up_rows + moe_shuffle_weight to ops/shuffle.py

Needed by atom-k3 (rocm/atom HEAD imports them from aiter.ops.shuffle);
ported verbatim from aiter main. Keeps the single aiter-k3 branch usable
by vllm-k3, sglang-k3 and atom-k3.

* [kimi-K3] extend conv2d support to gfx1250

* [Kimi-K3] pin _MIN_FLYDSL_VERSION to 0.2.2 (K3 MoE kernels need loc= API removed in 0.2.4)

* [Kimi-K3] fix MoE A16W4 for flydsl 0.2.4 and bump _MIN_FLYDSL_VERSION

flydsl 0.2.4 removed the loc= parameter from rocdl._split_mfma_operands.
The A16W4 BF16 K32 MFMA helper in mixed_moe_gemm_2stage.py called it as
_split_mfma(operands, loc=loc), which raises TypeError under 0.2.4.

Drop loc= from both (identical) helper sites; _split_mfma_operands only
unwraps operand Values and reads int flags, so it never needed loc. The
loc/ip are still threaded to the real op builder (_mfma_k32_raw), so
location tracking is preserved.

Bump _MIN_FLYDSL_VERSION to 0.2.4 and drop the now-stale 0.2.2 pin note.

Verified on gfx950 with flydsl 0.2.4 via test_flydsl_moe_a16wfp4.py
(stage1, stage2, e2e all pass, cos>=0.99999).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Kimi-K3] add fmoe tuned config

* [Kimi-K3] add bf16 dense GEMM tuned config

* [Kimi-K3] extend a16w4 fmoe tuned config inter_dim=384

* fix(kimi-k3): support A4W4 SiTUv2 on gfx950 and gfx1250

Keep Kimi-K3 SiTUv2 on the A4W4 FlyDSL path, add gfx1250 grouped-MoE activation and stability support, preserve ATOM's legacy A4W4 call, and cover the gfx1250 path with focused tests.

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

* [Kimi-K3] fix flydsl aot build failed

* Gluon reduce guard

* add a8w4 fmoe tune config

* Enable a8w4 SiTUv2 MoE via AITER_SITUV2_A8W4

Signed-off-by: Hongxia Yang <hongxia.yang@amd.com>

* Support row-strided inputs in grouped TopK

* chore(gfx1250): drop CK enable patch — not needed for Kimi-K3

patches/ck_gfx1250_enable.patch only served to build native CK kernels
(quant/cache/rmsnorm/moe/sample/custom_all_reduce) on gfx1250. Verified that
with ENABLE_CK=0 (no patch, triton/hip fallbacks) Kimi-K3 serves correctly on
gfx1250 x4 (tp4): full gsm8k 1319 = 0.956 flexible/strict, vs 0.9613 with the
patched native-CK path — within stderr (+/-0.0056). Dropping the patch keeps
cleanup/k3-minimal minimal for merge into k3-for-amd; gfx1250 runs ENABLE_CK=0
(cost: ~13% decode vs native CK, no accuracy loss).

* chore(gfx1250): drop unified_attention Gluon guard — K3 doesn't use it

Kimi-K3 runs full-attn via MLAAttention (MLA-latent) and KDA via fla, so it never
dispatches to unified_attention (verified 0 calls across a full gsm8k run). The
non-power-of-two head_size / Gluon-reduce guards were added for K3's old
MHA-via-unified_attention path, which ATOM has now removed. Reverting
unified_attention.py to k3-for-amd keeps cleanup/k3-minimal focused on what K3
actually needs. Full gsm8k 1319 with this + the dropped CK patch = 0.955.

* Support row-strided inputs in grouped TopK opt-sort

* rm test_flydsl_moe_situv2.py: SiTUv2 coverage lives in test_flydsl_moe_a8w4.py

Per PR #6 review: the SiTUv2 stage1 variants (a4w4/a8w4) are already covered
by test_flydsl_moe_a8w4.py (cases were migrated there), so drop the
standalone file.

* test: fold row-strided biased_grouped_topk case into test_moeTopkSoftmax.py

Per PR review: stride is a simple feature — extend the existing
test_biased_grouped_topk with a dense-vs-strided equality check
(gating_output copied into a padded, non-contiguous row-strided view)
instead of a dedicated test file. Removes
op_tests/test_biased_grouped_topk_strided.py.

Verified on gfx950: topk_ids/topk_weights [dense vs strided] pass.

* test: simplify strided coverage — make gating_output itself row-strided

Instead of a separate dense-vs-strided check block, create gating_output
as a non-contiguous row-strided view (slice of a padded backing buffer) so
the existing biased_grouped_topk_hip checks exercise the strided path
directly. moe_fused_gate still gets a dense copy (it does not accept
strided input). The randn stream is kept identical to the dense layout so
later tests in the file see the same random inputs.

Verified on gfx950: full file run, zero failures.

* test: add K3 row-strided biased_grouped_topk case in main

Per review discussion: keep test_biased_grouped_topk's dense default
unchanged; the function now accepts an optional gating_output, and main
gains one case passing the K3 fused MoE-front router layout — logits as a
non-contiguous row-strided slice of the fused [gate_up|experts|routed]
buffer (896 experts, topk=16). The dedicated strided test file stays
removed.

Verified on gfx950: full file run, zero failures (err_aiter = 0).

* style: apply black formatting to gfx1250 flydsl files

Match the repo's pre-checks CI (black via psf/black@stable, ruff check):
reformat grouped_moe_gfx1250.py, gemm_mxscale_gfx1250.py and
moe_grouped_gemm_mxscale_gfx1250.py. Ruff reports no issues on the
branch. No functional change.

* add gfx1250 bf16 tuned config

* [Kimi-K3] fix ruff findings reported by the CI style check

The pre-checks ruff job installs ruff unpinned (`pip3 install ruff`) and the
repo carries no ruff config, so the enforced rule set follows ruff's defaults.
ruff 0.16.0 widened those defaults well beyond E4/E7/E9/F, which is what this
branch tripped over. Fix the 41 findings reviewdog flagged inside the PR diff:

- silu_and_mul_fq.py (B023): bind the per-iteration SSA values as default args
  in _fmin / _sigmoid_s / _situv2_elem. The closures are deliberately redefined
  per unrolled iter_idx because the arith.constant ops must be emitted at the
  current insertion point, so hoisting them out of the loop is not an option.
- moe_kernels.py: narrow the logger-import guard to ImportError (BLE001) and
  switch the flagged annotations to PEP 604 / builtin generics (UP006, UP045).
- flydsl moe tests + test_moe_2stage.py: sort the import blocks (I001), drop
  the now-unused `# noqa: E402` markers (RUF100), rewrite dict() calls as
  literals (C408), and mark the sweep-runner blanket catches with an explicit
  `# noqa: BLE001` plus the reason they are intentional.

Only findings inside the PR diff are addressed; pre-existing findings elsewhere
in the same files are left alone. black --check stays clean.

* [Kimi-K3] fix the second batch of ruff findings from the CI style check

reviewdog only posts a bounded number of results per run, so resolving the
first 41 findings surfaced a second batch of 27 that were in the PR diff all
along. Same root cause (unpinned ruff + no repo ruff config, 0.16.0 defaults).

- fused_moe.py / grouped_moe_gfx1250.py / moe_grouped_gemm_mxscale_gfx1250.py
  (UP045): PEP 604 annotations. Converted whole parameter runs rather than the
  individual flagged lines, because fixing one line pulls its neighbours into
  the diff hunk and the findings cascade.
- mfma_preshuffle_pipeline.py: drop the quotes from the ir.Value/ir.Type
  annotations (UP037) and sort __all__ (RUF022).
- mixed_moe_gemm_2stage.py: sort the import blocks (I001), functools.cache for
  the maxsize=None caches (UP033), inline the directly-called lambda in
  out_mlir (PLC3002), merge the nested bf16-atomics guard (SIM102), and
  underscore-prefix the unpacked values that are never read (RUF059) -- the
  decode calls are kept since they emit IR.

All rewrites are behaviour-preserving; black --check stays clean and the import
name sets are unchanged. Findings outside the PR diff are still left alone.

---------

Signed-off-by: Hongxia Yang <hongxia.yang@amd.com>
Co-authored-by: billishyahao <yahao.he@gmail.com>
Co-authored-by: MHYang <mengyang@amd.com>
Co-authored-by: Clement Lin <Clement.Lin@amd.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: billishyahao <bill.he@amd.com>
Co-authored-by: mh <mh@local>
Co-authored-by: Dewei Wang <Dewei.Wang@amd.com>
Co-authored-by: Felix Li <felix.li@amd.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Felix Li <felixamd@163.com>
Co-authored-by: XiaobingSuper <xiaobingzhangupc@gmail.com>
Co-authored-by: root <root@smci355-ccs-aus-m12-33.cs-aus.dcgpu>
Co-authored-by: Hongxia Yang <hongxia.yang@amd.com>
Co-authored-by: RolaoDenthu <xinyisong0111@gmail.com>
Co-authored-by: zejunchen-zejun <zejun.chen@amd.com>
Co-authored-by: Lingpeng Jin <103567126+valarLip@users.noreply.github.com>
BadrBasowid pushed a commit that referenced this pull request Aug 19, 2026
* [FLYDSL] add MLA decode reduce kernel for gfx942

Add FlyDSL MLA reduce kernel and production opt-in fallback, with
multi-token decode support (decode_qlen > 1), num_kv_splits dispatch
compat, and fp8 partial/output dtype options. Include correctness tests
against the HIP kernel and standalone bench/profile harness scripts.

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

* Update op_tests/prof_mla_reduce.py

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* [FLYDSL] drop redundant Claude-generated comments from MLA reduce

Co-Authored-By: Claude <noreply@anthropic.com>

* style: black format MLA reduce FlyDSL files

Fix Check Code Style with Black CI failure on PR ROCm#3901.

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

* [FLYDSL] replace tier string literals with Tier enum in mla_reduce.py

Replace bare string tier values ("simple", "m64", "m256", "mlds") with
a proper Tier(enum.Enum) class. select_tier() now returns Tier; the
compile_mla_reduce() tier param is typed Tier = Tier.SIMPLE; all
string comparisons use enum members; the LDS global_sym_name f-string
uses tier.value to preserve the original naming.

Co-Authored-By: Claude <noreply@anthropic.com>

* style: black 26 tuple unpack in bench_mla_reduce_standalone

Match psf/black@stable (Black 26) used by CI Checks workflow.

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

* [FLYDSL] use flydsl.utils.env OptBool for MLA reduce opt-in gate

Replace the raw os.environ.get("AITER_MLA_REDUCE_FLYDSL") read with
FlyDSL's typed env helper (OptBool on an EnvManager subclass). The
helper import is kept inside the existing try/except alongside the
FlyDSL availability check, so the gate still falls back silently to
HIP when FlyDSL is not installed. Drops the now-unused `import os`.

Co-Authored-By: Claude <noreply@anthropic.com>

* [FLYDSL] consolidate mla_reduce expr imports and relocate HIP bench

Use fx.* qualified flydsl.expr access per review feedback and move the standalone MLA reduce benchmark under op_benchmarks/hip/.

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

* chore: drop dev-only prof_mla_reduce.py harness

Remove the standalone rocprofv3 driver; it was not used in CI or production
testing and is superseded by the bench/correctness harnesses.

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

* [FLYDSL] black-format MLA reduce and trim review docstrings

Remove unverified HBM-bound and dev-doc references from MLA reduce docstrings and apply Black wrapping in mla_reduce.py.

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

* Refactor MLA reduce test harness into pytest and FlyDSL bench script.

Split shared helpers into flydsl_mla_reduce_common, convert correctness
coverage to pytest (HIP + torch ref for GLM Dv=256), and add a dedicated
FlyDSL bandwidth benchmark alongside the existing HIP bench.

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

* Add MLA reduce serving guards and discriminating differential tests.

Harden the FlyDSL kernel with gather/store bounds checks and a store q-range
guard, plus in-process guards-on/off tests that prove the guards matter via
mapped-allocation slack fixtures rather than only asserting passes-with-fix.

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

* [FLYDSL] opt1: persistent grid-stride launch for MLA reduce

Wrap the per-work-item body in process_work_item() and add a kn_mla_reduce_v1_ps
-style 1-D persistent grid (num_cu*OCC*2 blocks) that grid-strides over the flat
work index with a CSR-sentinel early-out. Host (mla_reduce_kernels.py) and the
bench/test harness auto-select persistent via should_use_persistent_launch when
H*NTG*num_reduce_tile exceeds the HIP threshold. Guard semantics (bounds-checked
gather, q-range clamp, disable_guards) preserved.

Bench (MI300X, H=16, Dv=512): sparse 16384-tile/8-active 127.8us->12.3us (~10x);
uniform tiles=8/splits=32 7.3->6.8us; tiles=256/splits=8 flat (below threshold).
pytest: 40 passed + 8 slow/serving (sparse grid) passed.

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

* [FLYDSL] opt2: vectorize MLA reduce epilogue store

Truncate each accumulator element to out_t before packing into the out_t
vector so the epilogue lowers to buffer_store_dwordx2/4 instead of a
scalarized per-element store. Drops the unused f32 acc_vt staging vector.

pytest: 40 passed (matrix, not-slow). Bench within noise of opt1 at the
prod replay shapes (store path already saturated post-opt1).

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

* [FLYDSL] opt3: replace _as_list with tuple loop-carried state

Use a tuple init and range_constexpr (compile-time) indexing in the
massive-path accumulator loop, removing _as_list and its runtime range(n)
index workaround that could lower to i64 index arithmetic. VEC==1 scalar
unwrap after the yield is handled explicitly.

pytest: 40 passed (matrix, not-slow). Bench flat at prod shapes (bandwidth
-bound kernel; this is a codegen cleanup).

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

* [FLYDSL] opt4: wire MLA reduce waves_per_eu (default 4)

Stamp rocdl.waves_per_eu on the emitted gpu.func at compile time and expose
AITER_MLA_REDUCE_WAVES_PER_EU for sweeps. Default lowered 8->4 (opt4 sweep:
on H=16 Dv=512 tiles=8 splits=32, wpe=4 best at 6.6us; H=128 graph time flat
across 2/4/6/8). Host wrapper and bench/test harness pass the env-resolved
value.

pytest: 40 passed (matrix, not-slow).

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

* [FLYDSL] opt5: convert MLA reduce seq loop to FlyDSL range

Replace the raw scf.ForOp seq loop with FlyDSL range(seq0, ub_seq, ntg,
init=None) so the AST rewriter emits scf_range without iter_args, letting
hot_loop_scheduler interleave the inner split-loop VMEM loads with compute.
Body unchanged; seq -> seq_i32 induction value.

pytest: 40 passed (matrix, not-slow). Perf-neutral on the prod shapes by
CUDA-graph replay (the bandwidth-bound reduce is already at 24-72% BW); kept
for codegen hygiene / scheduler enablement.

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

* [FLYDSL] fix massive-tier os prefetch with deferred bounds guard

Defer the pmap bounds select to point-of-use in emit_massive_body so the
prefetched os[s+1] load stays in flight (vmcnt(1)) instead of draining
every iteration. Add load_split_o_raw and carry a float OOB mask folded
into the LDS scale at the FMA.

Wire Tier.ALL as the production compile path (device-side runtime tier
selection per tile, mirrors HIP) and update tests/benchmarks accordingly.

* [FLYDSL] stage reduce_partial_map to LDS once per work item

Cooperative-copy pmap[t0:t1] into lds_pmap before the split loop so
gather_row reads LDS instead of repeated global pmap loads (mirrors
reduce.cu:431-438). Adds pmap to the shared allocator for all tiers.

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

* mla_reduce: depth-2 double-rate pipeline in emit_massive_body

Process 2 splits/iter with two output buffer_loads in flight (mirror HIP
reduce_output_massive oaccu_0/oaccu_1). Carrying two distinct loaded vectors
(os0/os1) plus the next pair's prefetch as separate SSA yields lets the
compiler allocate separate VGPRs, so the accumulate loop reaches
s_waitcnt vmcnt(1) overlap instead of the depth-1 vmcnt(0) drain (register
aliasing). Split index is clamped for OOB prefetch (lds_pmap[0]/lds_scale[0]
always written) with a deferred float mask -> no NaN*0 pollution.

Graph mode: b8_s32 9.0->7.0us (ratio 1.50->1.17x), b1_s128 25.8->16.9us
(ratio 1.86->1.22x). SIMPLE unchanged. vgpr 42->56.

* mla_reduce: generalize accumulate to GRP=8 double-rate pipeline

Generalize the depth-2 double-rate loop to process GRP splits/iter with GRP
output buffer_loads in flight (loop-carried grouped state). GRP=8 is the
gfx942 sweet spot: vgpr ~125 keeps 2 waves/SIMD while the accumulate loop
reaches s_waitcnt vmcnt(7) overlap (was vmcnt(1) at depth-2). GRP=16 pushes
vgpr ~199 (1 wave/SIMD) for no b8_s32 gain and only marginal b1_s128 gain.

Graph mode: b8_s32 7.0->6.5 (ratio 1.17->1.08x), b1_s128 16.9->15.3 (ratio
1.22->1.10x) vs depth-2. SIMPLE unchanged. buffer_load_dwordx4 14->50,
vmcnt_max 3->7.

* mla_reduce: vectorize group LDS reads (ds_read_b128) in emit_massive_body

Collapse the per-split scalar lds_pmap/lds_scale gathers into one wide
STensor vector load per GRP group. The group's pmap indices and lse scales
live at contiguous LDS slots (base = i*GRP, 16B-aligned), so read them with
ds_read_b128 instead of GRP scalar ds_read_b32. ATT showed LDS/SMEM-wait
(lgkmcnt) as the #1 stall category once the GRP=8 pipeline hid VMEM; this
cuts ds_read_b32 66->10 (ds_read_b128 10->24), vgpr 125->123.

Refactor gather_row -> row_from_pmap(pmap_value, local_seq) so scalar and
vectorized paths share the identical bounds clamp. OOB tail lanes of the
vector read hit stale slots: substitute slot-0's pmap value (always staged;
massive body only runs when n_splits>1) so the row computation never sees a
stale value even with guards disabled, and select-force the scale to 0 for
invalid splits (no stale NaN reaches the FMA, and selecting on the LDS scale
does not touch the VMEM os load so the deferred-guard vmcnt overlap holds).

Graph-mode vs HIP (gfx942, GLM-5.2 serving): b1_s128 15.3->13.1us
(1.09x -> 0.95x, BEATS HIP); b8_s32 6.5->6.0us (1.08x -> 1.00x, parity).
SIMPLE tier unchanged. Correctness 44/44 (matrix + differential + graph).

* mla_reduce: tier-dependent GRP=16 for M256/MLDS (-8% b1_s128 graph, 0.95x->0.87x)

The long-loop M256/MLDS accumulate path (nlse>=4) uses a GRP=16 double-rate
software pipeline (16 output buffer_loads in flight, deeper vmcnt overlap over
the many-iteration loop): b1_s128 @128 splits drops 13.1->12.0us graph
(0.87x HIP), reproduced across two runs.

The M64 path (nlse=1) keeps GRP=8: a blanket GRP=16 regressed the low-split
tail (b8_s5/s6 @5-6 splits) +11% because each group wastes 10 masked lanes
instead of 2, with no compensating gain on b8_s32 (within run noise). Scoping
GRP to the long-loop tiers captures the b1_s128 win with zero tail regression.
44/44 correctness matrix + differential + cudagraph replay pass.

* mla_reduce: hoist group-0 os prefetch ahead of the LSE scale barrier (lever #6)

emit_massive_body's group-0 output loads depend only on lds_pmap (staged +
barriered at the top of the work item), not on lds_scale (written by the warp0
LSE reduce). Splitting load_group into a pmap/os phase (load_os_group) and a
scale phase (load_scales) lets the GRP os buffer_loads issue *before* the scale
barrier, overlapping ~GRP VMEM loads with the warp0 LSE reduce + barrier wait.

ISA confirms it: the group-0 buffer_load_dwordx4 batch is now emitted before
s_barrier, with the lds_scale ds_read_b128 after it; vmcnt_max=14 overlap and
the uniform SGPR gather descriptor (no waterfall) are preserved. Graph mode:
b8_s32 6.1->6.0, b8_s26 6.1->6.0, b1_s128 12.0->11.9 (reproduced across two
runs, zero regressions). 44/44 correctness matrix + differential + cudagraph
replay pass.

* mla_reduce: scalar indptr sentinel via raw pointer deref (-0.2us all active shapes graph, b8_s32 1.00x->0.98x)

Load the CSR traversal sentinels (`last`, per-work-item `tile_start`) with a raw
uniform `llvm.load` + `rocdl.readfirstlane` instead of the GTensor `buffer_load`
path. A `buffer_load` is inherently a vector memory op (voffset addressing) and
never lowers to `s_load_dword`, so simply wrapping the GTensor load in
readfirstlane (tried, inert) leaves the vector load + `s_waitcnt lgkmcnt(0)`
traversal-floor stall in place. Dereferencing the uniform address raw makes the
load scalarizable, mirroring HIP `__builtin_amdgcn_readfirstlane(p_reduce_indptr[tile])`
(reduce.cu:688).

Graph-mode (clean back-to-back A/B): every M64/SIMPLE shape drops ~0.2us
(b8_s32 6.1->5.9 = beats HIP 6.0; b8_s6 4.5->4.25; b8_s3 4.4->4.2) with b1_s128
held at 11.9 (0.86x). 44/44 correctness (matrix + differential + graph replay).
Kept t0/t1 inside process_work_item on the GTensor path: scalarizing those
regressed b1_s128 +1.2us (they feed the M256 accumulate-loop bounds).

* mla_reduce: invariant sentinel load -> scalar s_load_dword (-0.1..0.2us all shapes graph)

Mark the traversal indptr sentinel llvm.load invariant. reduce_indptr is
read-only in the kernel and the tile index is block-uniform, so the AMDGPU
backend now scalarizes the uniform-address load into s_load_dword (SMEM)
instead of a per-lane global_load_dword + s_waitcnt vmcnt(0). This matches
HIP kn_mla_reduce_v1_ps's scalar sentinel and removes the #1 traversal stall
(re-profile: line 316 was 42.5K vmcnt, 35% of total).

ISA: global_load 1->0, s_load_dword 2->3, vmcnt(0) 15->14. Graph A/B (same
session, fresh JIT): b8_s32 5.95->5.9 (0.98x), b8_s13 4.8->4.7, b8_s6/s5
4.25->4.1, b8_s3 4.3->4.1, b8_s2 4.2->4.0; b1_s128 held 11.9 (0.86x).
44/44 correctness (matrix + differential + graph replay).

* mla_reduce: persistent grid = num_cu (T2, SIMPLE 1.08x->0.97x, beats HIP; dense unchanged)

The persistent launch used grid = num_cu*OCC*2 (=4864), mirroring HIP
`num_cu*kOccupancy*2`. But the FlyDSL Tier.ALL kernel runs at occupancy 1
wave/SIMD (193 VGPR from the shared massive accumulate path), so that 16x grid
is ~8x oversubscribed on the sparse serving profile: thousands of blocks each do
a single sentinel `s_load_dword` then terminate, and at occupancy 1 that latency
cannot be hidden. Dropping the grid to num_cu (mult=1) trims the wasted blocks.
The grid-stride loop still covers any input (correctness unchanged); genuine
dense MLDS work is bandwidth-bound and unaffected.

Graph-mode (two runs each, clean A/B vs the mult=16 baseline this session):
- b8_s3 4.1->3.7 (1.08x->0.97x) and b8_s2 4.1->3.4 (1.11x->0.92x) -- SIMPLE now
  BEATS HIP (3.8/3.7), the skill's gated SIMPLE beat target.
- b8_s26 5.9->5.7, b8_s32 5.9->5.8, b1_s128 11.9->11.8 -- held/marginally better.
- b8_s13/s6/s5 flat at 4.7/4.1/4.1 -- the M64 mid-tail occupancy floor (emit_massive
  VGPR), out of this skill's scope.
- Dense uniform (256 tiles x 304 splits, MLDS): 722.9us -> 722.9us, no regression.
- mult=4 regressed b8_s5 (4.1->4.4); mult=1 is the sweep optimum. Overridable via
  MLA_PS_GRID_MULT.

44/44 correctness (matrix + differential + graph replay).

* mla_reduce: joint-config NUM_THREADS=256 (VEC 4->2, VGPR 193->133)

Joint-search knob NUM_THREADS (MLA_NUM_THREADS) set to 256 as the new
default. Halving VEC = Dv/NUM_THREADS (4->2) cuts the per-thread output
accumulator live-set: whole-kernel VGPR 193->133 and the M256/MLDS
accumulate throughput improves.

Sweep (graph, optionb vs 39dd385): b1_s128 11.8->11.0 (0.86x->0.80x),
mid-tail b8_s32/s26/s13/s6/s5 and SIMPLE b8_s3/s2 unchanged, dense
uniform 722.9->712.8us. 44/44 correctness (matrix + differential +
CUDA-graph replay). No regressions; hard gates (b1_s128<=0.90x, SIMPLE,
dense) all satisfied.

Also plumbs env-overridable joint-search knobs for the search:
MLA_GRP_M256 / MLA_GRP_M64 (accumulate GRP per tier) and MLA_M64_HI_THR
/ MLA_M64_HI_GRP (runtime M64 sub-split), defaults unchanged.

* mla_reduce: capture-safe host per-tier dispatch via num_kv_splits (opt-in)

Realizes the per-tier occupancy win (opt5: M64 alone occ-3, ~0.2-0.3us faster
on the mid-tail; b8_s6/s5 cross below HIP) capture-safely, WITHOUT CUDA-graph
conditional nodes (infeasible on ROCm 7.2.4).

The wrapper now picks the tier on the HOST from num_kv_splits (a pure host
scalar upper bound on per-tile n_splits; no device read/sync) when
AITER_MLA_REDUCE_HOST_TIER=1. Default stays Tier.ALL (unchanged). select_tier
is monotonic and each per-tier body reduces a tile's actual n_splits (tier only
caps LSE-register width), so select_tier(num_kv_splits) is correct for all
tiles. Because num_kv_splits is constant for a fixed CUDA-graph capture config,
PyTorch's per-config capture bakes the correct per-tier kernel and every replay
reuses it -- capture-safe, no extra launch.

Adds 3 wrapper-level graph capture/replay tests (M64, over-provisioned M256,
and the heterogeneous [8,304]->MLDS upper-bound safety case). 47/47 pass.

* mla_reduce: split-K for low-tile/high-split (b1_s128 11.0->7.4us)

Cooperative multi-block split-K reduction for the latency-bound low-tile /
high-split decode case (b1_s128 = 1 active tile x H=16 = 16 active blocks /
304 CUs, each serially reducing 128 splits). Opt-in via AITER_MLA_REDUCE_SPLITK
(default OFF); default path byte-for-byte untouched.

Two-kernel scheme (kernel boundary = free cross-block fence):
  - sk_partial_kernel (grid active_tiles*H*K): each block online-softmax
    partial-reduces a contiguous split subset of one (tile,head) into a
    pre-allocated scratch buffer (weighted acc + running max + sum-exp).
  - sk_combine_kernel (grid active_tiles*H): merges the K partials by global
    max renormalization; lse = ln(sum l)+M, matching the baseline exactly.

plan_splitk engages only when profitable (max_seqlen_q==1, splits>=64,
active_tiles*H<num_cu) from host-visible metadata. Scratch is pre-allocated
once (lru_cache) and reused every CUDA-graph replay: no alloc / device sync /
.item() in the launch path (capture-safe).

Measured (GPU4 MI300X gfx942, graph us, x2): b1_s128 11.0->7.4 (0.67x
baseline, 0.52x HIP 14.0), K=16 sweet spot (K=8 8.4, K=32 8.6). No regression
on any b8_* shape (they don't engage). Correctness 53/53 (47 default + 6 new
split-K: vs torch-ref K in {4,8,16}, vs HIP, cudagraph-replay, default-OFF).

* mla_reduce: guard host-tier dispatch against num_kv_splits under-baking (opt-in)

The num_kv_splits reaching flydsl_mla_reduce_v1 on the real dispatch is
max_split_per_batch (a per-BATCH split budget), NOT a per-tile upper bound.
The metadata (csrc/kernels/mla/metadata/v1_2_device.cuh:858,
num_splits = min(num_clusters, max_split_per_batch * num_batches)) uses it only
as a global payload divisor; the greedy CU load balancer can then concentrate a
skewed batch's reduce tile up to num_cu = num_clusters splits (measured per-tile
n_splits 171 at max_split_per_batch=32). So select_tier(num_kv_splits) = M64/M256
could bake a body whose fixed LSE cap (64/256) is below a real tile's split count
and SILENTLY drop the overflow (smoking gun: abs err 1.83 on a 128-split tile).

Add _safe_host_tier(): under AITER_MLA_REDUCE_HOST_TIER=1 trust only Tier.MLDS
(nlse=5 covers 320 >= LDS_MAX_SPLITS=304 = num_clusters, the hard per-tile
ceiling) and fall back to the always-correct device-side Tier.ALL for every
smaller selection, so the opt-in flag can no longer under-bake. Default
(flag off = Tier.ALL) is byte-identical. The true per-tile bound
get_mla_decode_fwd_max_splits (= cu_num * occupancy = 304) selects MLDS, so the
num_kv_splits=None dispatch path stays correct with no behavior change.

Correctness (both flags on): test_flydsl_mla_reduce.py 47/47;
test_mla_persistent.py (script) uniform + varlen + msb=304 pass. Also fix
docstrings that incorrectly claimed num_kv_splits is a per-tile upper bound.

* mla_reduce: device-adaptive capture-safe split-K, default-on

Fold the b1_s128 cooperative split-K win into the production wrapper
(flydsl_mla_reduce_v1) as a capture-safe, default-on path.

plan_splitk_capture_safe takes its ENTIRE plan from host-only values:
final_output.size(0) (= decode batch = active tiles), num_kv_splits (the
max_split_per_batch budget, a true upper bound on actual per-tile splits),
and num_cu -- no device read/sync -- so it engages under CUDA-graph capture,
unlike the opt-in plan_splitk which reads the CSR via .item(). The per-tile K
allocation stays device-adaptive, so ONE capture is correct across replays
whose per-tile split counts vary.

- b1_s128 graph 11.0 -> 7.4us (0.53x HIP); byte-identical single-kernel path on
  every other shape (heuristic declines when num_kv_splits<64 or grid saturated).
- Correctness: full matrix 57/57 + new test_da_splitk_capture_safe_varying_splits
  (one bs=1 capture stays correct across per-tile splits [128,304,64,200,8,96]).
- Off switch: AITER_MLA_REDUCE_DA_SPLITK=0. Decode-only (max_seqlen_q==1);
  assumes active tiles = CSR prefix. Existing opt-in plan_splitk untouched.

* mla_reduce: gate DA split-K on actual_max_splits (phase-1 prototype)

Add optional actual_max_splits to plan_splitk_capture_safe and
flydsl_mla_reduce_v1 so engagement uses the true max per-tile split width
instead of the loose num_kv_splits budget (~304 on persistent decode).

- derive_actual_max_splits(reduce_indptr): planning-time CSR max (phase 2
  replaces with metadata-emitted scalar).
- When actual_max_splits is set, engage_splits uses it for min_splits gate
  and K sizing; None preserves legacy behavior.
- Crossover probe: short-context over-engage edge closed (splits 2-32
  delta ~0us); b1_s128 win preserved (128 splits: -3.6us vs DA-off).
- Correctness: 61/61 incl. 4 new tests. Opt-in via actual_max_splits= arg;
  not folded to decode until phase-2 C++ metadata emit.

* mla_reduce: emit actual_max_splits from metadata; gate split-K on it (phase-2)

Phase-1 gated device-adaptive split-K on actual_max_splits derived host-side
from the reduce CSR. Phase 2 has get_mla_metadata_v1 emit that scalar natively
so no host CSR reduction is needed.

C++ emit (opt-in, back-compatible):
- New optional trailing output reduce_max_split (1-elem int32) on
  get_mla_metadata_v1. v1_2_device.cuh fills it via atomicMax at the two
  reduce_indptr write sites (parallel: num_frags, serial: num_splits) =
  max_t(reduce_indptr[t+1]-reduce_indptr[t]). Launcher zeroes it with
  hipMemsetAsync on the stream (capture-safe).
- Threaded through metadata.cu, mla.h, pybind (rocm_ops.hpp), attention.py.
- Defaults to nullopt; nullptr guard => non-emitting/HIP callers pay nothing.

Plumbing:
- mla_decode_fwd(actual_max_splits=None) -> both _mla_reduce_v1_dispatch sites
  -> flydsl_mla_reduce_v1. Pure pass-through; no hot-path sync.

Tests (74/74 FlyDSL reduce matrix; +13 phase-2):
- emitted scalar == derive_actual_max_splits for both planners x 10 shapes
- metadata-sourced crossover + over-provisioned-budget edge closure
- dispatch forwards actual_max_splits
- persistent decode e2e (bs1 ctx8192 bf16+fp8): decode err = 0

Overhead is opt-in and on the once-per-shape planning path only
(+2.7-7.4us metadata), cheaper than the phase-1 device .max() reduction.

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

* mla_reduce: lv3 depth lever - default-on M64 deep sub-split (GRP16, -5% b8_s32/s26 graph)

The accumulate loop is already a GRP-wide double-rate software pipeline
(vmcnt overlap up to 15, well beyond the HIP depth-2 oaccu_0/oaccu_1), so
the literal "depth-1 -> depth-2" conversion would regress. The remaining
pipeline-depth win is in the M64 path: enable the existing device-side
sub-split by default (M64_HI_THR 0->8) so high-split M64 tiles take the
deeper GRP=16 accumulate (more os buffer_loads in flight) while the
low-split tail keeps GRP=8.

Measured graph us (optionb, same session): b8_s32 5.9->5.6 (-5%,
0.97->0.92x HIP), b8_s26 5.9->5.6, b8_s13 4.7->4.5; b1_s128 (M256) and
the SIMPLE tail (b8_s3) unchanged; b8_s6/s5 keep GRP=8 (byte-identical).
ISA: vmcnt max 15->24, buffer_load 97->122 (deeper overlap confirmed).
Correctness: 74/74 pytest matrix pass. Capture-safe (device branch).

* mla_reduce: adaptive active_tiles×H launch (default-on, multi-tile only)

Launch one block per active (tile, head) on sparse multi-tile decode instead
of the persistent grid-stride kernel, eliminating the traversal WhileOp floor.
Gated: num_final_rows > 1 (split-K owns bs=1), num_final_rows < num_reduce_tile.
Opt-out: AITER_MLA_REDUCE_ADAPTIVE_LAUNCH=0. b8_s32 graph 5.6→5.2µs (1.15× HIP).

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

* mla_reduce: drop redundant accumulate mask from GRP pipeline state

load_scales already zeroes invalid split scales and OOB os reads are
pmap0-substituted, so carrying mask_g through the loop only added dead
VALU. Removes ~230 instructions and cuts b8_s32 graph 5.2→4.9µs on the
production adaptive path without changing SIMPLE or split-K behavior.

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

* mla_reduce: low-split direct pmap path for adaptive decode

When actual_max_splits is known and <= 8, compile a separate adaptive kernel
that reads reduce_partial_map directly instead of staging to LDS first. This
removes the fixed pmap-staging barrier on low-split serving shapes while
high-split captures keep the vectorized LDS pmap path via separate JIT entries.

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

* bench_mla_reduce: add GLM-5.2 serving scoreboard as default path

Make PR numbers reproducible from the aiter tree: default bench runs the
production wrapper (trimmed final_output, actual_max_splits, adaptive +
DA split-K) against hip/wrapper-daoff/wrapper-daon. Keep uniform/irregular/replay
under --mode for synthetic and metadata replay sweeps.

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

* Drop actual_max_splits from mla_decode_fwd; resolve via warmup cache.

Auto-derive the split-K gate input inside flydsl_mla_reduce_v1 from
reduce_indptr using a capture-safe warmup-populated cache, so callers
need not thread actual_max_splits through mla.py. Removes the stale
dispatch docstring and fixes mla_prefill_ps_fwd forwarding a missing arg.

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

* bench_mla_reduce: use production actual_max_splits auto-resolve

Stop passing actual_max_splits explicitly in the serving harness; let
flydsl_mla_reduce_v1 resolve it from reduce_indptr via the warmup cache
so graph replay matches mla_decode_fwd. Plan annotation still uses
derive_actual_max_splits at host planning time.

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

* mla_reduce: always-on DA split-K; simplify bench to hip/wrapper

Remove AITER_MLA_REDUCE_DA_SPLITK and da_splitk_enabled(); split-K engage
logic in plan_splitk_capture_safe is always active. Bench backends are now
hip and wrapper only (production flydsl_mla_reduce_v1 path).

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

* mla_reduce: drop AI-narrative comments, simplify wrapper/bench/tests

Trim docstrings to production style, remove opt-in host-tier/adaptive-launch
env gates (Tier.ALL + adaptive-on are now unconditional), dedupe wrapper
setup, rewrite the benchmark to the standard @benchmark()/pandas pattern,
and drop dead helpers left over from the cleanup.

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

* mla_reduce: restore uniform/irregular benches and fix test/dispatch issues

Restore uniform and irregular benchmark sweeps under the @benchmark
interface. Re-read AITER_MLA_REDUCE_FLYDSL on each dispatch (cache only
FlyDSL availability) and work around module_mla_metadata check_args
failures in reduce_max_split tests.

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

* mla_reduce: fix black/ruff style on kernel and hip bench

Remove an unused import, rename ambiguous loop variables, and apply
black formatting so branch Python files pass lint checks.

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

* mla_reduce: fix HIP reference LDS sizing on GPUs with <304 CUs

hip_ref/hip_ref_like_fout passed num_kv_splits=0, so the HIP kernel
sized its LDS from the GPU's CU count. On gfx950 (256 CUs) that
undersizes the buffer for fixtures using up to LDS_MAX_SPLITS (304)
splits, causing failures on MI35X CI while gfx942 (304 CUs) passed.

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

* mla_reduce: fix CI failures on metadata tests and MI35X

Forward reduce_max_split through get_mla_metadata_v1 wrapper so metadata
gate tests can call the public API, and scope FlyDSL MLA reduce tests to
gfx942 only until MI35X is a supported target.

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

* remove unnecessary pytest markers section

* mla_reduce: remove superseded reduce_max_split metadata emission

The phase-2 reduce_max_split output on get_mla_metadata_v1 was replaced
by the capture-safe warmup cache (_resolve_actual_max_splits), which
resolves actual_max_splits host-side without threading a metadata scalar.
It was dead in production (only tests used it), so drop it entirely from
the C++ metadata kernel/binding, the Python op + wrapper, and its tests.

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

* Restore CP round-robin comment dropped by unrelated cleanup

The comment documenting round-robin context-parallel semantics was
removed as collateral damage in a Claude-comment cleanup pass; it
doesn't belong to this branch's work, so restore it.

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

* space

* mla_reduce: fold HIP baseline bench into FlyDSL script, drop replay mode

Add an opt-in --include-hip flag to the FlyDSL benchmark so it can run
the production HIP kernel as a comparison candidate across all sweeps,
and remove the now-redundant standalone HIP bench (and its unused
replay mode).

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

* Fix MLA reduce test imports for CI.

CI runs op tests with python3 directly, so fix the import path for flydsl_mla_reduce_common.

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

* test(mla_reduce): merge FlyDSL perf bench into the correctness suite

Drop pytest in favor of a plain python3 entrypoint so aiter CI's
exit-code-based test runner actually exercises this suite, then fold the
standalone bench script's serving/uniform/irregular perf sweeps in as the
tail of the same run (gate first, sweep only if it passes).

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

* style(mla_reduce): black-format test file

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

* Inline flydsl_mla_reduce_common into test_flydsl_mla_reduce

Merge the single-use helper module into its only consumer so the op test
is self-contained, per review feedback on PR 3901.

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

* refactor FlyDSL MLA reduce APIs

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

* fix FlyDSL MLA runtime and cache isolation

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

* fix: harden FlyDSL MLA reducer safety

Keep the validated reducer changes and CI corrections while excluding internal PR notes from the source tree.

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

* refactor(flydsl): remove env-var tuning knobs from mla_reduce

Replace all os.environ reads in the kernel with function parameters
(defaults unchanged), and drop a dead math-wrapper branch.

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

* test(flydsl): consolidate MLA reduce coverage

Merge eager/CUDA-graph replay test pairs behind a replay flag, fold the
small_split graph case into the shared case table, extract shared
helpers for the guard-differential and split-K b1_s128 tests, replace
the hand-written run_checks() call list with a data-driven registry,
dedupe benchmark roofline/candidate/table logic across the three perf
sweeps, and drop redundant local imports/duplicated constants. No
scenario, assertion, or CLI behavior changes.

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

* refactor(flydsl): move MLA scratch and output stores off buffer_ops

Two of the four raw buffer_ops paths in mla_reduce now go through the public
layout API, cutting direct call sites from 13 to 5. The f32 scratch load and
store use a buffer tensor with a copy atom over a row view; VEC=8 f32 is wider
than one atom, so it composes two atoms over adjacent chunks -- the same shape
fused_compress_attn already uses, expressed through the layout API.

The final-output store moves into shared helpers used by both the normal and
split-K combine kernels, where a single atom always covers the fragment. A
probe confirmed the layout copy still emits one packed store, refuting an
in-code comment that had claimed it would scalarize the write.

Invariant checks pass, ISA opcode counts are identical across all four emitted
kernels, VGPR counts are unchanged, and the perf sweep shows no regression. The
two paths still on buffer_ops are documented in the code.

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

* refactor(flydsl): drop remaining buffer_ops from mla_reduce

mla_reduce no longer references buffer_ops. The indptr reads use a plain
layout view, which the backend already lowers to a scalar load, so the raw
descriptor and the explicit readfirstlane broadcast were both unnecessary.
The partial-output load now uses the same slice/copy form as the output
store; the regression seen in the earlier attempt came from a redundant
predicate, not from the load itself. The f32 atom/chunk helpers move to
module level so the 2-D and 3-D paths share one composition.

All bounds guards are unchanged. Invariant checks pass, the split-K ISA is
byte-identical, VGPR counts are unchanged, and the perf sweep shows no
regression.

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

* fix(flydsl): satisfy Ruff 0.16 style checks

Remove stale noqa and simplify mechanical expressions flagged by the pinned CI linter.

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

* refactor(flydsl): clean up MLA reduce dispatch and tests

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

* Update aiter/mla.py

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* refactor(flydsl): use empty split-K scratch buffers

The partial kernel fully overwrites scratch rows each launch, so zero-init is unnecessary.

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

* Keep FlyDSL MLA reduction decode-only

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant