From 0a6dd383496d4b2b5503313fbb949c6949269b69 Mon Sep 17 00:00:00 2001 From: v-tairan Copilot user Date: Sat, 30 May 2026 09:24:19 +0000 Subject: [PATCH 1/2] [wip] blackwell: sm100 INT4 grouped GEMM Triton stub Placeholder commit for draft PR. Implementation tracked in: batchgen-agent-metadata/batchgen_design/blackwell/blackwell-kernel-port-v1.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/BLACKWELL_KERNELS_WIP.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/BLACKWELL_KERNELS_WIP.md diff --git a/docs/BLACKWELL_KERNELS_WIP.md b/docs/BLACKWELL_KERNELS_WIP.md new file mode 100644 index 00000000..55dc7aa7 --- /dev/null +++ b/docs/BLACKWELL_KERNELS_WIP.md @@ -0,0 +1 @@ +# WIP: [wip] blackwell: sm100 INT4 grouped GEMM Triton stub From 3d6941a737319c002bfd7b806ea2f70ed9ff7d36 Mon Sep 17 00:00:00 2001 From: v-tairan Copilot user Date: Sat, 30 May 2026 14:28:51 +0000 Subject: [PATCH 2/2] feat(sm100): INT4 (W4A16) grouped GEMM Triton kernel (K2.5 decode) Pure-Triton port of the Hopper WGMMA INT4 grouped MoE GEMM building block. Matches the SM90a .cu weight layout: w_packed [N, K//2] uint8 (low nibble = even-K, high nibble = odd-K), scale [N, K//32] bf16, group_size=32, dequant (nibble-8)*scale. GEMM via even/odd column split + two bf16 tl.dot with FP32 accumulation (avoids in-kernel nibble interleave). - batchgen_kernels/triton/int4_grouped_gemm.py: int4_grouped_gemm (single expert) + int4_moe_grouped_gemm (per-expert sorted-token dispatch) - triton/__init__.py: export both Verified on B200: max_abs_err 0.07-0.18 (rel ~0.3%) vs dequant reference, well under the 0.5 INT4-noise threshold; grouped path matches. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- batchgen_kernels/triton/__init__.py | 4 + batchgen_kernels/triton/int4_grouped_gemm.py | 163 +++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 batchgen_kernels/triton/int4_grouped_gemm.py diff --git a/batchgen_kernels/triton/__init__.py b/batchgen_kernels/triton/__init__.py index 27874ab1..0a3838be 100644 --- a/batchgen_kernels/triton/__init__.py +++ b/batchgen_kernels/triton/__init__.py @@ -33,3 +33,7 @@ from batchgen_kernels.triton.fused_dequant_gemm import fused_fp8_bf16_gemm from batchgen_kernels.triton.fused_q_absorb import fused_q_absorb_query_states from batchgen_kernels.triton.fused_out_absorb import fused_out_absorb_reshape +from batchgen_kernels.triton.int4_grouped_gemm import ( + int4_grouped_gemm, + int4_moe_grouped_gemm, +) diff --git a/batchgen_kernels/triton/int4_grouped_gemm.py b/batchgen_kernels/triton/int4_grouped_gemm.py new file mode 100644 index 00000000..adf92538 --- /dev/null +++ b/batchgen_kernels/triton/int4_grouped_gemm.py @@ -0,0 +1,163 @@ +"""INT4 (W4A16) GEMM Triton kernels for SM100 (Blackwell). + +Pure-Triton port of the Hopper WGMMA INT4 grouped MoE kernels (K2.5 decode) +whose `.cu` uses `wgmma`/TMA intrinsics not built for sm_100a. + +Weight / scale layout (matches the SM90a `.cu`, e.g. +`single_expert_int4_wgmma.cu::load_decode_rhs_int4_swizzled`): + * `w_packed` : [N, K // 2] uint8 — output-channel major, two INT4 values per + byte along K. The **low** nibble `(byte & 0x0F)` is the even K index, the + **high** nibble `((byte >> 4) & 0x0F)` is the odd K index. + * `scale` : [N, K // group_size] bf16 — one scale per group of + `group_size` (=32) contiguous K values, shared across the group. + * dequant : `w[n, k] = (nibble - 8) * scale[n, k // group_size]` + * GEMM : `out[m, n] = sum_k x[m, k] * w[n, k]` (i.e. `x @ dequant(W).T`). +""" + +import torch +import triton +import triton.language as tl + + +@triton.autotune( + configs=[ + triton.Config({'BLOCK_M': 16, 'BLOCK_N': 64, 'BLOCK_KP': 64}, num_stages=4, num_warps=4), + triton.Config({'BLOCK_M': 32, 'BLOCK_N': 128, 'BLOCK_KP': 64}, num_stages=3, num_warps=4), + triton.Config({'BLOCK_M': 16, 'BLOCK_N': 128, 'BLOCK_KP': 64}, num_stages=4, num_warps=4), + triton.Config({'BLOCK_M': 64, 'BLOCK_N': 128, 'BLOCK_KP': 32}, num_stages=3, num_warps=8), + ], + key=['M', 'K', 'N', 'GROUP_SIZE'], +) +@triton.jit +def _int4_gemm_kernel( + x_ptr, w_ptr, scale_ptr, out_ptr, + M, K, N, + stride_xm, stride_xk, + stride_wn, stride_wk, # w_packed: [N, K//2] uint8 + stride_sn, stride_sk, # scale: [N, K//group_size] bf16 + stride_om, stride_on, + GROUP_SIZE: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_KP: tl.constexpr, # packed-K (bytes) per iteration; real-K = 2*BLOCK_KP +): + pid_m = tl.program_id(0) + pid_n = tl.program_id(1) + + offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M) + offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N) + offs_kp = tl.arange(0, BLOCK_KP) + + Kp = K // 2 + # packed-K bytes per scale group (group_size K-values -> group_size//2 bytes) + GROUP_BYTES = GROUP_SIZE // 2 + + m_mask = offs_m[:, None] < M + n_mask_out = offs_n[None, :] < N # [1, BLOCK_N] for output/x-side + n_mask_w = offs_n[:, None] < N # [BLOCK_N, 1] for weight/scale rows + + acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32) + for kp_start in range(0, Kp, BLOCK_KP): + kp = kp_start + offs_kp # [BLOCK_KP] packed indices + kp_mask = kp < Kp + + # x even / odd columns: x[:, 2*kp] and x[:, 2*kp+1] + x_even = tl.load( + x_ptr + offs_m[:, None] * stride_xm + (2 * kp[None, :]) * stride_xk, + mask=m_mask & kp_mask[None, :], other=0.0, + ) + x_odd = tl.load( + x_ptr + offs_m[:, None] * stride_xm + (2 * kp[None, :] + 1) * stride_xk, + mask=m_mask & kp_mask[None, :], other=0.0, + ) + + # packed weight byte [BLOCK_N, BLOCK_KP] + wb = tl.load( + w_ptr + offs_n[:, None] * stride_wn + kp[None, :] * stride_wk, + mask=n_mask_w & kp_mask[None, :], other=0, + ) + lo = ((wb & 0x0F).to(tl.int32) - 8).to(tl.bfloat16) + hi = (((wb >> 4) & 0x0F).to(tl.int32) - 8).to(tl.bfloat16) + + # scale [BLOCK_N, BLOCK_KP]: group index = (2*kp) // group_size = kp // GROUP_BYTES + sg = kp // GROUP_BYTES + sc = tl.load( + scale_ptr + offs_n[:, None] * stride_sn + sg[None, :] * stride_sk, + mask=n_mask_w & kp_mask[None, :], other=0.0, + ) + w_lo = (lo * sc) # [BLOCK_N, BLOCK_KP] bf16 + w_hi = (hi * sc) + + acc += tl.dot(x_even, tl.trans(w_lo), out_dtype=tl.float32) + acc += tl.dot(x_odd, tl.trans(w_hi), out_dtype=tl.float32) + + out_ptrs = out_ptr + offs_m[:, None] * stride_om + offs_n[None, :] * stride_on + tl.store(out_ptrs, acc.to(tl.bfloat16), mask=m_mask & n_mask_out) + + +def int4_grouped_gemm( + x: torch.Tensor, # [M, K] BF16 + w_packed: torch.Tensor, # [N, K // 2] uint8 (INT4 packed, output-major) + scales: torch.Tensor, # [N, K // group_size] BF16 + group_size: int = 32, +) -> torch.Tensor: + """Single-expert INT4×BF16 GEMM with per-group dequant. Returns [M, N] BF16. + + Computes `out = x @ dequant(w_packed, scales).T` where + `dequant[n, k] = ((nibble(n,k) - 8) * scales[n, k // group_size])`. + """ + assert x.dim() == 2, f"x must be 2D, got {x.shape}" + M, K = x.shape + if w_packed.dtype == torch.int32: + w_packed = w_packed.view(torch.uint8) + assert w_packed.dtype == torch.uint8, f"w_packed must be uint8, got {w_packed.dtype}" + N, Kp = w_packed.shape + assert Kp == K // 2, f"w_packed K mismatch: x K={K} -> expected {K // 2}, got {Kp}" + assert K % group_size == 0, f"K={K} not divisible by group_size={group_size}" + + x = x.contiguous() + w_packed = w_packed.contiguous() + scales = scales.contiguous() + + out = torch.empty((M, N), dtype=torch.bfloat16, device=x.device) + grid = lambda meta: ( + triton.cdiv(M, meta['BLOCK_M']), + triton.cdiv(N, meta['BLOCK_N']), + ) + _int4_gemm_kernel[grid]( + x, w_packed, scales, out, + M, K, N, + x.stride(0), x.stride(1), + w_packed.stride(0), w_packed.stride(1), + scales.stride(0), scales.stride(1), + out.stride(0), out.stride(1), + GROUP_SIZE=group_size, + ) + return out + + +def int4_moe_grouped_gemm( + x: torch.Tensor, # [total_M, K] BF16 (sorted by expert) + w_packed_list, # list[Tensor [N, K//2] uint8] per expert + scales_list, # list[Tensor [N, K//group_size] bf16] per expert + m_offsets, # [num_experts + 1] int (row offsets into x) + group_size: int = 32, +) -> torch.Tensor: + """Batched INT4 MoE GEMM across experts (decode: small M per expert). + + Tokens are assumed pre-sorted by expert; `m_offsets[e]:m_offsets[e+1]` is the + contiguous row range for expert `e`. Returns [total_M, N] BF16. + """ + total_M, K = x.shape + N = w_packed_list[0].shape[0] + out = torch.empty((total_M, N), dtype=torch.bfloat16, device=x.device) + num_experts = len(w_packed_list) + offs = m_offsets.tolist() if torch.is_tensor(m_offsets) else list(m_offsets) + for e in range(num_experts): + lo, hi = offs[e], offs[e + 1] + if hi <= lo: + continue + out[lo:hi] = int4_grouped_gemm( + x[lo:hi], w_packed_list[e], scales_list[e], group_size=group_size, + ) + return out