Skip to content
KernelIndex
Search⌘K

submission 723187

Hamza · python · License unknown

Use it

Vendorable · source mirrored · license unknownView source →

No package. Vendor the mirrored source: 660 lines, June 9 Researcher Reciprocity License v1.0.

submission.py
curl "https://kernelindex.com/api/v1/implementations/kernelbot-amd-mixed-mla-723187?include=source"
interfacepython
Compatibility
measured onAMD Instinct MI355X
declared hardwareAMD Instinct MI355X
architecturesgfx950
dtypesbf16, int32

Benchmark evidence

1 measurement across 1 GPU, fastest first.

Operation / workload
Hardware
Latency
Rank
Observed
AMD Instinct MI355X
32.8µs
#36 of 766
2026-04-04

Reported · How evidence levels are derived →

Source and license

sourceavailable
revision digestsha256:8df5d765ee2875c22367f98470cc5c0bcb6b89ac8f7038203dca2e2ccd4a2711
license declaredunknown
license concludedunknown
authorsHamza
imported2026-08-15

Techniques

Extracted from the mirrored source by pattern, never inferred. Each row cites its line.

fp4"mxfp4": (Tensor, Tensor) kv_buffer fp4x2 + fp8_e8m0 — block-32 quantized
mmaqk = tl.dot(q_ne, tl.trans(kv_ne), out_dtype=tl.float32)
online-softmaxm_new = tl.maximum(m_i, m_ij)
persistent-kernelDecode only — persistent mode with get_mla_metadata_v1.
tile-n = 64BLOCK_N=64,

Kernel source

submission.py660 lines
#!POPCORN leaderboard amd-mixed-mla
#!POPCORN gpu MI355X

# gpumode leaderboard reference
"""
Reference implementation for MLA (Multi-head Latent Attention) decode kernel.

Uses aiter MLA kernels (mla_decode_fwd) as the reference.
DeepSeek R1 forward_absorb MLA: absorbed q (576), compressed kv_buffer (576),
output v_head_dim = kv_lora_rank = 512.

The input provides:
  q:       (total_q, 16, 576) bfloat16 — absorbed query
  kv_data: dict with KV cache in three formats:
    "bf16":  Tensor  (total_kv, 1, 576)  bfloat16          — highest precision
    "fp8":   (Tensor, Tensor)  kv_buffer fp8 + scalar scale — per-tensor quantized
    "mxfp4": (Tensor, Tensor)  kv_buffer fp4x2 + fp8_e8m0  — block-32 quantized
  The reference quantizes Q to fp8 on-the-fly inside ref_kernel.

The reference kernel quantizes Q to fp8 on-the-fly and uses fp8 KV (a8w8 kernel),
which is ~2-3x faster than bf16 on MI355X with negligible accuracy loss.

Decode only — persistent mode with get_mla_metadata_v1.
"""

import os as _os
_os.environ.setdefault("HIP_FORCE_DEV_KERNARG", "1")

import gc as _gc
import sys as _sys

_gc.disable()  # prevent GC pauses during benchmark tight loops
_sys.setswitchinterval(1.0)  # reduce GIL check frequency (single-threaded benchmark)

import torch
import torch.nn.functional as F
from task import input_t, output_t
from utils import make_match_reference

from aiter.mla import mla_decode_fwd
from aiter import dtypes as aiter_dtypes
from aiter import get_mla_metadata_info_v1, get_mla_metadata_v1
from aiter.utility.fp4_utils import (
    dynamic_mxfp4_quant,
    mxfp4_to_f32,
    e8m0_to_f32,
)
import triton
import triton.language as tl

torch.set_grad_enabled(False)  # once at import, replaces per-call @inference_mode()

