Skip to content

gemm: add fp8 per-tensor grouped GEMM forward (M-grouped/MoE)#887

Open
kyle-256 wants to merge 1 commit into
mainfrom
dev/kyle/grouped_gemm
Open

gemm: add fp8 per-tensor grouped GEMM forward (M-grouped/MoE)#887
kyle-256 wants to merge 1 commit into
mainfrom
dev/kyle/grouped_gemm

Conversation

@kyle-256

Copy link
Copy Markdown

Motivation

FlyDSL currently ships dense fp8 GEMMs (kernels/gemm/fp8_gemm_4wave.py /
fp8_gemm_8wave.py) but has no grouped (M-grouped / MoE) GEMM. MoE models
need a grouped GEMM where several per-expert matmuls with a shared K but
different row counts run as one launch. This PR adds the fp8 per-tensor
grouped GEMM forward (NT: out = a @ b^T), reusing the existing dense fp8
GEMM primitives so it stays consistent with the rest of kernels/gemm.

Technical Details

New kernel kernels/gemm/fp8_grouped_gemm.py:

  • Layout. a = [M_total, K] fp8 (the groups concatenated along M),
    b_T = [G, N, K] fp8 (per-group weights), out = [M_total, N],
    group_offs = [G+1] int64 splitting M_total into G groups. group_offs
    is read as an int32 view (a free reinterpret; low word at i32[2*idx]).
  • CPU-sync-free grouping. The grid is over-launched to a host upper bound
    (ceil(M_total/BLOCK_M) + G) * n_blocks (no host read of the group lengths).
    Each WG computes the true total_tiles on-device via an O(G) scan and
    s_endpgms when its tile id is past the end (one tile / WG, full-device
    default). The same scan maps a tile id → (group_idx, local tile).
  • int64-safe per-group addressing. A/B loads fold the group element offset
    (m_start*K / group_idx*N*K) into the i64 SRD base, so in-tile offsets stay
    int32 even for A/B beyond 2^31 elems / 4 GB; the SRD num_records bound clamps
    the last over-read to 0. The C store passes c_rows = group_offs[group_idx+1]
    (the absolute group-end row) so a partial M-tile's extra rows (which belong to
    the next group) are dropped by the row-band SRD bound — no spill across groups.
  • Reuse. The shared LDS coop-loaders (G2SLoader / S2RLoader), global
    swizzle, and barriers come from fp8_gemm_utils. The grouped-specific
    primitives — i64-rebased SRD, row-band scalar store, in-place AGPR MFMA,
    K-tail mask, L2-reuse tile swizzle, and XCD remap — are local to the new file.
  • Scales are scalar per-tensor (a_scale, b_scale).
  • Ported from Primus-Turbo, authored with FlyDSL.

Public entry: grouped_gemm_fp8_forward(a, b_T, a_scale, b_scale, group_offs, out_dtype=torch.bfloat16), with a compile cache keyed by the static dims
(N, K, G, out_fp16, cbsz, blgp).

Also adds tests/kernels/test_fp8_grouped_gemm.py (correctness + perf; a torch
per-group reference and a CLI benchmark entry).

Test Plan

Run on gfx950 (MI355X) with flydsl 0.2.2:

pytest tests/kernels/test_fp8_grouped_gemm.py -q

The parametrized cases deliberately cover the group-handling edge paths:
aligned groups, ragged (non-BLOCK_M-multiple) groups, an empty group
(M_g = 0), a single-token group (M_g = 1), and a large-K shape — so the
on-device group scan, K-tail masking, and partial-M-tile / group-boundary clamps
are all exercised. Correctness is checked against a torch per-group reference
with verify_output(rtol=0.1, atol=0.1).

Test Result

5/5 pass:

case shape (group_ms / N / K) TFLOPS
aligned [256,256,256,256] / 512 / 512 41.7
ragged [300,128,700,45] / 1024 / 768 126.4
empty group [1024,0,512,1536] / 2048 / 2048 972.5
single token [1,255,4096] / 4096 / 1024 1050.2
large-K [2048,2048] / 4096 / 7168 2707.8
======================== 5 passed, 1 warning in 14.55s =========================

Submission Checklist

Copilot AI review requested due to automatic review settings July 23, 2026 06:47

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

Adds a new fp8 per-tensor M-grouped (MoE-style) GEMM forward kernel to kernels/gemm, plus a GPU correctness/perf test harness to exercise grouped edge cases (ragged groups, empty group, single-token group).

