Skip to content

Commit 47ed57a

Browse files
authored
MoE: add a16w mix fused 2-stage kernels (bf16 A × mxfp4/int4/bf16 W) (#948)
1 parent 7e6dca0 commit 47ed57a

13 files changed

Lines changed: 3272 additions & 6071 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (c) 2025 FlyDSL Project Contributors
3+
"""Fused a16w-mix (bf16 A x mxfp4/int4 W) 2-stage MoE kernels.
4+
5+
Kernel builders live in :mod:`gemm1` / :mod:`gemm2` (shared helpers in
6+
:mod:`utils`). Host launch/tile-config/CSV glue is a test-side concern and
7+
lives in ``tests/kernels/moe_a16wmix_host.py``.
8+
"""

kernels/moe/moe_2stage_a16wmix/gemm1.py

Lines changed: 916 additions & 0 deletions
Large diffs are not rendered by default.

kernels/moe/moe_2stage_a16wmix/gemm2.py

Lines changed: 675 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (C) 2025-2026 FlyDSL Project Contributors
3+
4+
"""Shared low-level helpers for the a16w4/a16wi4/a16w16 fused MoE kernels
5+
(:mod:`gemm1` stage1 and :mod:`gemm2` stage2). Pointer/GEP builders, buffer-tensor
6+
views, e8m0/int4 dequant, the A-LDS XOR swizzle, and the arch gate."""
7+
8+
import os
9+
10+
import flydsl.expr as fx
11+
from flydsl._mlir import ir
12+
from flydsl._mlir.dialects import llvm
13+
from flydsl.expr import arith, range_constexpr, rocdl
14+
from flydsl.expr.typing import T
15+
from flydsl.runtime.device import get_rocm_arch
16+
from kernels.common import buffer_ops
17+
18+
_PTR3 = "!llvm.ptr<3>"
19+
LOG2E = 1.4426950408889634
20+
21+
# a16wi4 (int4 W) groupwise scale: group_size = 32 == one MFMA K32 step (one ku per
22+
# K-group). Scale packed bf16 pairs (E, N, G//2, 2); even/odd ku selects lo/hi half.
23+
A16WI4_GROUP_SIZE = 32
24+
25+
26+
def a16wmix_use_k16(arch=None):
27+
"""True for the gfx942 (CDNA3) codepath: K=16 MFMA + scalar int4 dequant.
28+
29+
Arch-gate: gfx950 (CDNA4) has K=32 mfma_f32_16x16x32_bf16 + v_cvt_pk_bf16_f32;
30+
gfx942 has neither and falls back to K=16 MFMA + scalar-trunc dequant.
31+
``FLYDSL_A16WMIX_FORCE_K16=1`` forces the gfx942 path (a strict ISA subset) for
32+
validation on a gfx950 box.
33+
"""
34+
if os.environ.get("FLYDSL_A16WMIX_FORCE_K16", "0") not in ("0", "", "false", "False"):
35+
return True
36+
if arch is None:
37+
arch = get_rocm_arch() or ""
38+
return "gfx95" not in str(arch)
39+
40+
41+
def _raw(v):
42+
if not isinstance(v, ir.Value) and hasattr(v, "ir_value"):
43+
return v.ir_value()
44+
return v
45+
46+
47+
def _udiv(a, c):
48+
cc = fx.Int32(c) if isinstance(c, int) else c
49+
return fx.Int32(arith.divui(_raw(a), _raw(cc)))
50+
51+
52+
def _umod(a, c):
53+
cc = fx.Int32(c) if isinstance(c, int) else c
54+
return fx.Int32(arith.remui(_raw(a), _raw(cc)))
55+
56+
57+
def _global_i32_buffer_view(addr_i64, num_bytes):
58+
# fx.copy BufferCopy atoms take soffset as an element count (not bytes); the
59+
# make_layout dynamic-shape leaf must be i32/i64, not fx.Index.
60+
num_bytes_i64 = fx.Int64(num_bytes)
61+
ptr_ty = fx.PointerType.get(T.i32, address_space=fx.AddressSpace.Global, alignment=4)
62+
ptr = fx.inttoptr(ptr_ty, fx.Int64(addr_i64))
63+
view = fx.Tensor(fx.make_view(ptr, fx.make_layout(num_bytes_i64 // fx.Int64(4), 1)))
64+
return fx.rocdl.make_buffer_tensor(view, max_size=False, num_records_bytes=num_bytes_i64)
65+
66+
67+
def _global_i32_buffer_tiles(addr_i64, num_bytes, tile_elems):
68+
return fx.logical_divide(_global_i32_buffer_view(addr_i64, num_bytes), fx.make_layout(tile_elems, 1))
69+
70+
71+
def _buffer_i32_scalar_read(tiles1, idx, atom):
72+
"""Read one i32 dword at element ``idx`` from a ``_global_i32_buffer_tiles(..., 1)``
73+
view via the layout-API BufferCopy atom (buffer_load_dword; OOB-clamped by the
74+
buffer resource). ``tiles1`` is 1-dword tiles so the tile index == ``idx``.
75+
"""
76+
r = fx.make_rmem_tensor(fx.make_layout(1, 1), fx.Int32)
77+
fx.copy(atom, fx.slice(tiles1, (None, idx)), r)
78+
return fx.Int32(fx.Vector(fx.memref_load_vec(r))[0])
79+
80+
81+
def _lds_ptr3(base_i32, byte_off_i32):
82+
addr_i64 = fx.Int64(base_i32 + byte_off_i32)
83+
return llvm.inttoptr(ir.Type.parse(_PTR3), _raw(addr_i64))
84+
85+
86+
def _gep3(base_ptr, byte_off_i32):
87+
return buffer_ops.get_element_ptr(base_ptr, byte_offset=_raw(byte_off_i32), elem_type=T.i8)
88+
89+
90+
def _global_base_ptr1(addr_i64):
91+
return llvm.inttoptr(ir.Type.parse("!llvm.ptr<1>"), _raw(fx.Int64(addr_i64)))
92+
93+
94+
def _gep1(base_ptr, byte_off_i32):
95+
return buffer_ops.get_element_ptr(base_ptr, byte_offset=_raw(byte_off_i32), elem_type=T.i8)
96+
97+
98+
def _global_i32_ptr(addr_i64):
99+
ptr_ty = fx.PointerType.get(T.i32, address_space=fx.AddressSpace.Global, alignment=4)
100+
return fx.inttoptr(ptr_ty, fx.Int64(addr_i64))
101+
102+
103+
def _global_i32_at(addr_i64, idx):
104+
return _global_i32_ptr(addr_i64)[idx]
105+
106+
107+
def _e8m0_byte_to_f32(packed_i32, byte_pos):
108+
shift = byte_pos * fx.Int32(8)
109+
b = packed_i32.shrui(shift) & fx.Int32(0xFF)
110+
return fx.Float32(_raw(b << fx.Int32(23)).bitcast(T.f32))
111+
112+
113+
def _cvt_pk_bf16_f32_se(src_a_f32, src_b_f32):
114+
# Side-effecting v_cvt_pk_bf16_f32 (pack 2 f32 -> 2xbf16 in i32). LOAD-BEARING:
115+
# the stateless rocdl.cvt_pk_bf16_f32 gets CSE-merged/reordered across K steps in
116+
# the a16wi4 gemm1 hot loop (garbage output); side_effects pins each call.
117+
return llvm.inline_asm(
118+
ir.IntegerType.get_signless(32),
119+
[_raw(src_a_f32), _raw(src_b_f32)],
120+
"v_cvt_pk_bf16_f32 $0, $1, $2",
121+
"=v,v,v",
122+
has_side_effects=True,
123+
)
124+
125+
126+
def _int4_nibble_to_bf16x8(raw_i32, scale_f32, *, use_k16=False):
127+
"""int4 (signed) -> bf16 upconvert for one MFMA K32 step (8 nibbles -> v8bf16).
128+
129+
``raw_i32`` holds 8 signed-int4 nibbles in bits[4n+3:4n] (same K order as the
130+
mxfp4 sel 0..3 path). ``v_cvt_off_f32_i4`` reads the nibble unsigned, subtracts 8,
131+
and scales the mantissa by 16, so the x16 is folded into eff = scale*16.
132+
``use_k16`` (gfx942): v_cvt_pk_bf16_f32 is gfx950-only -> scalar .to(BFloat16).
133+
"""
134+
eff = fx.Float32(scale_f32 * fx.Float32(16.0))
135+
raw_even = fx.Int32(raw_i32)
136+
raw_odd = raw_even.shrui(fx.Int32(4))
137+
if use_k16:
138+
# gfx942 fallback: scalar f32 -> bf16 truncation (no v_cvt_pk_bf16_f32).
139+
bf16s = []
140+
for j in range_constexpr(4):
141+
f_lo = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_even), byte_sel=j)) * eff
142+
f_hi = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_odd), byte_sel=j)) * eff
143+
bf16s.append(f_lo.to(fx.BFloat16))
144+
bf16s.append(f_hi.to(fx.BFloat16))
145+
return fx.Vector.from_elements([_raw(x) for x in bf16s], fx.BFloat16) # v8bf16
146+
# byte_sel loads (1 shift total); side-effecting pk-convert.
147+
i32s = []
148+
for j in range_constexpr(4):
149+
f_lo = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_even), byte_sel=j)) * eff
150+
f_hi = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_odd), byte_sel=j)) * eff
151+
i32s.append(fx.Int32(_cvt_pk_bf16_f32_se(_raw(f_lo), _raw(f_hi))))
152+
v4i32 = fx.Vector.from_elements([_raw(x) for x in i32s], fx.Int32)
153+
return v4i32.bitcast(fx.BFloat16) # v8bf16
154+
155+
156+
def _int4_nibble_to_bf16x8_raw(raw_i32, *, use_k16=False):
157+
"""int4 (signed) -> bf16 for one MFMA K32 step WITHOUT the groupwise scale.
158+
159+
Same as :func:`_int4_nibble_to_bf16x8` but emits the raw dequant weights
160+
``(nibble-8)/16`` (``v_cvt_off_f32_i4``'s native output -- no per-element
161+
``v_mul_f32``). The groupwise scale (and the folded x16) is applied ONCE per
162+
K-group on the small MFMA accumulator instead (see the ``_acc_scale_int4`` path in
163+
the stage1 body): for BM16 (m_repeat=1) that trades 8 per-nibble muls for 4
164+
per-accumulator fmas and drops the long-lived scaled-f32 operand VGPRs.
165+
``(nibble-8)/16`` is bf16-exact (values in ``{-7/16..7/16}``).
166+
``use_k16`` (gfx942): v_cvt_pk_bf16_f32 is gfx950-only -> scalar .to(BFloat16).
167+
"""
168+
raw_even = fx.Int32(raw_i32)
169+
raw_odd = raw_even.shrui(fx.Int32(4))
170+
if use_k16:
171+
bf16s = []
172+
for j in range_constexpr(4):
173+
f_lo = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_even), byte_sel=j))
174+
f_hi = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_odd), byte_sel=j))
175+
bf16s.append(f_lo.to(fx.BFloat16))
176+
bf16s.append(f_hi.to(fx.BFloat16))
177+
return fx.Vector.from_elements([_raw(x) for x in bf16s], fx.BFloat16) # v8bf16
178+
i32s = []
179+
for j in range_constexpr(4):
180+
f_lo = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_even), byte_sel=j))
181+
f_hi = fx.Float32(rocdl.cvt_off_f32_i4(_raw(raw_odd), byte_sel=j))
182+
i32s.append(fx.Int32(_cvt_pk_bf16_f32_se(_raw(f_lo), _raw(f_hi))))
183+
v4i32 = fx.Vector.from_elements([_raw(x) for x in i32s], fx.Int32)
184+
return v4i32.bitcast(fx.BFloat16) # v8bf16
185+
186+
187+
def kmchunks_for(BM):
188+
return BM // 16
189+
190+
191+
def lds_acc_bytes_for(rows, BN):
192+
return rows * BN * 4
193+
194+
195+
def _a16w4_swizzle_xor16(row, col_bytes, k_blocks16, *, enable=False):
196+
"""A-LDS bank-conflict XOR swizzle (aiter swizzle_xor16: col ^ ((row&(kb16-1))*16)).
197+
198+
Both the DMA write and the LDS read go through this helper so the physical layout
199+
stays consistent. gemm1 keeps linear (enable=False); gemm2 enables it.
200+
"""
201+
if not enable:
202+
return col_bytes
203+
rem = row & fx.Int32(k_blocks16 - 1)
204+
return col_bytes ^ (rem * fx.Int32(16))

kernels/moe/moe_gemm_2stage/__init__.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)