# ---------------------------------------------------------------------------
# DeepSeek R1 latent MQA constants (forward_absorb path)
# https://huggingface.co/deepseek-ai/DeepSeek-R1-0528/blob/main/config.json
# ---------------------------------------------------------------------------
NUM_HEADS = 16
NUM_KV_HEADS = 1
KV_LORA_RANK = 512
QK_ROPE_HEAD_DIM = 64
QK_HEAD_DIM = KV_LORA_RANK + QK_ROPE_HEAD_DIM   # 576
V_HEAD_DIM = KV_LORA_RANK                        # 512
SM_SCALE = 1.0 / (QK_HEAD_DIM ** 0.5)

PAGE_SIZE = 1
NUM_KV_SPLITS = 32  # default for all known shapes

# FP8 dtype (platform-specific via aiter)
FP8_DTYPE = aiter_dtypes.fp8

# KV cache dtype for the reference kernel: "fp8" or "bf16"
KV_DTYPE = "fp8"

# Known benchmark shapes (batch_size, kv_seq_len) — same on public & ranked
_PUBLIC_SHAPES = [
    (4, 1024), (4, 8192),
    (32, 1024), (32, 8192),
    (64, 1024), (64, 8192),
    (256, 1024), (256, 8192),
]

# Per-shape config: (page_size, num_kv_splits, use_np, kv_granularity, intra_batch)
# Step 2 hybrid: NP pg2 for b32/k1024 only (-3.6µs ranked), all else v10c baseline
# NP b64/b256 kv=1024 FAILS ranked (kernel bug: 33k+ mismatched elements)
_SHAPE_CONFIG = {
    (4, 1024):   (1, 8, False, 128, False),
    (4, 8192):   (8, 32, False, 128, False),
    (32, 1024):  (2, 1, True, 128, False),
    (32, 8192):  (8, 32, False, 32, True),
    (64, 1024):  (2, 8, False, 128, False),
    (64, 8192):  (8, 32, False, 32, False),
    (256, 1024): (2, 8, False, 32, False),
    (256, 8192): (8, 32, False, 32, False),
}

# ---------------------------------------------------------------------------
# FP8 quantization (fallback for reference/tests)
# ---------------------------------------------------------------------------
_FP8_FINFO = torch.finfo(FP8_DTYPE)
_FP8_MAX = _FP8_FINFO.max


