Files
TheRock/rocm-libraries/projects/composablekernel/dispatcher/python/fmha_utils.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

1843 lines
60 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""
FMHA Dispatcher Python Utilities
Provides Python wrappers for FMHA dispatcher kernels via ctypes.
Mirrors ctypes_utils.py (GEMM) and grouped_conv_utils.py (Conv).
Usage:
from fmha_utils import FmhaDispatcherLib, FmhaRunner, FmhaProblem, cpu_attention_fwd
runner = FmhaRunner.from_prebuilt()
result = runner.run(Q, K, V, problem)
"""
import ctypes
import json
import os
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple
import numpy as np
# =============================================================================
# Utility helpers
# =============================================================================
try:
from dispatcher_common import detect_gpu_arch, get_dispatcher_root
except ImportError:
# Standalone usage without dispatcher_common on PYTHONPATH
def get_dispatcher_root() -> Path:
return Path(__file__).parent.parent
def detect_gpu_arch(fallback: str = "gfx950") -> str:
try:
out = subprocess.check_output(
["rocminfo"], text=True, stderr=subprocess.DEVNULL
)
for line in out.splitlines():
if "Name:" in line and "gfx" in line:
return line.split()[-1].strip()
except Exception:
pass
return fallback
# =============================================================================
# Data types
# =============================================================================
@dataclass
class FmhaResult:
success: bool
output: Optional[np.ndarray] = None
time_ms: float = 0.0
tflops: float = 0.0
error: str = ""
@dataclass
class FmhaProblem:
batch: int = 2
nhead_q: int = 8
nhead_k: int = 8
seqlen_q: int = 128
seqlen_k: int = 128
hdim_q: int = 128
hdim_v: int = 128
@property
def scale(self) -> float:
return 1.0 / (self.hdim_q**0.5)
@property
def num_ops(self) -> int:
sq, sk = self.seqlen_q, self.seqlen_k
return 2 * self.batch * self.nhead_q * sq * sk * (self.hdim_q + self.hdim_v)
def q_shape(self):
return (self.batch, self.nhead_q, self.seqlen_q, self.hdim_q)
def k_shape(self):
return (self.batch, self.nhead_k, self.seqlen_k, self.hdim_q)
def v_shape(self):
return (self.batch, self.nhead_k, self.seqlen_k, self.hdim_v)
def o_shape(self):
return (self.batch, self.nhead_q, self.seqlen_q, self.hdim_v)
@dataclass
class FmhaKernelConfig:
"""Complete kernel configuration for FMHA.
All tile/wave/warp dimensions are explicitly named to match the
GEMM pattern (tile_m, tile_n, tile_k) but extended for FMHA's
two-stage computation (Q*K^T stage 0, Attn*V stage 1).
"""
# -- Signature: what operation --
family: str = "fwd"
data_type: str = "fp16"
mode: str = "batch"
vlayout: str = "r"
hdim_q: int = 128
hdim_v: int = 128
gfx_arch: str = "gfx950"
# -- Algorithm: tile shape --
# Stage 0 (Q * K^T): seqlen_q x seqlen_k x hdim_q
tile_m0: int = 128 # seqlen_q tile
tile_n0: int = 128 # seqlen_k tile
tile_k0: int = 32 # hdim_q tile
# Stage 1 (Attn * V): seqlen_q x hdim_v x seqlen_k
tile_n1: int = 128 # hdim_v tile
tile_k1: int = 32 # seqlen_k tile
tile_k0max: int = 128 # max k0 (alignment)
# BWD extra stages (9-element tile)
tile_bwd6: int = 0
tile_bwd7: int = 0
tile_bwd8: int = 0
# -- Algorithm: wave config (warps per block) --
wave_m0: int = 4
wave_n0: int = 1
wave_k0: int = 1
wave_m1: int = 4
wave_n1: int = 1
wave_k1: int = 1
wave_m2: int = 1
wave_n2: int = 1
wave_k2: int = 1
# -- Algorithm: warp tile (elements per warp) --
warp_m0: int = 32
warp_n0: int = 32
warp_k0: int = 16
warp_m1: int = 32
warp_n1: int = 32
warp_k1: int = 16
warp_m2: int = 16
warp_n2: int = 16
warp_k2: int = 16
# -- Algorithm: padding --
# Values: 0=no pad, 1=pad, 8=pad with 8-byte alignment (BWD-specific)
pad_s: int = 1
pad_sk: int = 1
pad_d: int = 1
pad_dv: int = 1
# -- Algorithm: pipeline --
pipeline: str = "qr_async"
block_per_cu: int = -1
num_wave_groups: int = 1
# -- Signature: features --
mask: str = "no"
bias: str = "no"
lse: bool = False
dropout: bool = False
qscale: str = "no"
rope: str = "none"
logits: bool = False
paged_kv: bool = False
sink: bool = False
skip_min_seqlen_q: bool = False
page_size: int = 1
kv_memory_layout: str = "vectorized"
kv_lookup_table: str = "sglang"
deterministic: bool = False
dbias: bool = False
dropout_variant: str = "" # BWD: "no"/"dropout_wg16"/"dropout_wg16_storerandval"
tile_tag: str = "" # extra tile variant discriminator (e.g. "trload", "small")
use_trload: bool = False # BWD dq_dk_dv: use trload pipeline path
@property
def tile(self) -> Tuple[int, ...]:
base = (
self.tile_m0,
self.tile_n0,
self.tile_k0,
self.tile_n1,
self.tile_k1,
self.tile_k0max,
)
if self.family == "bwd_dq_dk_dv" and self.tile_bwd6 > 0:
return base + (self.tile_bwd6, self.tile_bwd7, self.tile_bwd8)
return base
@property
def wave(self) -> Tuple[int, ...]:
return (
self.wave_m0,
self.wave_n0,
self.wave_k0,
self.wave_m1,
self.wave_n1,
self.wave_k1,
self.wave_m2,
self.wave_n2,
self.wave_k2,
)
@property
def warp(self) -> Tuple[int, ...]:
return (
self.warp_m0,
self.warp_n0,
self.warp_k0,
self.warp_m1,
self.warp_n1,
self.warp_k1,
self.warp_m2,
self.warp_n2,
self.warp_k2,
)
@property
def padding(self) -> Tuple[bool, ...]:
return (self.pad_s, self.pad_sk, self.pad_d, self.pad_dv)
@property
def name(self) -> str:
s = self.pad_s
k = self.pad_sk
d = self.pad_d
v = self.pad_dv
parts = [
f"fmha_{self.family}_{self.data_type}",
self.mode,
f"h{self.hdim_q}x{self.hdim_v}"
if self.hdim_q != self.hdim_v
else f"h{self.hdim_q}",
self.pipeline,
f"t{self.tile_m0}x{self.tile_n0}x{self.tile_k0}x{self.tile_n1}x{self.tile_k1}x{self.tile_k0max}"
+ (f".{self.tile_tag}" if self.tile_tag else ""),
]
# Always include warp class for uniform naming
parts.append(f"w{self.warp_m0}x{self.warp_n0}x{self.warp_k0}")
parts.extend(
[
f"pad{s}{k}{d}{v}",
f"mask={self.mask}",
f"bias={self.bias}",
]
)
if self.lse:
parts.append("lse=1")
if self.dropout:
parts.append("drop=1")
if self.logits:
parts.append("logits=1")
if self.sink:
parts.append("sink=1")
if self.skip_min_seqlen_q:
parts.append("skip=1")
if self.qscale != "no":
parts.append(f"qs={self.qscale}")
if self.paged_kv:
parts.append("pkv=1")
if self.rope != "none":
parts.append(f"rope={self.rope}")
if self.page_size != 1:
parts.append(f"ps={self.page_size}")
if self.kv_memory_layout != "vectorized":
parts.append(f"kvl={self.kv_memory_layout}")
if self.kv_lookup_table != "sglang":
parts.append(f"kvt={self.kv_lookup_table}")
if self.deterministic:
parts.append("det=1")
if self.dbias:
parts.append("dbias=1")
if self.dropout_variant and self.dropout_variant != "no":
parts.append(f"drv={self.dropout_variant}")
# Always include block_per_cu for uniform naming
parts.append(f"bpc={self.block_per_cu}")
return "_".join(parts)
def to_codegen_json(self) -> str:
return json.dumps(
{
"arch": self.gfx_arch,
"signature": {
"family": self.family,
"data_type": self.data_type,
"mode": self.mode,
"vlayout": self.vlayout,
"hdim_q": self.hdim_q,
"hdim_v": self.hdim_v,
"mask": self.mask,
"bias": self.bias,
"lse": self.lse,
"dropout": self.dropout,
"qscale": self.qscale,
"rope": self.rope,
"logits": self.logits,
"paged_kv": self.paged_kv,
"fp8_static_quant": False,
"skip_min_seqlen_q": self.skip_min_seqlen_q,
"sink": self.sink,
"dbias": self.dbias,
"store_randval": "storerandval" in self.dropout_variant,
"deterministic": self.deterministic,
"dropout_variant": self.dropout_variant,
"kv_memory_layout": self.kv_memory_layout,
"kv_lookup_table": self.kv_lookup_table,
"page_size": self.page_size,
},
"algorithm": {
"pipeline": self.pipeline,
"tile": list(self.tile),
"wave": list(self.wave),
"warp": list(self.warp),
"padding": list(self.padding),
"block_per_cu": self.block_per_cu,
"num_wave_groups": self.num_wave_groups,
"max_splits_log2": 0,
"max_seq_len_q": 0,
"use_trload": self.use_trload,
},
}
)
# =============================================================================
# CPU reference
# =============================================================================
def _float32_to_bf16(arr: np.ndarray) -> np.ndarray:
"""Convert float32 array to bf16 stored as uint16 (truncate lower 16 bits)."""
return arr.astype(np.float32).view(np.uint32).__rshift__(16).astype(np.uint16)
def _bf16_to_float32(arr: np.ndarray) -> np.ndarray:
"""Convert bf16 (uint16) array back to float32."""
return (arr.astype(np.uint32) << 16).view(np.float32)
def cpu_attention_fwd(
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
scale: float,
mask_type: int = 0,
) -> np.ndarray:
"""CPU reference: scaled dot-product attention (supports GQA and causal mask).
Args:
Q: [batch, nhead_q, seqlen_q, hdim_q] float32
K: [batch, nhead_k, seqlen_k, hdim_q] float32
V: [batch, nhead_k, seqlen_k, hdim_v] float32
mask_type: 0=no mask, 1=causal top-left, 2=causal bottom-right
Returns:
O: [batch, nhead_q, seqlen_q, hdim_v] float32
"""
nhead_q = Q.shape[1]
nhead_k = K.shape[1]
if nhead_q != nhead_k:
ratio = nhead_q // nhead_k
K = np.repeat(K, ratio, axis=1)
V = np.repeat(V, ratio, axis=1)
S = np.matmul(Q, K.transpose(0, 1, 3, 2)) * scale
if mask_type in (1, 2):
sq, sk = S.shape[-2], S.shape[-1]
row = np.arange(sq).reshape(sq, 1)
col = np.arange(sk).reshape(1, sk)
if mask_type == 1: # top-left causal
causal_mask = col <= row
else: # bottom-right causal
causal_mask = col <= (row + sk - sq)
S = np.where(causal_mask, S, -1e9)
S_max = S.max(axis=-1, keepdims=True)
S_exp = np.exp(S - S_max)
P = S_exp / S_exp.sum(axis=-1, keepdims=True)
return np.matmul(P, V)
def cpu_attention_fwd_with_intermediates(
Q: np.ndarray, K: np.ndarray, V: np.ndarray, scale: float
) -> tuple:
"""CPU reference forward returning (output, P) for backward use.
Same as cpu_attention_fwd but also returns the softmax probability matrix P.
"""
nhead_q = Q.shape[1]
nhead_k = K.shape[1]
if nhead_q != nhead_k:
ratio = nhead_q // nhead_k
K = np.repeat(K, ratio, axis=1)
V = np.repeat(V, ratio, axis=1)
S = np.matmul(Q, K.transpose(0, 1, 3, 2)) * scale
S_max = S.max(axis=-1, keepdims=True)
S_exp = np.exp(S - S_max)
P = S_exp / S_exp.sum(axis=-1, keepdims=True)
out = np.matmul(P, V)
return out, P
def cpu_attention_bwd(
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
out: np.ndarray,
dO: np.ndarray,
P: np.ndarray,
scale: float,
) -> tuple:
"""CPU reference backward. Returns (dQ, dK, dV).
Args:
Q, K, V: forward inputs [batch, heads, seq, dim]
out: forward output
dO: gradient of output
P: softmax probabilities from forward
scale: attention scale factor
"""
D = (dO * out).sum(axis=-1, keepdims=True)
dP = np.matmul(dO, V.transpose(0, 1, 3, 2))
dS = P * (dP - D)
dQ = np.matmul(dS, K) * scale
dK = np.matmul(dS.transpose(0, 1, 3, 2), Q) * scale
dV = np.matmul(P.transpose(0, 1, 3, 2), dO)
return dQ, dK, dV
# =============================================================================
# Low-level ctypes wrapper
# =============================================================================
class FmhaDispatcherLib:
"""Wrapper for the FMHA dispatcher shared library (libdispatcher_fmha_lib.so)."""
SEARCH_PATHS = [
"build/examples/libdispatcher_fmha_lib.so",
"build/libdispatcher_fmha_lib.so",
"build/lib/libdispatcher_fmha_lib.so",
]
def __init__(self, lib: ctypes.CDLL, path: Path):
self._lib = lib
self.path = path
self._setup()
def _setup(self):
lib = self._lib
lib.fmha_dispatcher_initialize.argtypes = [ctypes.c_char_p]
lib.fmha_dispatcher_initialize.restype = ctypes.c_int
lib.fmha_dispatcher_run_fwd.argtypes = [
ctypes.c_void_p, # q
ctypes.c_void_p, # k
ctypes.c_void_p, # v
ctypes.c_void_p, # o
ctypes.c_int, # batch
ctypes.c_int, # nhead_q
ctypes.c_int, # nhead_k
ctypes.c_int, # seqlen_q
ctypes.c_int, # seqlen_k
ctypes.c_int, # hdim_q
ctypes.c_int, # hdim_v
ctypes.c_float, # scale
ctypes.c_int, # mask_type
ctypes.c_int, # bias_type
ctypes.c_int, # has_lse
ctypes.c_int, # has_dropout
ctypes.c_int, # traits_hdim_q (0=same as hdim_q)
ctypes.c_int, # traits_hdim_v (0=same as hdim_v)
ctypes.c_int, # is_v_rowmajor (1=row, 0=col)
ctypes.c_int, # perm (1=BHSD, 0=BSHD)
ctypes.c_char_p, # data_type ("fp16", "bf16")
ctypes.c_int, # is_group_mode
ctypes.c_int, # window_left (-1=no window)
ctypes.c_int, # window_right (-1=no window, 0=causal)
ctypes.c_int, # has_logits
ctypes.c_int, # has_sink
ctypes.c_int, # has_skip
ctypes.POINTER(ctypes.c_float), # time_ms_out
]
lib.fmha_dispatcher_run_fwd.restype = ctypes.c_int
lib.fmha_dispatcher_run_bwd.argtypes = [
ctypes.c_void_p, # q
ctypes.c_void_p, # k
ctypes.c_void_p, # v
ctypes.c_void_p, # o
ctypes.c_void_p, # lse
ctypes.c_void_p, # do
ctypes.c_void_p, # dq
ctypes.c_void_p, # dk
ctypes.c_void_p, # dv
ctypes.c_int, # batch
ctypes.c_int, # nhead_q
ctypes.c_int, # nhead_k
ctypes.c_int, # seqlen_q
ctypes.c_int, # seqlen_k
ctypes.c_int, # hdim_q
ctypes.c_int, # hdim_v
ctypes.c_float, # scale
ctypes.c_char_p, # data_type_str
ctypes.c_int, # mask_type_int
ctypes.c_int, # bias_type_int
ctypes.c_int, # has_dropout
ctypes.c_int, # has_dbias
ctypes.c_int, # is_deterministic
ctypes.c_int, # is_group_mode
ctypes.c_int, # is_store_randval
ctypes.c_int, # tile_n0 (kN0 for nsplits computation)
ctypes.POINTER(ctypes.c_float), # time_ms_out
]
lib.fmha_dispatcher_run_bwd.restype = ctypes.c_int
# Split-KV forward
lib.fmha_dispatcher_run_splitkv.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_float,
ctypes.c_int, # mask_type
ctypes.c_int, # num_splits
ctypes.c_int, # is_v_rowmajor
ctypes.c_char_p,
ctypes.c_int, # has_lse
ctypes.c_int, # is_group_mode
ctypes.c_int, # perm
ctypes.c_int, # has_logits
ctypes.c_int, # bias_type
ctypes.c_int, # has_sink
ctypes.c_int, # paged_kv
ctypes.c_int, # page_block_size
ctypes.c_int, # window_left
ctypes.c_int, # window_right
ctypes.POINTER(ctypes.c_float),
]
lib.fmha_dispatcher_run_splitkv.restype = ctypes.c_int
# Paged-KV forward
lib.fmha_dispatcher_run_pagedkv.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_float,
ctypes.c_int, # mask_type
ctypes.c_int, # page_block_size
ctypes.c_int, # is_v_rowmajor
ctypes.c_char_p,
ctypes.c_int, # has_lse
ctypes.c_int, # has_logits
ctypes.c_int, # has_sink
ctypes.c_int, # skip_min_seqlen_q
ctypes.c_int, # bias_type
ctypes.POINTER(ctypes.c_float),
]
lib.fmha_dispatcher_run_pagedkv.restype = ctypes.c_int
# Append-KV
lib.fmha_dispatcher_run_appendkv.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int, # is_v_rowmajor
ctypes.c_int, # rope_type
ctypes.c_int, # paged_kv
ctypes.c_int, # page_block_size
ctypes.c_char_p,
ctypes.POINTER(ctypes.c_float),
]
lib.fmha_dispatcher_run_appendkv.restype = ctypes.c_int
# Batch Prefill
lib.fmha_dispatcher_run_batch_prefill.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_int,
ctypes.c_float,
ctypes.c_int, # mask_type
ctypes.c_int, # bias_type
ctypes.c_int, # page_block_size
ctypes.c_int, # kv_layout_int
ctypes.c_int, # kv_lookup_int
ctypes.c_int, # is_v_rowmajor
ctypes.c_char_p,
ctypes.c_int, # has_lse
ctypes.c_int, # has_dropout
ctypes.c_int, # has_logits
ctypes.c_int, # has_sink
ctypes.c_int, # skip_min_seqlen_q
ctypes.POINTER(ctypes.c_float),
]
lib.fmha_dispatcher_run_batch_prefill.restype = ctypes.c_int
lib.fmha_dispatcher_kernel_count.argtypes = []
lib.fmha_dispatcher_kernel_count.restype = ctypes.c_int
lib.fmha_dispatcher_cleanup.argtypes = []
lib.fmha_dispatcher_cleanup.restype = None
@classmethod
def find(cls) -> Optional["FmhaDispatcherLib"]:
root = get_dispatcher_root()
for rel in cls.SEARCH_PATHS:
path = root / rel
if path.exists():
try:
lib = ctypes.CDLL(str(path))
return cls(lib, path)
except OSError:
continue
return None
@classmethod
def load(cls, path: str) -> "FmhaDispatcherLib":
lib = ctypes.CDLL(path)
return cls(lib, Path(path))
def initialize(self, arch: str = "gfx950") -> bool:
return self._lib.fmha_dispatcher_initialize(arch.encode()) == 0
def run_bwd(
self,
q: ctypes.c_void_p,
k: ctypes.c_void_p,
v: ctypes.c_void_p,
o: ctypes.c_void_p,
lse: ctypes.c_void_p,
do_grad: ctypes.c_void_p,
dq: ctypes.c_void_p,
dk: ctypes.c_void_p,
dv: ctypes.c_void_p,
prob: FmhaProblem,
data_type: str = "fp16",
mask_type: int = 0,
bias_type: int = 0,
has_dropout: bool = False,
has_dbias: bool = False,
is_deterministic: bool = False,
is_group_mode: bool = False,
is_store_randval: bool = False,
tile_n0: int = 128,
) -> Tuple[int, float]:
time_ms = ctypes.c_float(0.0)
rc = self._lib.fmha_dispatcher_run_bwd(
q,
k,
v,
o,
lse,
do_grad,
dq,
dk,
dv,
prob.batch,
prob.nhead_q,
prob.nhead_k,
prob.seqlen_q,
prob.seqlen_k,
prob.hdim_q,
prob.hdim_v,
prob.scale,
data_type.encode(),
ctypes.c_int(mask_type),
ctypes.c_int(bias_type),
ctypes.c_int(int(has_dropout)),
ctypes.c_int(int(has_dbias)),
ctypes.c_int(int(is_deterministic)),
ctypes.c_int(int(is_group_mode)),
ctypes.c_int(int(is_store_randval)),
ctypes.c_int(tile_n0),
ctypes.byref(time_ms),
)
return rc, time_ms.value
def kernel_count(self) -> int:
return self._lib.fmha_dispatcher_kernel_count()
def cleanup(self):
self._lib.fmha_dispatcher_cleanup()
# =============================================================================
# High-level GPU runner (mirrors GpuGroupedConvRunner)
# =============================================================================
class FmhaRunner:
"""High-level FMHA runner with NumPy interface and HIP memory management."""
HIP_MEMCPY_H2D = 1
HIP_MEMCPY_D2H = 2
def __init__(self, dispatch_lib: FmhaDispatcherLib, arch: str = "gfx950"):
self._lib = dispatch_lib
self._arch = arch
self._hip = None
self._load_hip()
if not dispatch_lib.initialize(arch):
raise RuntimeError("Failed to initialize FMHA dispatcher")
def _load_hip(self):
for name in ["libamdhip64.so", "libamdhip64.so.6"]:
try:
self._hip = ctypes.CDLL(name)
self._hip.hipMalloc.argtypes = [
ctypes.POINTER(ctypes.c_void_p),
ctypes.c_size_t,
]
self._hip.hipMalloc.restype = ctypes.c_int
self._hip.hipFree.argtypes = [ctypes.c_void_p]
self._hip.hipFree.restype = ctypes.c_int
self._hip.hipMemcpy.argtypes = [
ctypes.c_void_p,
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_int,
]
self._hip.hipMemcpy.restype = ctypes.c_int
self._hip.hipMemset.argtypes = [
ctypes.c_void_p,
ctypes.c_int,
ctypes.c_size_t,
]
self._hip.hipMemset.restype = ctypes.c_int
return
except OSError:
continue
raise RuntimeError("Could not load libamdhip64.so")
@classmethod
def from_prebuilt(cls, arch: Optional[str] = None) -> "FmhaRunner":
arch = arch or detect_gpu_arch()
lib = FmhaDispatcherLib.find()
if lib is None:
raise RuntimeError(
"FMHA dispatcher library not found. Build with:\n"
" cd dispatcher/build && cmake .. -DBUILD_DISPATCHER_EXAMPLES=ON && make dispatcher_fmha_lib"
)
return cls(lib, arch)
@classmethod
def from_library(cls, path: str, arch: Optional[str] = None) -> "FmhaRunner":
arch = arch or detect_gpu_arch()
return cls(FmhaDispatcherLib.load(path), arch)
def run(
self,
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
prob: FmhaProblem,
mask_type: int = 0,
bias_type: int = 0,
has_lse: int = 0,
has_dropout: int = 0,
has_logits: int = 0,
has_sink: int = 0,
has_skip: int = 0,
api_family: str = "fwd",
data_type: str = "fp16",
**kwargs,
) -> "FmhaResult":
"""Run FMHA forward on GPU with automatic HIP memory management.
Args:
Q: [batch, nhead_q, seqlen_q, hdim_q] float16
K: [batch, nhead_k, seqlen_k, hdim_q] float16
V: [batch, nhead_k, seqlen_k, hdim_v] float16
Returns:
FmhaResult with output array, timing, TFLOPS
"""
# Map CK dtype to numpy dtype for buffer allocation.
# bf16 is stored as uint16 (upper 16 bits of float32).
# fp8 uses uint8 (1 byte per element).
_NP_DTYPE = {
"fp16": np.float16,
"bf16": np.uint16,
"fp32": np.float32,
"fp8bf16": np.uint8,
"fp8fp32": np.uint8,
"bf8": np.uint8,
}
_NP_OUT_DTYPE = {
"fp16": np.float16,
"bf16": np.uint16,
"fp32": np.float32,
"fp8bf16": np.float16,
"fp8fp32": np.float32,
"bf8": np.uint8,
}
in_dt = _NP_DTYPE.get(data_type, np.float16)
out_dt = _NP_OUT_DTYPE.get(data_type, np.float16)
if data_type == "bf16":
Q_c = _float32_to_bf16(np.ascontiguousarray(Q.astype(np.float32)))
K_c = _float32_to_bf16(np.ascontiguousarray(K.astype(np.float32)))
V_c = _float32_to_bf16(np.ascontiguousarray(V.astype(np.float32)))
else:
Q_c = np.ascontiguousarray(Q.astype(in_dt))
K_c = np.ascontiguousarray(K.astype(in_dt))
V_c = np.ascontiguousarray(V.astype(in_dt))
O_c = np.zeros(prob.o_shape(), dtype=out_dt)
d_q, d_k, d_v, d_o = (ctypes.c_void_p() for _ in range(4))
try:
self._hip.hipMalloc(ctypes.byref(d_q), Q_c.nbytes)
self._hip.hipMalloc(ctypes.byref(d_k), K_c.nbytes)
self._hip.hipMalloc(ctypes.byref(d_v), V_c.nbytes)
self._hip.hipMalloc(ctypes.byref(d_o), O_c.nbytes)
self._hip.hipMemcpy(d_q, Q_c.ctypes.data, Q_c.nbytes, self.HIP_MEMCPY_H2D)
self._hip.hipMemcpy(d_k, K_c.ctypes.data, K_c.nbytes, self.HIP_MEMCPY_H2D)
self._hip.hipMemcpy(d_v, V_c.ctypes.data, V_c.nbytes, self.HIP_MEMCPY_H2D)
self._hip.hipMemset(d_o, 0, O_c.nbytes)
time_ms = ctypes.c_float(0.0)
lib = self._lib._lib
is_v_rowmajor = kwargs.get("is_v_rowmajor", 1)
is_group_mode = kwargs.get("is_group_mode", 0)
perm = kwargs.get("perm", 1)
window_left = kwargs.get("window_left", -1)
window_right = kwargs.get("window_right", -1)
num_splits = kwargs.get("num_splits", 4)
page_size = kwargs.get("page_size", 64)
kv_layout = kwargs.get("kv_layout", 0)
kv_lookup = kwargs.get("kv_lookup", 0)
traits_hdim_q = kwargs.get("traits_hdim_q", 0)
traits_hdim_v = kwargs.get("traits_hdim_v", 0)
if api_family == "splitkv":
paged_kv = kwargs.get("paged_kv", 0)
rc = lib.fmha_dispatcher_run_splitkv(
d_q,
d_k,
d_v,
d_o,
prob.batch,
prob.nhead_q,
prob.nhead_k,
prob.seqlen_q,
prob.seqlen_k,
prob.hdim_q,
prob.hdim_v,
prob.scale,
mask_type,
num_splits,
is_v_rowmajor,
data_type.encode(),
has_lse,
is_group_mode,
perm,
has_logits,
bias_type,
has_sink,
paged_kv,
page_size,
window_left,
window_right,
ctypes.byref(time_ms),
)
elif api_family == "pagedkv":
rc = lib.fmha_dispatcher_run_pagedkv(
d_q,
d_k,
d_v,
d_o,
prob.batch,
prob.nhead_q,
prob.nhead_k,
prob.seqlen_q,
prob.seqlen_k,
prob.hdim_q,
prob.hdim_v,
prob.scale,
mask_type,
page_size,
is_v_rowmajor,
data_type.encode(),
has_lse,
has_logits,
has_sink,
has_skip,
bias_type,
ctypes.byref(time_ms),
)
elif api_family == "appendkv":
seqlen_knew = kwargs.get("seqlen_knew", prob.seqlen_k)
rc = lib.fmha_dispatcher_run_appendkv(
Q_c.ctypes.data,
K_c.ctypes.data,
V_c.ctypes.data,
prob.batch,
prob.nhead_q,
prob.nhead_k,
prob.seqlen_q,
seqlen_knew,
prob.hdim_q,
prob.hdim_v,
is_v_rowmajor,
kwargs.get("rope_type", 0),
kwargs.get("paged_kv", 0),
page_size,
data_type.encode(),
ctypes.byref(time_ms),
)
elif api_family == "batch_prefill":
skip_min_sq = kwargs.get("skip_min_seqlen_q", 0)
rc = lib.fmha_dispatcher_run_batch_prefill(
d_q,
d_k,
d_v,
d_o,
prob.batch,
prob.nhead_q,
prob.nhead_k,
prob.seqlen_q,
prob.seqlen_k,
prob.hdim_q,
prob.hdim_v,
prob.scale,
mask_type,
bias_type,
page_size,
kv_layout,
kv_lookup,
is_v_rowmajor,
data_type.encode(),
has_lse,
has_dropout,
has_logits,
has_sink,
skip_min_sq,
ctypes.byref(time_ms),
)
else:
rc = lib.fmha_dispatcher_run_fwd(
d_q,
d_k,
d_v,
d_o,
prob.batch,
prob.nhead_q,
prob.nhead_k,
prob.seqlen_q,
prob.seqlen_k,
prob.hdim_q,
prob.hdim_v,
prob.scale,
mask_type,
bias_type,
has_lse,
has_dropout,
traits_hdim_q,
traits_hdim_v,
is_v_rowmajor,
perm,
data_type.encode(),
is_group_mode,
window_left,
window_right,
has_logits,
has_sink,
has_skip,
ctypes.byref(time_ms),
)
if rc != 0:
return FmhaResult(success=False, error=f"Kernel failed (rc={rc})")
self._hip.hipMemcpy(O_c.ctypes.data, d_o, O_c.nbytes, self.HIP_MEMCPY_D2H)
# Convert bf16 output (uint16) back to float32 for comparison
if data_type == "bf16":
O_c = _bf16_to_float32(O_c)
# appendkv is a memory op (KV cache copy), not compute -- no TFLOPS
ops = 0 if api_family == "appendkv" else prob.num_ops
tflops = (
ops / (time_ms.value * 1e-3) / 1e12
if time_ms.value > 0 and ops > 0
else 0.0
)
return FmhaResult(
success=True, output=O_c, time_ms=time_ms.value, tflops=tflops
)
finally:
for d in [d_q, d_k, d_v, d_o]:
if d.value:
self._hip.hipFree(d)
def run_bwd(
self,
Q: np.ndarray,
K: np.ndarray,
V: np.ndarray,
out: np.ndarray,
LSE: np.ndarray,
dO: np.ndarray,
prob: FmhaProblem,
data_type: str = "fp16",
mask_type: int = 0,
bias_type: int = 0,
has_dropout: bool = False,
has_dbias: bool = False,
is_deterministic: bool = False,
is_group_mode: bool = False,
is_store_randval: bool = False,
tile_n0: int = 128,
) -> "FmhaResult":
"""Run FMHA backward on GPU with automatic HIP memory management.
Returns FmhaResult with dQ, dK, dV packed in output as a tuple.
"""
_NP_DTYPE = {
"fp16": np.float16,
"bf16": np.float16,
"fp32": np.float32,
"fp8bf16": np.uint8,
"fp8fp32": np.uint8,
"bf8": np.uint8,
}
in_dt = _NP_DTYPE.get(data_type, np.float16)
Q_c = np.ascontiguousarray(Q.astype(in_dt))
K_c = np.ascontiguousarray(K.astype(in_dt))
V_c = np.ascontiguousarray(V.astype(in_dt))
O_c = np.ascontiguousarray(out.astype(in_dt))
LSE_c = np.ascontiguousarray(LSE.astype(np.float32))
dO_c = np.ascontiguousarray(dO.astype(in_dt))
dQ_c = np.zeros_like(Q_c)
dK_c = np.zeros_like(K_c)
dV_c = np.zeros_like(V_c)
ptrs = [ctypes.c_void_p() for _ in range(9)]
d_q, d_k, d_v, d_o, d_lse, d_do, d_dq, d_dk, d_dv = ptrs
try:
for d, arr in zip(ptrs[:6], [Q_c, K_c, V_c, O_c, LSE_c, dO_c]):
self._hip.hipMalloc(ctypes.byref(d), arr.nbytes)
self._hip.hipMemcpy(d, arr.ctypes.data, arr.nbytes, self.HIP_MEMCPY_H2D)
for d, arr in zip(ptrs[6:], [dQ_c, dK_c, dV_c]):
self._hip.hipMalloc(ctypes.byref(d), arr.nbytes)
self._hip.hipMemset(d, 0, arr.nbytes)
rc, elapsed = self._lib.run_bwd(
d_q,
d_k,
d_v,
d_o,
d_lse,
d_do,
d_dq,
d_dk,
d_dv,
prob,
data_type,
mask_type=mask_type,
bias_type=bias_type,
has_dropout=has_dropout,
has_dbias=has_dbias,
is_deterministic=is_deterministic,
is_group_mode=is_group_mode,
is_store_randval=is_store_randval,
tile_n0=tile_n0,
)
if rc != 0:
return FmhaResult(success=False, error=f"BWD kernel failed (rc={rc})")
for d, arr in zip(ptrs[6:], [dQ_c, dK_c, dV_c]):
self._hip.hipMemcpy(arr.ctypes.data, d, arr.nbytes, self.HIP_MEMCPY_D2H)
tflops = prob.num_ops / (elapsed * 1e-3) / 1e12 if elapsed > 0 else 0.0
return FmhaResult(
success=True,
output=(dQ_c, dK_c, dV_c),
time_ms=elapsed,
tflops=tflops,
)
finally:
for d in ptrs:
if d.value:
self._hip.hipFree(d)
@property
def kernel_count(self) -> int:
return self._lib.kernel_count()
@property
def library_path(self) -> str:
return str(self._lib.path)
def cleanup(self):
self._lib.cleanup()
# =============================================================================
# JIT Build Support (mirrors setup_multiple_gemm_dispatchers)
# =============================================================================
@dataclass
class FmhaSetupResult:
success: bool
config: Optional[FmhaKernelConfig] = None
runner: Optional[FmhaRunner] = None
library_path: str = ""
error: str = ""
build_time_s: float = 0.0
def _build_static_lib(root: Path) -> Optional[Path]:
"""Build libck_tile_dispatcher.a via cmake if not already present."""
build_dir = root / "build"
build_dir.mkdir(parents=True, exist_ok=True)
hipcc = _find_hipcc()
cmake_cmd = ["cmake", str(root), f"-DCMAKE_CXX_COMPILER={hipcc}"]
r = subprocess.run(cmake_cmd, cwd=str(build_dir), capture_output=True, text=True)
if r.returncode != 0:
print(
f"Warning: cmake failed for dispatcher lib: {r.stderr[:200]}",
file=sys.stderr,
)
return None
make_cmd = ["make", "ck_tile_dispatcher", f"-j{os.cpu_count() or 4}"]
r = subprocess.run(make_cmd, cwd=str(build_dir), capture_output=True, text=True)
if r.returncode != 0:
print(
f"Warning: make failed for dispatcher lib: {r.stderr[:200]}",
file=sys.stderr,
)
return None
lib_path = build_dir / "libck_tile_dispatcher.a"
return lib_path if lib_path.exists() else None
def _find_static_lib() -> Optional[Path]:
root = get_dispatcher_root()
for rel in ["build/libck_tile_dispatcher.a", "build/lib/libck_tile_dispatcher.a"]:
p = root / rel
if p.exists():
return p
# Auto-build if not found
print(" Building libck_tile_dispatcher.a (first time)...", file=sys.stderr)
return _build_static_lib(root)
def _find_hipcc() -> str:
for path in ["/opt/rocm/bin/hipcc", "/usr/bin/hipcc"]:
if os.path.exists(path):
return path
return "hipcc"
def fmha_compile_flags(arch: str, hipcc: str = "", family: str = "") -> List[str]:
"""Base hipcc flags for compiling FMHA kernels. Shared by JIT and tile engine.
Source: example/ck_tile/01_fmha/CMakeLists.txt — mirrors CK's own build
flags to ensure parity. Key defines:
- CK_TILE_FMHA_FWD_FAST_EXP2: enables fast exp2 on gfx9 (CDNA)
- CK_TILE_USE_OCP_FP8: uses OCP standard fp8 format
- CK_GFX950_SUPPORT / CK_USE_GFX950: enables gfx950-specific code paths
- CK_USE_XDL: enables MFMA (matrix fused multiply-add) instructions
- CK_TILE_USE_WMMA: 0 for CDNA (uses MFMA instead)
- CK_TILE_FLOAT_TO_BFLOAT16_DEFAULT=3: BWD bf16 conversion mode
"""
if not hipcc:
hipcc = _find_hipcc()
root = get_dispatcher_root()
flags = [
hipcc,
"-c",
"-fPIC",
"-O3",
"-DNDEBUG",
f"--offload-arch={arch}",
"-std=c++17",
f"-I{root.parent / 'include'}",
f"-I{root / 'include'}",
f"-I{root.parent}",
"-Wno-undefined-func-template",
"-Wno-float-equal",
"-fgpu-flush-denormals-to-zero",
"-fno-offload-uniform-block",
"-mllvm",
"--lsr-drop-solution=1",
"-mllvm",
"-enable-post-misched=0",
"-mllvm",
"-amdgpu-early-inline-all=true",
"-mllvm",
"-amdgpu-function-calls=false",
]
if arch.startswith("gfx9"):
flags.append("-DCK_TILE_FMHA_FWD_FAST_EXP2=1")
flags.append("-DCK_TILE_USE_OCP_FP8")
flags.append("-DCK_GFX950_SUPPORT")
flags.append("-DCK_USE_GFX950")
flags.append("-DCK_USE_GFX94")
flags.append("-DCK_USE_XDL")
flags.append("-DCK_TILE_USE_WMMA=0")
else:
flags.append("-DCK_TILE_FMHA_FWD_FAST_EXP2=0")
# API enablement flags (match CMakeLists.txt conditional defines)
flags.append("-DCK_TILE_FMHA_FWD_SPLITKV_API=1")
flags.append("-DCK_TILE_FMHA_FWD_APPENDKV_API=1")
flags.append("-DCK_TILE_FMHA_FWD_PAGEDKV_API=1")
# BWD-specific flags
if family.startswith("bwd"):
flags.append("-DCK_TILE_FLOAT_TO_BFLOAT16_DEFAULT=3")
return flags
def _make_splitkv_combine_config(splitkv_cfg: FmhaKernelConfig) -> FmhaKernelConfig:
"""Create a matching fwd_splitkv_combine config for a fwd_splitkv config.
Source: fmha_fwd.py splitkv_combine tile — fixed (32, hdim_v, 32, 32) tile.
The combine_bn1=32 comes from specs.py load_arch_specs() splitkv_combine dict.
The combine kernel merges partial results from the split stage into the
final output. Must be in the same .so as the split kernel for the
2-stage splitkv pipeline.
"""
import copy
comb = copy.copy(splitkv_cfg)
comb.family = "fwd_splitkv_combine"
comb.pipeline = "splitkv_combine"
hv = splitkv_cfg.hdim_v
comb.hdim_q = hv
comb.hdim_v = hv
comb.tile_m0 = 32
comb.tile_n0 = hv
comb.tile_k0 = 32
comb.tile_n1 = 32
comb.tile_k1 = 0
comb.tile_k0max = 0
comb.pad_s = 1 if splitkv_cfg.mode == "group" else 0
comb.pad_sk = 1
comb.pad_d = 1
comb.pad_dv = 1
comb.lse = True
# Combine doesn't use mask/bias/etc., but the dispatcher's supports() check
# matches the combine kernel's signature against the problem traits.
# Keep them from the split config so the signatures match.
comb.dropout = False
comb.skip_min_seqlen_q = False
comb.qscale = "no"
comb.rope = "none"
return comb
def _make_bwd_dot_do_o_config(dq_cfg: FmhaKernelConfig) -> FmhaKernelConfig:
"""Create a matching bwd_dot_do_o config for a bwd_dq_dk_dv config.
Source: fmha_bwd.py FmhaBwdDotDoOTileSize — fixed tile (64, max(hv,128), 32).
Warp tile (32,32,16) with 4 waves in M = standard fp16/bf16 MFMA config.
The dot_do_o kernel computes d = rowsum(O * dO) and must be in the same
.so as the dq_dk_dv kernel for the 2-stage BWD pipeline.
"""
import copy
dot = copy.copy(dq_cfg)
dot.family = "bwd_dot_do_o"
dot.pipeline = "qr"
hq, hv = dq_cfg.hdim_q, dq_cfg.hdim_v
dot.tile_m0 = 64
dot.tile_n0 = max(hv, 128)
dot.tile_k0 = 32
dot.tile_n1 = max(hv, 128)
dot.tile_k1 = 32
dot.tile_k0max = max(hq, 128)
dot.wave_m0 = 4
dot.wave_n0 = 1
dot.wave_k0 = 1
dot.wave_m1 = 4
dot.wave_n1 = 1
dot.wave_k1 = 1
dot.warp_m0 = 32
dot.warp_n0 = 32
dot.warp_k0 = 16
dot.warp_m1 = 32
dot.warp_n1 = 32
dot.warp_k1 = 16
dot.use_trload = False
# dot_do_o uses all-padded for maximum compatibility
dot.pad_s = 1
dot.pad_sk = 1
dot.pad_d = 1
dot.pad_dv = 1
# BWD traits don't have logits/sink/skip/lse/paged_kv -- from_invocation
# defaults them to false/0. The dot_do_o signature must match these defaults.
dot.logits = False
dot.sink = False
dot.skip_min_seqlen_q = False
dot.lse = False
dot.paged_kv = False
dot.qscale = "no"
dot.rope = "no"
# dot_do_o must match the problem's is_store_randval (from traits);
# keep dropout_variant as-is so store_randval matches
return dot
def setup_fmha_dispatcher(
config: FmhaKernelConfig,
output_dir: Optional[Path] = None,
verbose: bool = False,
) -> FmhaSetupResult:
"""JIT-compile a single FMHA kernel and return a runner.
Cached: if the .so already exists, loads it directly (~1ms).
Fresh build: codegen → parallel compile (kernel + ctypes) → link.
"""
import time
t0 = time.perf_counter()
root = get_dispatcher_root()
codegen_dir = root / "codegen"
ctypes_src = root / "bindings" / "ctypes" / "fmha_ctypes_lib.cpp"
static_lib = _find_static_lib()
hipcc = _find_hipcc()
if output_dir is None:
output_dir = root / "build" / "examples" / f"fmha_jit_{config.name}"
output_dir.mkdir(parents=True, exist_ok=True)
lib_name = f"libdispatcher_fmha_{config.name}.so"
lib_path = output_dir / lib_name
# Cache hit: .so already exists, just load
if lib_path.exists():
try:
runner = FmhaRunner.from_library(str(lib_path), config.gfx_arch)
return FmhaSetupResult(
success=True,
config=config,
runner=runner,
library_path=str(lib_path),
build_time_s=time.perf_counter() - t0,
)
except Exception:
pass # stale .so, rebuild
if not static_lib:
return FmhaSetupResult(
success=False, config=config, error="libck_tile_dispatcher.a not found"
)
if not ctypes_src.exists():
return FmhaSetupResult(
success=False, config=config, error="fmha_ctypes_lib.cpp not found"
)
# Step 1: Codegen
# BWD dq_dk_dv needs a matching dot_do_o kernel in the same .so
# BWD dq_dk_dv needs matching dot_do_o kernel for the 2-stage pipeline
if config.family == "bwd_dq_dk_dv":
dot_cfg = _make_bwd_dot_do_o_config(config)
config_json_str = json.dumps(
[
json.loads(dot_cfg.to_codegen_json()),
json.loads(config.to_codegen_json()),
]
)
else:
config_json_str = config.to_codegen_json()
gen_cmd = [
sys.executable,
str(codegen_dir / "fmha" / "generate_fallback.py"),
"--output-dir",
str(output_dir),
"--gpu-target",
config.gfx_arch,
"--config-json",
config_json_str,
]
r = subprocess.run(gen_cmd, capture_output=True, text=True, cwd=str(codegen_dir))
if r.returncode != 0:
return FmhaSetupResult(
success=False, config=config, error=f"Codegen failed: {r.stderr[:500]}"
)
dispatch_header = output_dir / "fmha_python_dispatch.hpp"
if not dispatch_header.exists():
return FmhaSetupResult(
success=False, config=config, error="Dispatch header not generated"
)
# Step 2: Compile kernel .cpp AND ctypes in parallel
kernel_cpps = list(output_dir.glob("fmha_*.cpp"))
base_flags = fmha_compile_flags(config.gfx_arch, hipcc, family=config.family)
compile_jobs = []
for cpp in kernel_cpps:
obj = cpp.with_suffix(".o")
compile_jobs.append((base_flags + [str(cpp), "-o", str(obj)], obj, "kernel"))
ctypes_obj = output_dir / "fmha_ctypes_lib.o"
ctypes_cmd = base_flags + [
f"-I{output_dir}",
f"-I{output_dir / 'dispatcher_wrappers'}",
f"-include{dispatch_header}",
f'-DGFX_ARCH="{config.gfx_arch}"',
str(ctypes_src),
"-o",
str(ctypes_obj),
]
compile_jobs.append((ctypes_cmd, ctypes_obj, "ctypes"))
def _run_compile(job):
cmd, obj, label = job
if obj.exists():
return (True, obj, label, "")
r = subprocess.run(cmd, capture_output=True, text=True)
return (r.returncode == 0, obj, label, r.stderr[:500])
with ThreadPoolExecutor(max_workers=len(compile_jobs)) as pool:
results = list(pool.map(_run_compile, compile_jobs))
kernel_objs = []
for ok, obj, label, err in results:
if not ok:
return FmhaSetupResult(
success=False,
config=config,
error=f"{label} compile failed: {err}",
)
if label == "kernel":
kernel_objs.append(str(obj))
# Step 3: Link
link_cmd = [
hipcc,
"-shared",
"-fPIC",
str(ctypes_obj),
*kernel_objs,
str(static_lib),
"-o",
str(lib_path),
]
r = subprocess.run(link_cmd, capture_output=True, text=True)
if r.returncode != 0:
return FmhaSetupResult(
success=False, config=config, error=f"Link failed: {r.stderr[:500]}"
)
# Step 4: Load
try:
runner = FmhaRunner.from_library(str(lib_path), config.gfx_arch)
except Exception as e:
return FmhaSetupResult(success=False, config=config, error=f"Load failed: {e}")
elapsed = time.perf_counter() - t0
return FmhaSetupResult(
success=True,
config=config,
runner=runner,
library_path=str(lib_path),
build_time_s=elapsed,
)
def _run_compile_job(job):
"""Module-level compile worker -- no threads, uses file-based stderr."""
cmd, obj_str, name, label = job
if os.path.exists(obj_str):
return (name, True, "")
err_path = obj_str + ".err"
with open(err_path, "w") as ef:
rc = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=ef)
if rc != 0:
try:
err = open(err_path).read()[:200]
except Exception:
err = f"rc={rc}"
return (name, False, err)
try:
os.unlink(err_path)
except OSError:
pass
return (name, True, "")
def setup_multiple_fmha_dispatchers(
configs: List[FmhaKernelConfig],
output_dir: Optional[Path] = None,
verbose: bool = False,
max_workers: Optional[int] = None,
executor=None,
progress_callback=None,
) -> List[FmhaSetupResult]:
"""3-stage pipelined JIT: codegen(parallel) -> compile(parallel) -> link+load(parallel).
Faster than calling setup_fmha_dispatcher() per-kernel because all hipcc
compile jobs (kernel + ctypes from ALL kernels) share one thread pool.
"""
if not configs:
return []
root = get_dispatcher_root()
codegen_dir = root / "codegen"
ctypes_src = root / "bindings" / "ctypes" / "fmha_ctypes_lib.cpp"
static_lib = _find_static_lib()
hipcc = _find_hipcc()
arch = configs[0].gfx_arch
if output_dir is None:
output_dir = root / "build" / "examples"
results: dict[str, FmhaSetupResult] = {}
# --- Stage 1: Codegen (sequential, skip cached) ---
def _codegen(cfg):
out = output_dir / f"fmha_jit_{cfg.name}"
lib_path = out / f"libdispatcher_fmha_{cfg.name}.so"
# Fast path: .so exists, register result and skip
if lib_path.exists():
results[cfg.name] = FmhaSetupResult(
success=True, config=cfg, library_path=str(lib_path)
)
return (cfg.name, cfg, out, True)
# Fast path: previous codegen already failed (no .hpp generated)
if out.exists() and not (out / "fmha_python_dispatch.hpp").exists():
err_file = out / "_codegen_err.txt"
if err_file.exists():
results[cfg.name] = FmhaSetupResult(
success=False, config=cfg, error="Codegen failed (cached)"
)
return (cfg.name, cfg, out, False)
out.mkdir(parents=True, exist_ok=True)
# Check if codegen was already done (has .hpp but no .so yet)
if (out / "fmha_python_dispatch.hpp").exists():
return (cfg.name, cfg, out, True)
if cfg.family == "bwd_dq_dk_dv":
dot = _make_bwd_dot_do_o_config(cfg)
config_json_str = json.dumps(
[
json.loads(dot.to_codegen_json()),
json.loads(cfg.to_codegen_json()),
]
)
elif cfg.family == "fwd_splitkv":
comb = _make_splitkv_combine_config(cfg)
config_json_str = json.dumps(
[
json.loads(cfg.to_codegen_json()),
json.loads(comb.to_codegen_json()),
]
)
else:
config_json_str = cfg.to_codegen_json()
err_file = out / "_codegen_err.txt"
with open(err_file, "w") as ef:
rc = subprocess.call(
[
sys.executable,
str(codegen_dir / "fmha" / "generate_fallback.py"),
"--output-dir",
str(out),
"--gpu-target",
cfg.gfx_arch,
"--config-json",
config_json_str,
],
stdout=subprocess.DEVNULL,
stderr=ef,
cwd=str(codegen_dir),
)
ok = rc == 0 and (out / "fmha_python_dispatch.hpp").exists()
if not ok:
err_msg = err_file.read_text()[:200] if err_file.exists() else "unknown"
results[cfg.name] = FmhaSetupResult(
success=False, config=cfg, error=f"Codegen failed: {err_msg}"
)
return (cfg.name, cfg, out, ok)
codegen_results = []
for i, cfg in enumerate(configs):
codegen_results.append(_codegen(cfg))
if progress_callback:
progress_callback("codegen", i + 1, len(configs))
# --- Stage 2: Collect ALL compile jobs, run in one pool ---
# Use bwd family flag to get the superset of all flags (includes BWD-specific defines)
base_flags = fmha_compile_flags(arch, hipcc, family="bwd")
compile_jobs = [] # (cmd, obj_path, kernel_name, label)
config_dirs: dict[str, tuple[FmhaKernelConfig, Path]] = {}
for name, cfg, out, ok in codegen_results:
if not ok or name in results:
continue
config_dirs[name] = (cfg, out)
for cpp in out.glob("fmha_*.cpp"):
obj = cpp.with_suffix(".o")
if not obj.exists():
compile_jobs.append(
(base_flags + [str(cpp), "-o", str(obj)], str(obj), name, "kernel")
)
ctypes_obj = out / "fmha_ctypes_lib.o"
if not ctypes_obj.exists():
dispatch = out / "fmha_python_dispatch.hpp"
compile_jobs.append(
(
base_flags
+ [
f"-I{out}",
f"-I{out / 'dispatcher_wrappers'}",
f"-include{dispatch}",
f'-DGFX_ARCH="{arch}"',
str(ctypes_src),
"-o",
str(ctypes_obj),
],
str(ctypes_obj),
name,
"ctypes",
)
)
failed_names: set = set()
if compile_jobs:
_own_pool = None
_pool = executor
if _pool is None:
workers = max_workers or min(len(compile_jobs), os.cpu_count() or 4)
_own_pool = ProcessPoolExecutor(max_workers=workers)
_pool = _own_pool
try:
done_count = 0
total_jobs = len(compile_jobs)
for name, ok, err in _pool.map(_run_compile_job, compile_jobs):
done_count += 1
if progress_callback:
progress_callback("compile", done_count, total_jobs)
if not ok:
failed_names.add(name)
if name not in results:
cfg, _ = config_dirs[name]
results[name] = FmhaSetupResult(
success=False, config=cfg, error=f"Compile: {err}"
)
finally:
if _own_pool is not None:
_own_pool.shutdown(wait=True)
# --- Stage 3: Link (no GPU access -- runner loading deferred to caller) ---
def _link(item):
name, (cfg, out) = item
if name in failed_names or name in results:
return
objs = list(out.glob("*.o"))
lib_path = out / f"libdispatcher_fmha_{name}.so"
if not lib_path.exists():
r = subprocess.run(
[
hipcc,
"-shared",
"-fPIC",
*[str(o) for o in objs],
str(static_lib),
"-o",
str(lib_path),
],
capture_output=True,
text=True,
)
if r.returncode != 0:
results[name] = FmhaSetupResult(
success=False, config=cfg, error=f"Link: {r.stderr[:200]}"
)
return
results[name] = FmhaSetupResult(
success=True, config=cfg, library_path=str(lib_path)
)
for item in config_dirs.items():
_link(item)
# Return in original order
return [
results.get(c.name, FmhaSetupResult(success=False, config=c, error="skipped"))
for c in configs
]
# =============================================================================
# Registry (mirrors ctypes_utils.Registry)
# =============================================================================
class FmhaRegistry:
"""Kernel registry with parallel JIT build support."""
def __init__(self, name: str = "fmha"):
self._name = name
self._kernels: List[FmhaKernelConfig] = []
def register_kernel(self, config: FmhaKernelConfig):
self._kernels.append(config)
def __len__(self):
return len(self._kernels)
def build(
self,
verbose: bool = False,
max_workers: Optional[int] = None,
) -> List[FmhaSetupResult]:
return setup_multiple_fmha_dispatchers(
self._kernels,
verbose=verbose,
max_workers=max_workers,
)
# =============================================================================
# Validator (mirrors ctypes_utils.Validator)
# =============================================================================
class FmhaValidator:
"""Validates FMHA GPU output against a reference.
Usage:
validator = FmhaValidator(rtol=1e-2, atol=1e-2)
ok, max_abs, max_rel = validator.check(gpu_output, cpu_reference)
"""
def __init__(self, rtol: float = 1e-2, atol: float = 1e-2):
self.rtol = rtol
self.atol = atol
def check(
self, output: np.ndarray, reference: np.ndarray
) -> Tuple[bool, float, float]:
"""Check output against reference.
Returns:
(is_valid, max_abs_error, max_rel_error)
"""
out_f32 = output.astype(np.float32)
ref_f32 = reference.astype(np.float32)
diff = np.abs(out_f32 - ref_f32)
max_abs = float(diff.max())
max_rel = float((diff / (np.abs(ref_f32) + 1e-6)).max())
ok = bool(np.allclose(out_f32, ref_f32, atol=self.atol, rtol=self.rtol))
return ok, max_abs, max_rel
# =============================================================================
# KernelSpec + spec_to_config (mirrors ctypes_utils.KernelSpec)
# =============================================================================
@dataclass
class FmhaKernelSpec:
"""High-level kernel specification for easy declaration.
Mirrors GEMM's KernelSpec: specify name + key dimensions, get a
full FmhaKernelConfig via spec_to_config().
"""
name: str
hdim: int = 128
pipeline: str = "qr_async"
# Stage 0 tile (Q*K^T)
tile_m0: int = 128
tile_n0: int = 128
tile_k0: int = 32
def spec_to_config(
spec: FmhaKernelSpec, dtype: str = "fp16", arch: str = "gfx950"
) -> FmhaKernelConfig:
"""Convert a high-level FmhaKernelSpec to a full FmhaKernelConfig."""
hdim = spec.hdim
return FmhaKernelConfig(
data_type=dtype,
hdim_q=hdim,
hdim_v=hdim,
pipeline=spec.pipeline,
tile_m0=spec.tile_m0,
tile_n0=spec.tile_n0,
tile_k0=spec.tile_k0,
tile_n1=hdim,
tile_k1=spec.tile_k0,
tile_k0max=hdim,
gfx_arch=arch,
)
# =============================================================================
# Split-K heuristic (from fmhaarch.md Section 9.5)
# =============================================================================