Files
TheRock/rocm-libraries/dnn-providers/hip-kernel-provider/rocke/library/kernels/common/fmha_mfma.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

328 lines
13 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Tiled FMHA forward (production attention kernel; unified MMA body).
This is the **first attention kernel** consuming the production
``16x16x16`` f16 contract atom directly. One CTA = one wave handles a
16-row Q tile across the full K span via the QK + softmax + PV matmul
chain. The single body is arch-polymorphic: the ``16x16x16`` atom
resolves to ``mfma_f32_16x16x16_f16`` on CDNA wave64 (gfx942/gfx950)
and ``wmma_f32_16x16x16_f16`` on RDNA wave32 (gfx1151).
The module / symbol keep the historical ``_mfma`` suffix for caller
stability (it predates the MFMA/WMMA unification), but
:func:`build_fmha_fwd_mfma` emits MFMA *or* WMMA depending on ``arch``;
it is not CDNA-only. The helper :func:`mfma_attention_fwd_inner_body`
factors the QK / softmax / PV pipeline and dispatches on the target
wave size; this module's :func:`build_fmha_fwd_mfma` is the thin
spec→kernel wrapper.
Grid layout: ``(seqlen_q // BLOCK_M, num_query_heads, batch)``. The
spec's ``seqlen_q`` must be a multiple of ``BLOCK_M = 16``;
``head_size`` must be a multiple of ``16`` (all standard FMHA head
sizes -- 64 / 128 / 256 -- qualify); ``seqlen_k`` must be a multiple
of ``BLOCK_K = 16``.
CK Tile parity context: the standard CK Tile ``01_fmha`` fwd kernel
uses the same atom shape + LDS-staging skeleton; v1 here ships the
single-warp variant (no multi-warp BLOCK_M, no async DMA), which is
already ~16x denser in FLOPs than the warp-scalar body and forms
the spec surface the multi-warp + cshuffle hoist consumes verbatim.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Tuple
from rocke.core.ir import KernelDef
from rocke.helpers.mfma_attention import (
MFMA_ATTN_BLOCK_K,
MFMA_ATTN_BLOCK_M,
mfma_attention_fwd_inner_body,
)
from rocke.helpers.spec import kernel_name_join
from ._fmha_common import FmhaCommonSpec, FmhaKernelBuilder, validate_common_spec
__all__ = [
"FmhaMfmaSpec",
"build_fmha_fwd_mfma",
"fmha_fwd_mfma_grid",
"fmha_fwd_mfma_signature",
"is_valid_spec",
]
@dataclass(frozen=True)
class FmhaMfmaSpec:
"""One MFMA-tiled FMHA forward configuration.
``seqlen_q`` and ``seqlen_k`` are compile-time so the spec can
pre-size the launch grid; runtime variability (e.g. varlen) lifts
via a derived spec that overrides the grid computation.
"""
common: FmhaCommonSpec
seqlen_q: int
seqlen_k: int
name: str = "rocke_fmha_fwd_mfma"
def kernel_name(self) -> str:
s = self.common.shape
return kernel_name_join(
self.name,
f"H{s.head_size}",
f"HQ{s.num_query_heads}",
f"HK{s.num_kv_heads}",
self.common.dtype,
f"Q{self.seqlen_q}",
f"K{self.seqlen_k}",
self.common.mask_mode,
)
def _mma_family(arch: str) -> str:
"""MMA atom family for ``arch``: ``"wmma"`` on the RDNA wave32 targets
(gfx11xx), ``"mma"`` (MFMA) on CDNA.
The single body in :func:`build_fmha_fwd_mfma` selects the QK/PV atom from
the target catalog using this family, so the same spec resolves to an MFMA
atom on gfx942/gfx950 and a WMMA atom on gfx1151 -- the attention analogue
of the unified GEMM's family selection.
"""
from rocke.core.arch import ArchTarget
return "wmma" if ArchTarget.from_gfx(arch).wave_size == 32 else "mma"
def is_valid_spec(spec: FmhaMfmaSpec, arch: str = "gfx950") -> Tuple[bool, str]:
"""Return ``(ok, reason)`` for one tiled FMHA fwd config on ``arch``.
The body runs the canonical small-tile ``16x16x16`` f16 atom
(BLOCK_M = BLOCK_K = 16). On CDNA (gfx942 / gfx950) that is the
``mfma_f32_16x16x16_f16`` MFMA atom; on the RDNA wave32 targets
(gfx1151) it is the ``wmma_f32_16x16x16_f16`` WMMA atom. The same
spec is thus arch-polymorphic across both families -- the matmul,
online-softmax reduction, and P fragment re-layout are driven off the
contract's per-arch ``MmaOp`` (see
:func:`rocke.helpers.mfma_attention.mfma_attention_fwd_inner_body`).
The architecture facts (legal atom for the f16 ⊗ f16 → f32 combo,
per-WG LDS capacity, wave size) are sourced from
:class:`rocke.core.arch.ArchTarget` so an unknown arch -- or an arch
that ever drops the 16x16x16 atom -- is rejected with a structured
reason rather than crashing comgr at lower time. gfx950 / gfx942
behaviour and atom selection are unchanged.
"""
from rocke.core.arch import ArchTarget
try:
target = ArchTarget.from_gfx(arch)
except KeyError as e:
return False, str(e)
family = _mma_family(arch)
ok, why = validate_common_spec(spec.common)
if not ok:
return False, why
s = spec.common.shape
if s.head_size % 16 != 0:
return False, (f"tiled FMHA needs head_size % 16 == 0 (got {s.head_size})")
if spec.seqlen_q % MFMA_ATTN_BLOCK_M != 0:
return False, (
f"seqlen_q ({spec.seqlen_q}) must be a multiple of BLOCK_M "
f"({MFMA_ATTN_BLOCK_M})"
)
if spec.seqlen_k % MFMA_ATTN_BLOCK_K != 0:
return False, (
f"seqlen_k ({spec.seqlen_k}) must be a multiple of BLOCK_K "
f"({MFMA_ATTN_BLOCK_K})"
)
if spec.common.dtype != "f16":
return False, (
f"tiled FMHA v1 ships f16 only; bf16 lands once the bf16 "
f"atom factory is exposed (got {spec.common.dtype})"
)
# The QK / PV chain is the f16 16x16x16 atom; require it on the
# target catalog (MFMA on CDNA, WMMA on RDNA) so an arch missing the
# atom is rejected cleanly.
if not target.supports_dtype_combo("f16", "f16", "fp32", family=family):
return False, f"unsupported f16 {family} dtype combo on {arch}"
if not target.mma.has_shape(
family=family,
a_dtype="f16",
b_dtype="f16",
c_dtype="fp32",
m=MFMA_ATTN_BLOCK_M,
n=MFMA_ATTN_BLOCK_M,
k=MFMA_ATTN_BLOCK_K,
):
return False, (
f"unsupported f16 {family} warp_tile "
f"({MFMA_ATTN_BLOCK_M},{MFMA_ATTN_BLOCK_M},{MFMA_ATTN_BLOCK_K}) "
f"on {arch}"
)
# LDS budget: one BLOCK_M x BLOCK_K f16 P-staging buffer.
bytes_lds = MFMA_ATTN_BLOCK_M * MFMA_ATTN_BLOCK_K * 2
if not target.fits_lds(bytes_lds):
return False, (
f"LDS budget {bytes_lds} > {target.lds_capacity_bytes} cap on {arch}"
)
return True, "ok"
def _declare_params(kb: FmhaKernelBuilder) -> None:
"""The MFMA FMHA kernel ABI (shared between build + sig)."""
kb.add_tensor("Q", readonly=True)
kb.add_tensor("K", readonly=True)
kb.add_tensor("V", readonly=True)
kb.add_tensor("O", readonly=False, writeonly=True)
kb.add_scalar("scale_log2", "f32")
kb.add_scalar("seqlen_q", "i32")
kb.add_scalar("seqlen_k", "i32")
kb.add_strides("q", "k", "v", "o")
def build_fmha_fwd_mfma(spec: FmhaMfmaSpec, arch: str = "gfx950") -> KernelDef:
"""Tiled FMHA forward kernel (one unified body, MFMA on CDNA / WMMA on RDNA).
Grid: ``(seqlen_q / BLOCK_M, num_query_heads, batch)``. Each CTA owns one
``(q_tile, head, batch)`` triple and one wave (wave64 on CDNA, wave32 on
RDNA).
``arch`` selects the QK/PV matmul atom from the contract catalog: the f16
``16x16x16`` atom resolves to ``mfma_f32_16x16x16_f16`` on gfx942/gfx950 and
``wmma_f32_16x16x16_f16`` on gfx1151. The single common inner body
(:func:`rocke.helpers.mfma_attention.mfma_attention_fwd_inner_body`)
dispatches on the target wave size and drives the matmul, online-softmax
reduction, and P fragment re-layout off that op's contract layout maps. On
the default ``arch="gfx950"`` the emitted IR is byte-for-byte unchanged from
the pre-unification MFMA kernel; ``arch="gfx1151"`` produces the WMMA
variant. An arch-illegal config is rejected by :func:`is_valid_spec` before
any IR is emitted.
Per-batch causal masking (G4): the masked query position is the
*within-batch* row (``q_tile_local``), passed to the inner body as
``q_pos_base``, while the global-batched row (``q_tile_local +
batch_idx * seqlen_q``) addresses Q / O in memory via ``q_tile_base``.
This makes the causal / sliding-window mask correct for ``batch > 1``
(each batch's row 0 is causal position 0, not ``batch_idx * seqlen_q``),
matching the gfx1151 adapter
:func:`kernels.gfx1151.wmma_fmha_fwd.build_wmma_fmha_fwd`.
``q_pos_base`` is only threaded for the masked modes
(``causal`` / ``sliding_window``); for ``mask_mode == "none"`` the
masked position is unused, so it is left unset and the emitted CDNA IR
is byte-for-byte unchanged from the pre-G4 kernel (the default
``q_pos_base = q_tile_base`` path). The masked-mode IR does change
(the within-batch position replaces the global-batched one in the mask
predicate) -- that is the bug fix; the masked path was numerically
wrong for ``batch > 1`` before.
"""
ok, why = is_valid_spec(spec, arch)
if not ok:
raise ValueError(f"invalid fmha_mfma spec: {why}")
s = spec.common
# One wave per CTA: wave64 on CDNA (MFMA), wave32 on RDNA (WMMA). The block
# size is sourced from the target so the launch geometry matches the atom
# family the body selects.
from rocke.core.arch import ArchTarget
wave_size = ArchTarget.from_gfx(arch).wave_size
kb = FmhaKernelBuilder(spec.kernel_name(), s)
kb.block_size(wave_size) # one wave per CTA (64 on CDNA, 32 on RDNA)
_declare_params(kb)
kb.decode_grid(has_batch_axis=True)
b = kb.builder
seqlen_q = kb.scalar("seqlen_q")
seqlen_k = kb.scalar("seqlen_k")
head_idx = kb.head_idx
kv_head_idx = kb.kv_head_idx
batch_idx = kb.batch_idx
# block_id_x is the Q-tile index; q_tile_local is its first Q row
# within the current batch.
q_tile_idx = kb.q_token # reuses block_id_x; semantically a tile, not a token
q_tile_local = b.mul(q_tile_idx, b.const_i32(MFMA_ATTN_BLOCK_M))
# Per-batch shift in rows (for Q and O). The helper multiplies
# the row index by stride_q_token / stride_o_token internally, so
# we only need to pass the row offset -- the previous duplicate
# ``q_batch_offset = batch_idx * seqlen_q * stride_q_token``
# multiply was dead and has been removed (the helper does that
# same multiplication once via ``q_row * stride_q_token``). For
# K / V we still need byte / element offsets because the helper
# treats ``k_token_offset_elems`` as an additive element offset
# rather than as a row index.
batch_row_q = b.mul(batch_idx, seqlen_q)
k_batch_offset = b.mul(b.mul(batch_idx, seqlen_k), kb.stride_token("k"))
v_batch_offset = b.mul(b.mul(batch_idx, seqlen_k), kb.stride_token("v"))
causal_ctx = b.const_i32(0) # self-attention: no cache offset
# G4: the masked query position is the within-batch row so causal /
# sliding-window masking is correct for ``batch > 1`` -- batch b's row 0
# is causal position 0, not ``b * seqlen_q``. Only thread it for the
# masked modes; for ``mask_mode == "none"`` the position is unused and
# leaving ``q_pos_base`` unset keeps the CDNA IR byte-identical (the
# default ``q_pos_base = q_tile_base`` path).
masked = s.mask_mode in ("causal", "sliding_window")
q_pos_base = q_tile_local if masked else None
mfma_attention_fwd_inner_body(
b,
Q=kb.tensor("Q"),
K=kb.tensor("K"),
V=kb.tensor("V"),
O=kb.tensor("O"),
head_size=s.head_size,
seqlen_k=seqlen_k,
# q_tile_base = local Q row + per-batch row shift. The helper
# does ``(q_tile_base + m_in_atom) * stride_q_token`` for both
# Q and O, which is the equivalent of folding
# ``batch_idx * seqlen_q * stride_{q,o}_token`` into the row
# index up front. Saves one multiply per axis vs the previous
# ``b.mul(b.mul(batch_idx, seqlen_q), stride_{q,o}_token)``
# which the helper then ignored.
q_tile_base=b.add(q_tile_local, batch_row_q),
q_pos_base=q_pos_base,
head_idx=head_idx,
kv_head_idx=kv_head_idx,
stride_q_token=kb.stride_token("q"),
stride_q_head=kb.stride_head("q"),
stride_k_token=kb.stride_token("k"),
stride_k_head=kb.stride_head("k"),
stride_v_token=kb.stride_token("v"),
stride_v_head=kb.stride_head("v"),
stride_o_token=kb.stride_token("o"),
stride_o_head=kb.stride_head("o"),
scale_log2=kb.scalar("scale_log2"),
dtype=s.dtype,
mask_mode=s.mask_mode,
sliding_window=s.sliding_window,
causal_ctx_offset=causal_ctx,
k_token_offset_elems=k_batch_offset,
v_token_offset_elems=v_batch_offset,
arch=arch,
)
b.ret()
return kb.kernel
def fmha_fwd_mfma_grid(spec: FmhaMfmaSpec, *, batch: int) -> Tuple[int, int, int]:
q_tiles = spec.seqlen_q // MFMA_ATTN_BLOCK_M
return (q_tiles, spec.common.shape.num_query_heads, batch)
def fmha_fwd_mfma_signature(spec: FmhaMfmaSpec):
kb = FmhaKernelBuilder("rocke_fmha_fwd_mfma_sig_probe", spec.common)
_declare_params(kb)
return kb.signature()