def quantize_fp8(tensor: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
    """Dynamic per-tensor FP8 quantization. Used by reference kernel only."""
    amax = tensor.abs().amax()
    fp8_tensor = (tensor * (_FP8_MAX / amax)).to(FP8_DTYPE)
    scale = (amax / _FP8_MAX).to(torch.float32).reshape(1)
    return fp8_tensor, scale


# ---------------------------------------------------------------------------
# MXFP4 quantization (aiter native: block-32, fp4x2 + fp8_e8m0 dtypes)
# Uses aiter.utility.fp4_utils.dynamic_mxfp4_quant
# ---------------------------------------------------------------------------

def quantize_mxfp4(tensor: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
    """
    MXFP4 block-wise quantization using aiter's dynamic_mxfp4_quant.

    Block size = 32. Each block gets an E8M0 scale factor.
    Two FP4 E2M1 values are packed per byte.

    Args:
        tensor: bf16 tensor of shape [B, M, N] (N must be divisible by 32)

    Returns:
        (fp4_data, scale_e8m0)
        - fp4_data:   shape [B, M, N//2] in aiter_dtypes.fp4x2
        - scale_e8m0: shape [B*M, ceil(N/32)] padded, in aiter_dtypes.fp8_e8m0
    """
    orig_shape = tensor.shape  # (B, M, N)
    B, M, N = orig_shape

    # dynamic_mxfp4_quant expects 2D: (B*M, N)
    tensor_2d = tensor.reshape(B * M, N)
    fp4_data_2d, scale_e8m0 = dynamic_mxfp4_quant(tensor_2d)

    # Reshape fp4_data back to 3D: (B, M, N//2)
    fp4_data = fp4_data_2d.view(B, M, N // 2)

    return fp4_data, scale_e8m0


def dequantize_mxfp4(
    fp4_data: torch.Tensor,
    scale_e8m0: torch.Tensor,
    orig_shape: tuple,
    dtype: torch.dtype = torch.bfloat16,
) -> torch.Tensor:
    """
    Dequantize MXFP4 tensor using aiter utilities.

    Note: dynamic_mxfp4_quant may pad both row and block dimensions in scale_e8m0.
    We trim scales to match the actual data dimensions.

    Args:
        fp4_data:   packed FP4 data, shape [B, M, N//2] in fp4x2 or uint8
        scale_e8m0: E8M0 block scale factors (possibly padded) in fp8_e8m0
        orig_shape: original (B, M, N) for reshaping
        dtype:      output dtype

    Returns:
        Dequantized tensor of shape orig_shape.
    """
    B, M, N = orig_shape
    num_rows = B * M
    block_size = 32
    num_blocks = N // block_size  # actual blocks needed (e.g. 576/32 = 18)

    # Unpack FP4 to float32: mxfp4_to_f32 expects (..., N//2) -> (..., N)
    fp4_data_2d = fp4_data.reshape(num_rows, N // 2)
    float_vals = mxfp4_to_f32(fp4_data_2d)  # (num_rows, N)

    # Convert E8M0 scales to float32 and trim padded dimensions
    scale_f32 = e8m0_to_f32(scale_e8m0)  # (padded_rows, padded_blocks)
    scale_f32 = scale_f32[:num_rows, :num_blocks]  # (num_rows, num_blocks)

    # Apply block scales
    float_vals_blocked = float_vals.view(num_rows, num_blocks, block_size)
    scaled = float_vals_blocked * scale_f32.unsqueeze(-1)

    return scaled.view(B, M, N).to(dtype)


# ---------------------------------------------------------------------------
# Triton MXFP4 MLA decode kernel — reads ~53% less HBM than fp8
# For bandwidth-dominated large shapes (b256/kv8192, b64/kv8192)
# ---------------------------------------------------------------------------
_MXFP4_SHAPES = set()  # disabled: manual dequant Triton is 16x slower than ASM fp8
_MXFP4_LAUNCHERS = None


@triton.jit
def _e2m1_dequant(nibble_u32, scale_u32):
    """Dequant E2M1 4-bit nibble with E8M0 scale to float32.

    nibble_u32: uint32 tensor, lower 4 bits = E2M1 value (S1 E2 M1)
    scale_u32:  uint32 tensor, 8-bit E8M0 exponent
    Returns:    float32 dequantized value = fp4_val * 2^(scale - 127)
    """
    sign = nibble_u32 >> 3
    abs_val = nibble_u32 & 7
    exp = abs_val >> 1
    mant = abs_val & 1

    # Normal (exp>0): fp32 = (exp+126)<<23 | mant<<22  →  2^(exp-1)*(1+0.5*mant)
    # Denorm (exp=0): fp32 = mant * 0x3F000000          →  0.0 or 0.5
    normal_bits = ((exp + 126) << 23) | (mant << 22)
    denorm_bits = mant * 0x3F000000
    base_val = tl.where(exp > 0, normal_bits, denorm_bits).to(tl.float32, bitcast=True)

    # E8M0 → float: 2^(byte-127) = bitcast(byte << 23)
    scale_val = (scale_u32 << 23).to(tl.float32, bitcast=True)

    val = base_val * scale_val
    val = tl.where(sign != 0, -val, val)
    return val


@triton.jit
def _mla_decode_mxfp4_kernel(
    Q, KV_FP4, KV_SCALE, Output,
    kv_indptr, kv_indices,
    sm_scale,
    stride_qb, stride_qh,
    stride_kvn, stride_ksn,
    stride_ob, stride_oh,
    BLOCK_N: tl.constexpr,
):
    """Single-pass MLA decode reading MXFP4 KV directly from HBM.

    Grid: (batch_size,)  — one program per batch item, all 16 heads inside.
    QK split into even/odd dims matching fp4x2 nibble packing (lo=even, hi=odd).
    """
    batch_id = tl.program_id(0)
    offs_h = tl.arange(0, 16)

    # ---- Load Q (bf16) split into even/odd for nope and rope ----
    q_base = Q + batch_id * stride_qb
    offs_ne = tl.arange(0, 256) * 2            # nope even: 0,2,..,510
    offs_no = tl.arange(0, 256) * 2 + 1        # nope odd:  1,3,..,511
    offs_re = 512 + tl.arange(0, 32) * 2       # rope even: 512,514,..,574
    offs_ro = 512 + tl.arange(0, 32) * 2 + 1   # rope odd:  513,515,..,575

    q_ne = tl.load(q_base + offs_h[:, None] * stride_qh + offs_ne[None, :])  # [16,256]
    q_no = tl.load(q_base + offs_h[:, None] * stride_qh + offs_no[None, :])  # [16,256]
    q_re = tl.load(q_base + offs_h[:, None] * stride_qh + offs_re[None, :])  # [16,32]
    q_ro = tl.load(q_base + offs_h[:, None] * stride_qh + offs_ro[None, :])  # [16,32]

    # ---- KV range ----
    kv_start = tl.load(kv_indptr + batch_id)
    kv_end = tl.load(kv_indptr + batch_id + 1)
    kv_len = kv_end - kv_start

    # ---- Online softmax state ----
    m_i = tl.full([16], float("-inf"), dtype=tl.float32)
    l_i = tl.zeros([16], dtype=tl.float32)
    acc_e = tl.zeros([16, 256], dtype=tl.float32)  # even-dim accumulator
    acc_o = tl.zeros([16, 256], dtype=tl.float32)  # odd-dim accumulator

    # Scale-block index tables (compile-time)
    nope_p = tl.arange(0, 256)
    nope_si = nope_p // 16                       # 0..15, each ×16
    rope_p = tl.arange(0, 32)
    rope_si = rope_p // 16 + 16                  # 16 or 17

    for start_n in range(0, kv_len, BLOCK_N):
        offs_n = start_n + tl.arange(0, BLOCK_N)
        mask_n = offs_n < kv_len
        kv_loc = tl.load(kv_indices + kv_start + offs_n, mask=mask_n, other=0)

        # ---- Dequant nope (512 dims → 256 packed bytes) ----
        npk = tl.load(KV_FP4 + kv_loc[:, None] * stride_kvn + nope_p[None, :],
                       mask=mask_n[:, None], other=0).to(tl.uint32)
        nsc = tl.load(KV_SCALE + kv_loc[:, None] * stride_ksn + nope_si[None, :],
                       mask=mask_n[:, None], other=127).to(tl.uint32)
        kv_ne = _e2m1_dequant(npk & 0xF, nsc).to(tl.bfloat16)          # [BN,256]
        kv_no = _e2m1_dequant((npk >> 4) & 0xF, nsc).to(tl.bfloat16)   # [BN,256]

        # ---- Dequant rope (64 dims → 32 packed bytes) ----
        rpk = tl.load(KV_FP4 + kv_loc[:, None] * stride_kvn + (256 + rope_p)[None, :],
                       mask=mask_n[:, None], other=0).to(tl.uint32)
        rsc = tl.load(KV_SCALE + kv_loc[:, None] * stride_ksn + rope_si[None, :],
                       mask=mask_n[:, None], other=127).to(tl.uint32)
        kv_re = _e2m1_dequant(rpk & 0xF, rsc).to(tl.bfloat16)          # [BN,32]
        kv_ro = _e2m1_dequant((rpk >> 4) & 0xF, rsc).to(tl.bfloat16)   # [BN,32]

        # ---- QK = sum of 4 partial dots ----
        qk = tl.dot(q_ne, tl.trans(kv_ne), out_dtype=tl.float32)
        qk += tl.dot(q_no, tl.trans(kv_no), out_dtype=tl.float32)
        qk += tl.dot(q_re, tl.trans(kv_re), out_dtype=tl.float32)
        qk += tl.dot(q_ro, tl.trans(kv_ro), out_dtype=tl.float32)
        qk *= sm_scale
        qk = tl.where(mask_n[None, :], qk, float("-inf"))

        # ---- Online softmax ----
        m_ij = tl.max(qk, 1)
        m_new = tl.maximum(m_i, m_ij)
        alpha = tl.exp(m_i - m_new)
        p = tl.exp(qk - m_new[:, None])
        l_i = l_i * alpha + tl.sum(p, 1)
        acc_e *= alpha[:, None]
        acc_o *= alpha[:, None]
        m_i = m_new

        # ---- PV (nope dims only, 512→ even+odd halves) ----
        p_bf = p.to(tl.bfloat16)
        acc_e += tl.dot(p_bf, kv_ne, out_dtype=tl.float32)
        acc_o += tl.dot(p_bf, kv_no, out_dtype=tl.float32)

    # ---- Normalize and store (interleaved even/odd) ----
    inv_l = (1.0 / l_i)[:, None]
    acc_e *= inv_l
    acc_o *= inv_l

    o_base = Output + batch_id * stride_ob
    oe = tl.arange(0, 256) * 2
    oo = tl.arange(0, 256) * 2 + 1
    tl.store(o_base + offs_h[:, None] * stride_oh + oe[None, :], acc_e.to(tl.bfloat16))
    tl.store(o_base + offs_h[:, None] * stride_oh + oo[None, :], acc_o.to(tl.bfloat16))


def _build_mxfp4_launcher(batch_size, kv_seq_len, device):
    """Build a closure that launches the Triton MXFP4 decode kernel."""
    total_kv = batch_size * kv_seq_len
    output = torch.empty(batch_size, NUM_HEADS, V_HEAD_DIM,
                         dtype=torch.bfloat16, device=device)
    kv_indptr_t = torch.arange(0, (batch_size + 1) * kv_seq_len, kv_seq_len,
                               dtype=torch.int32, device=device)
    kv_indices_t = torch.arange(total_kv, dtype=torch.int32, device=device)
    sm = SM_SCALE
    grid = (batch_size,)

    def launch(q, mxfp4_tuple):
        kv_fp4, kv_scale_raw = mxfp4_tuple
        kv_fp4_2d = kv_fp4.view(torch.uint8).reshape(-1, kv_fp4.shape[-1])
        kv_sc_2d = kv_scale_raw.view(torch.uint8)
        if kv_sc_2d.dim() == 1:
            kv_sc_2d = kv_sc_2d.reshape(-1, 18)

        _mla_decode_mxfp4_kernel[grid](
            q, kv_fp4_2d, kv_sc_2d, output,
            kv_indptr_t, kv_indices_t,
            sm,
            q.stride(0), q.stride(1),
            kv_fp4_2d.stride(0), kv_sc_2d.stride(0),
            output.stride(0), output.stride(1),
            BLOCK_N=64,
        )
        return output

    return launch


# ---------------------------------------------------------------------------
# Per-shape static launchers (Directions 1, 2, 5)
#
# At first custom_kernel call, we build one closure per known shape.
# Each closure captures ALL pre-allocated tensors:
#   metadata, index tensors, scratch buffers, output buffer, FP8 Q buffer.
# Hot path = 1 dict lookup + 1 closure call with 3 GPU kernels:
#   copy_(bf16→fp8) + stage1_asm + reduce
#
# Direction 3: copy_() reuses a pre-allocated buffer, avoiding Python object
# creation overhead of tensor.to(). non_blocking is default for same-device ops.
#
# Direction 4: a16w8 (bf16 Q, no quant) was tested — no persistent a16w8 ASM
# kernel for qSeqLen=1 exists, and a16w8 is 1.35x slower (see SESSION.md).
# Sticking with a8w8 + fixed-scale FP8 (q_scale=1.0).
# ---------------------------------------------------------------------------
_SHAPE_LAUNCHERS = None


def _build_launcher(batch_size, kv_seq_len, device, stage1_fn, reduce_fn):
    """
    Build a minimal closure for one (batch_size, kv_seq_len) shape.

    Two modes:
    1. Persistent (default): stage1 + reduce, pre-built metadata
    2. Non-persistent splits=1: stage1 writes directly to output, NO reduce.
       Only for large batches (b256) where batch provides CU parallelism.
    """
    total_q = batch_size  # q_seq_len = 1 for decode
    nq = NUM_HEADS
    nkv = NUM_KV_HEADS
    dq = QK_HEAD_DIM
    dv = V_HEAD_DIM
    pg, num_splits, use_np, kvg, intra = _SHAPE_CONFIG.get(
        (batch_size, kv_seq_len), (1, NUM_KV_SPLITS, False, 128, False)
    )

    # --- Pre-build all index tensors ---
    qo_indptr = torch.arange(0, batch_size + 1, dtype=torch.int32, device=device)
    pages_per_item = kv_seq_len // pg
    kv_indptr = torch.arange(
        0, (batch_size + 1) * pages_per_item, pages_per_item,
        dtype=torch.int32, device=device,
    )
    kv_indices = torch.arange(
        batch_size * pages_per_item, dtype=torch.int32, device=device,
    )
    kv_last = torch.full(
        (batch_size,), kv_seq_len, dtype=torch.int32, device=device,
    )

    # --- Pre-allocate output + FP8 Q buffer + fixed scale ---
    output = torch.empty(
        (total_q, nq, dv), dtype=torch.bfloat16, device=device,
    )
    q_fp8 = torch.empty(
        (total_q, nq, dq), dtype=FP8_DTYPE, device=device,
    )
    q_scale = torch.ones(1, dtype=torch.float32, device=device)

    # --- Closure constants ---
    sm = SM_SCALE
    kv_4d_shape = (-1, pg, nkv, dq)
    _copy_q = q_fp8.copy_  # pre-bind to skip attribute lookup per call
    _kv_cache = [None, None]  # [kv_raw, kv_viewed] — identity cache for view()

    # ===================================================================
    # Non-persistent splits=1: stage1 writes directly to output, skip reduce
    # From aiter mla.py: when splits=1 and fp8 Q, logits = output.view(...)
    # so stage1 writes bf16 result directly to output buffer.
    # ===================================================================
    if use_np and stage1_fn is not None:
        # Non-persistent: num_kv_splits_indptr is a real tensor, metadata is None
        kv_splits_indptr = torch.arange(
            0, batch_size + 1, dtype=torch.int32, device=device,
        )
        # logits = output viewed as (batch, 1, nhead, dv) — shares memory
        logits_alias = output.view(total_q, 1, nq, dv)
        np_attn_lse = torch.empty(
            (total_q, 1, nq, 1), dtype=torch.float32, device=device,
        )

        def launch(q_raw, kv_raw, kv_scale):
            _copy_q(q_raw)
            if _kv_cache[0] is not kv_raw:
                _kv_cache[0] = kv_raw
                _kv_cache[1] = kv_raw.view(kv_4d_shape)
            stage1_fn(
                q_fp8, _kv_cache[1],
                qo_indptr, kv_indptr, kv_indices, kv_last,
                kv_splits_indptr, None, None, None,
                1, pg, nkv, sm,
                logits_alias, np_attn_lse, output, q_scale, kv_scale,
            )
            return output  # result already written by stage1

        return launch

    # ===================================================================
    # Persistent mode: stage1 + reduce with pre-built metadata
    # ===================================================================

    # --- Pre-build persistent-mode metadata ---
    q_dtype = FP8_DTYPE
    kv_dtype = FP8_DTYPE
    info = get_mla_metadata_info_v1(
        batch_size, 1, nq, q_dtype, kv_dtype,
        is_sparse=False, fast_mode=False,
        num_kv_splits=num_splits, intra_batch_mode=intra,
    )
    w_meta, w_indptr, w_info, r_indptr, r_final, r_partial = [
        torch.empty(s, dtype=t, device=device) for s, t in info
    ]
    get_mla_metadata_v1(
        qo_indptr, kv_indptr, kv_last,
        nq // nkv, nkv, True,
        w_meta, w_info, w_indptr, r_indptr, r_final, r_partial,
        page_size=pg,
        kv_granularity=kvg,
        max_seqlen_qo=1,
        uni_seqlen_qo=1,
        fast_mode=False,
        max_split_per_batch=num_splits,
        intra_batch_mode=intra,
        dtype_q=q_dtype,
        dtype_kv=kv_dtype,
    )

    # --- Pre-allocate scratch for stage1 ---
    partial_count = int(r_partial.size(0))
    logits = torch.empty(
        (partial_count, 1, nq, dv), dtype=torch.float32, device=device,
    )
    attn_lse = torch.empty(
        (partial_count, 1, nq, 1), dtype=torch.float32, device=device,
    )

    if stage1_fn is not None and reduce_fn is not None:
        # Direct stage1 + reduce: minimal Python, maximum speed
        # 19-arg stage1 signature (runner has no lse param)
        def launch(q_raw, kv_raw, kv_scale):
            _copy_q(q_raw)
            if _kv_cache[0] is not kv_raw:
                _kv_cache[0] = kv_raw
                _kv_cache[1] = kv_raw.view(kv_4d_shape)
            stage1_fn(
                q_fp8, _kv_cache[1],
                qo_indptr, kv_indptr, kv_indices, kv_last,
                None, w_meta, w_indptr, w_info,
                1, pg, nkv, sm,
                logits, attn_lse, output, q_scale, kv_scale,
            )
            reduce_fn(
                logits, attn_lse,
                r_indptr, r_final, r_partial,
                1, output, None,
            )
            return output
    else:
        # Fallback: use mla_decode_fwd wrapper if direct ops unavailable
        meta = {
            "work_meta_data": w_meta,
            "work_indptr": w_indptr,
            "work_info_set": w_info,
            "reduce_indptr": r_indptr,
            "reduce_final_map": r_final,
            "reduce_partial_map": r_partial,
        }

        def launch(q_raw, kv_raw, kv_scale):
            _copy_q(q_raw)
            if _kv_cache[0] is not kv_raw:
                _kv_cache[0] = kv_raw
                _kv_cache[1] = kv_raw.view(kv_4d_shape)
            mla_decode_fwd(
                q_fp8, _kv_cache[1], output,
                qo_indptr, kv_indptr, kv_indices, kv_last,
                1,
                page_size=pg,
                nhead_kv=nkv,
                sm_scale=sm,
                logit_cap=0.0,
                num_kv_splits=num_splits,
                q_scale=q_scale,
                kv_scale=kv_scale,
                intra_batch_mode=intra,
                **meta,
            )
            return output

    return launch


def _init_launchers(device):
    """
    Direction 2: Pre-warm per-shape closures for all 8 known shapes at once.
    Called once on first custom_kernel invocation. Populates _SHAPE_LAUNCHERS
    so every subsequent call hits a pre-built closure with zero cache misses.
    """
    global _SHAPE_LAUNCHERS

    # Resolve direct ASM ops once, use .default to skip OpOverloadPacket dispatch
    aiter_ns = getattr(torch.ops, "aiter", None)
    stage1_op = getattr(aiter_ns, "mla_decode_stage1_asm_fwd", None) if aiter_ns else None
    reduce_op = getattr(aiter_ns, "mla_reduce_v1", None) if aiter_ns else None
    stage1 = getattr(stage1_op, "default", stage1_op) if stage1_op else None
    reduce = getattr(reduce_op, "default", reduce_op) if reduce_op else None

    # Try direct pybind11 reduce access (bypasses torch.ops + Python wrapper chain ~500ns)
    try:
        from aiter.jit.core import get_module as _get_mod
        _reduce_mod = _get_mod("module_mla_reduce")
        _reduce_pb = getattr(_reduce_mod, "mla_reduce_v1", None)
        if _reduce_pb is not None:
            reduce = _reduce_pb
            print("[mla] direct pybind reduce OK", file=_sys.stderr)
    except Exception as e:
        print(f"[mla] direct pybind reduce failed: {e}", file=_sys.stderr)

    launchers = {}
    for bs, kvl in _PUBLIC_SHAPES:
        launchers[(bs, bs * kvl)] = _build_launcher(bs, kvl, device, stage1, reduce)

    _SHAPE_LAUNCHERS = launchers

    # Build MXFP4 Triton launchers for large bandwidth-bound shapes
    global _MXFP4_LAUNCHERS
    mxfp4_map = {}
    for shape in _MXFP4_SHAPES:
        mxfp4_map[shape] = _build_mxfp4_launcher(shape[0], shape[1], device)
    _MXFP4_LAUNCHERS = mxfp4_map

    print(
        f"[mla] {len(launchers)} fp8 + {len(mxfp4_map)} mxfp4 launchers, direct={stage1 is not None}",
        file=_sys.stderr,
    )


# ---------------------------------------------------------------------------
# Fallback for unexpected shapes (safety net — should not be needed)
# ---------------------------------------------------------------------------

def _fallback_decode(q, kv_fp8, kv_scale, qo_indptr, kv_indptr, config):
    """General decode path for shapes not in the pre-built launcher table."""
    batch_size = config["batch_size"]
    nq = config["num_heads"]
    nkv = config["num_kv_heads"]
    dq = config["qk_head_dim"]
    dv = config["v_head_dim"]
    q_seq_len = config["q_seq_len"]
    total_kv = kv_fp8.shape[0]
    kv_seq_len = config.get("kv_seq_len")
    device = q.device

    # Fixed-scale FP8 quant
    q_fp8 = q.to(FP8_DTYPE)
    q_scale = torch.ones(1, dtype=torch.float32, device=device)

    kv_4d = kv_fp8.view(total_kv, PAGE_SIZE, nkv, kv_fp8.shape[-1])
    kv_indices = torch.arange(total_kv, dtype=torch.int32, device=device)
    if kv_seq_len is not None:
        kv_last = torch.full(
            (batch_size,), kv_seq_len, dtype=torch.int32, device=device,
        )
    else:
        kv_last = (kv_indptr[1:] - kv_indptr[:-1]).to(torch.int32)

    o = torch.empty((q.shape[0], nq, dv), dtype=torch.bfloat16, device=device)
    mla_decode_fwd(
        q_fp8.view(-1, nq, dq), kv_4d, o,
        qo_indptr, kv_indptr, kv_indices, kv_last,
        q_seq_len,
        page_size=PAGE_SIZE,
        nhead_kv=nkv,
        sm_scale=SM_SCALE,
        logit_cap=0.0,
        num_kv_splits=NUM_KV_SPLITS,
        q_scale=q_scale,
        kv_scale=kv_scale,
        intra_batch_mode=True,
    )
    return o


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------

def custom_kernel(data: input_t) -> output_t:
    global _SHAPE_LAUNCHERS

    q, kv_data, qo_indptr, kv_indptr, config = data

    if _SHAPE_LAUNCHERS is None:
        _init_launchers(q.device)

    # FP8 ASM path — key is (batch_size, total_kv) to avoid dict/div overhead
    kv_fp8, kv_scale = kv_data["fp8"]

    launcher = _SHAPE_LAUNCHERS.get((q.shape[0], kv_fp8.shape[0]))
    if launcher is not None:
        return launcher(q, kv_fp8, kv_scale)

    # Unexpected shape — use general fallback
    return _fallback_decode(q, kv_fp8, kv_scale, qo_indptr, kv_indptr, config)
scrolls · 660 lines total

Source code from GPU Mode and the KernelBot dataset · June 9 Researcher Reciprocity License v1.0

Best evidence level for this revision: reported

JSON