|
| 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)) |
0 commit comments