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
328 lines
13 KiB
Python
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()
|