Changes:

  • Introduces kernels/gemm/fp8_grouped_gemm.py: a grouped NT fp8 GEMM forward kernel that over-launches the grid and maps tiles to groups on-device, with i64 SRD rebasing and row-band bounded stores.
  • Adds a public Python entrypoint grouped_gemm_fp8_forward(...) with a compile cache keyed by (N, K, G, out_fp16, cbsz, blgp).
  • Adds tests/kernels/test_fp8_grouped_gemm.py covering correctness and reporting TFLOPS across representative grouped shapes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
kernels/gemm/fp8_grouped_gemm.py New grouped fp8 GEMM forward kernel + Python entrypoint/compile cache.
tests/kernels/test_fp8_grouped_gemm.py New grouped fp8 GEMM correctness + perf harness and benchmark CLI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +753 to +758
assert a.ndim == 2 and b_T.ndim == 3, f"a{tuple(a.shape)} b_T{tuple(b_T.shape)}"
M_total, K = a.shape
G, N, K_b = b_T.shape
assert K == K_b, f"K mismatch a={K} b_T={K_b}"

out = torch.empty((M_total, N), device=a.device, dtype=out_dtype)
Comment on lines +759 to +763
# The kernel reads group_offs as int64 low-words via a free int32-view (no
# .to(int32) cast); int32 callers are upcast to int64 once.
_go64 = group_offs if group_offs.dtype == torch.int64 else group_offs.to(torch.int64)
go32 = _go64.view(torch.int32)
out_fp16 = out_dtype == torch.float16
Comment thread kernels/gemm/fp8_grouped_gemm.py Outdated
Comment on lines +767 to +772
key = (N, K, G, out_fp16, cbsz, blgp)
launch = _GROUPED_FWD_CACHE.get(key)
if launch is None:
launch = compile_fp8_grouped_gemm(
K=K, G=G, out_fp16=out_fp16, cbsz=cbsz, blgp=blgp
)
Comment on lines +208 to +220
gSA = fx.rocdl.make_buffer_tensor(A_scale, max_size=False, num_records_bytes=4) # 1 fp32
gSB = fx.rocdl.make_buffer_tensor(B_scale, max_size=False, num_records_bytes=4) # 1 fp32
self.sa_div = fx.logical_divide(gSA, fx.make_layout(1, 1))
self.sb_div = fx.logical_divide(gSB, fx.make_layout(1, 1))
self.scale_atom_1 = fx.make_copy_atom(fx.rocdl.BufferCopy32b(), fx.Float32)
self.reg_f32_1 = fx.make_rmem_tensor(fx.make_layout(1, 1), fx.Float32)

def _load_scalar(self, div):
fx.copy(self.scale_atom_1, fx.slice(div, (None, fx.Int32(0))), self.reg_f32_1)
return Vec(fx.memref_load_vec(self.reg_f32_1))[0]

def store(self, c_frag, base_row, base_col):
scale = self._load_scalar(self.sa_div) * self._load_scalar(self.sb_div)
ok = verify_output(out.to(torch.float32), c_ref, rtol=0.1, atol=0.1)
assert ok, f"correctness failed for G={G} M_total={M_total} N={N} K={K}"

_, us = run_perftest(lambda _o: _launch(), out, num_iters=num_iters, num_warmup=num_warmups)
@kyle-256
kyle-256 force-pushed the dev/kyle/grouped_gemm branch from 51ca972 to fa9c760 Compare July 23, 2026 07:20
Adds a grouped (M-grouped / MoE) fp8 per-tensor GEMM forward kernel
(NT: out = a @ b^T) under kernels/gemm, plus a correctness+perf test.

The grid is over-launched to a host upper bound; each WG computes the
true tile count via an on-device O(G) scan (no host read of group lens)
and s_endpgm's when past the end. The same scan maps a tile id ->
(group, local tile). Per-group A/B addressing folds the group element
offset into the i64 SRD base (int64-safe > 4GB), and the C store passes
the absolute group-end row so partial-M tiles clamp cleanly at group
boundaries. Scales are scalar per-tensor.

Reuses the shared LDS coop-loaders / global swizzle / barriers from
fp8_gemm_utils; grouped-specific primitives are kept local. Ported from
Primus-Turbo, authored with FlyDSL.

Verified on gfx950 (MI355X) with flydsl 0.2.2: 5/5 cases pass
(aligned, ragged, empty-group, single-token, large-K).
@kyle-256
kyle-256 force-pushed the dev/kyle/grouped_gemm branch from fa9c760 to be9f88d Compare July 23, 2026 08:14
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.

2 participants