Files
TheRock/rocm-libraries/dnn-providers/hip-kernel-provider/rocke/library/kernels/common/fmha_appendkv.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

550 lines
17 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Append-KV kernel (CK Tile ``01_fmha`` appendkv parity).
Writes a new K / V token (or block of tokens) into a pre-allocated KV
cache at the position implied by the per-sequence ``seqlen_kv`` array;
the corresponding fwd attention kernel then reads the updated cache.
Optionally applies rotary embedding to K (the standard pattern for
LLaMA-style RoPE caches).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Tuple
from rocke.core.ir import F32, I32, IRBuilder, KernelDef, PtrType
from rocke.helpers.io import (
io_ir_type,
load_scalar_as_f32,
pack_f32_to,
store_scalar_from_f32,
vector_row_copy,
)
from rocke.helpers.rotary import (
RotarySpec,
apply_rotary_pair_f32,
load_cos_sin,
pair_indices,
)
from rocke.helpers.spec import SignatureBuilder, ceil_div_grid, kernel_name_join
from ._fmha_common import FmhaCommonSpec, validate_common_spec
# CK Tile's appendkv default policy reads 16 bytes at a time
# (``GetAlignmentK = 16 / sizeof(KDataType) = 8`` for f16/bf16; see
# ``include/ck_tile/ops/fmha/pipeline/block_fmha_fwd_appendkv_pipeline_default_policy.hpp``).
# The corresponding scalar load chain we replace below issues ``H`` 2-byte
# loads per thread; the vector path collapses that to ``H/_VEC`` 16-byte
# loads, the same shape ``buffer_load_dwordx4`` lowers to in the C++
# reference.
_VEC = 8 # 16-byte vector for f16 / bf16
__all__ = [
"FmhaAppendKvSpec",
"build_fmha_fwd_appendkv",
"fmha_appendkv_grid",
"fmha_appendkv_signature",
"is_valid_spec",
]
@dataclass(frozen=True)
class FmhaAppendKvSpec:
common: FmhaCommonSpec
batch: int
rotary: RotarySpec | None = None
block_size: int = 256
name: str = "rocke_fmha_appendkv"
def kernel_name(self) -> str:
s = self.common.shape
rot = "rope" if self.rotary is not None else "norope"
return kernel_name_join(
self.name,
f"H{s.head_size}",
f"HK{s.num_kv_heads}",
self.common.dtype,
f"B{self.batch}",
rot,
f"b{self.block_size}",
)
def is_valid_spec(spec: FmhaAppendKvSpec, arch: str = "gfx950") -> Tuple[bool, str]:
"""Return ``(ok, reason)`` for one append-KV config on ``arch``.
This kernel is a pure vectorised KV-cache scatter (optionally fused
with rotary): it issues no MFMA atoms and allocates no LDS, so the
only architecture fact that matters is the per-WG thread cap. It is
sourced from :class:`rocke.core.arch.ArchTarget` so the predicate
rejects an unknown arch (or an over-budget ``block_size``) with a
structured reason rather than failing later at lower/launch time.
The kernel builds identically on gfx942 and gfx950 (the f16 / bf16
vector load / store path is shared CDNA hardware), so gfx950
behavior is unchanged.
"""
from rocke.core.arch import ArchTarget
try:
target = ArchTarget.from_gfx(arch)
except KeyError as e:
return False, str(e)
ok, why = validate_common_spec(spec.common)
if not ok:
return False, why
if spec.batch <= 0:
return False, f"batch must be > 0 (got {spec.batch})"
if spec.rotary is not None and spec.rotary.head_size != spec.common.shape.head_size:
return False, (
f"rotary head_size ({spec.rotary.head_size}) != "
f"common head_size ({spec.common.shape.head_size})"
)
if spec.block_size > target.max_threads_per_block:
return False, (
f"block_size {spec.block_size} > max_threads_per_block "
f"{target.max_threads_per_block} on {arch}"
)
return True, "ok"
def build_fmha_fwd_appendkv(spec: FmhaAppendKvSpec, arch: str = "gfx950") -> KernelDef:
"""Append K / V tokens to a pre-allocated KV cache.
This is a pure vectorised KV-cache scatter (optionally fused with
rotary): it emits no MFMA atoms and allocates no LDS, so the f16 /
bf16 vector load / store path is identical on gfx942 and gfx950.
``arch`` is threaded only through :func:`is_valid_spec` (per-WG
thread-cap check); the emitted IR is byte-identical across arches.
"""
ok, why = is_valid_spec(spec, arch)
if not ok:
raise ValueError(f"invalid fmha_appendkv spec: {why}")
s = spec.common
H = s.shape.head_size
BS = spec.block_size
dtype = s.dtype
ty = io_ir_type(dtype)
b = IRBuilder(spec.kernel_name())
b.kernel.attrs["max_workgroup_size"] = BS
K_new = b.param(
"K_new", PtrType(ty, "global"), noalias=True, readonly=True, align=16
)
V_new = b.param(
"V_new", PtrType(ty, "global"), noalias=True, readonly=True, align=16
)
K_cache = b.param("K_cache", PtrType(ty, "global"), noalias=True, align=16)
V_cache = b.param("V_cache", PtrType(ty, "global"), noalias=True, align=16)
seqlen_kv = b.param(
"seqlen_kv",
PtrType(I32, "global"),
noalias=True,
readonly=True,
align=4,
)
cu_seqlens_new = b.param(
"cu_seqlens_new",
PtrType(I32, "global"),
noalias=True,
readonly=True,
align=4,
)
cos_table = (
b.param("cos_table", PtrType(F32, "global"), readonly=True, align=4)
if spec.rotary is not None
else None
)
sin_table = (
b.param("sin_table", PtrType(F32, "global"), readonly=True, align=4)
if spec.rotary is not None
else None
)
total_new_q = b.param("total_new_q", I32)
_batch = b.param("batch", I32) # noqa: F841 - ABI
stride_in_token = b.param("stride_in_token", I32)
stride_in_head = b.param("stride_in_head", I32)
stride_cache_token = b.param("stride_cache_token", I32)
stride_cache_head = b.param("stride_cache_head", I32)
tid = b.thread_id_x()
bid = b.block_id_x()
kv_head_idx = b.block_id_y()
new_token = b.add(b.mul(bid, b.const_i32(BS)), tid)
# Bounds-check: the workgroup width is ``block_size`` but the
# last block may have only a partial slab of tokens. Threads
# past ``total_new_q`` must skip the body or they'd read OOB
# metadata and scatter garbage into the cache.
in_bounds = b.cmp_lt(new_token, total_new_q)
with b.scf_if(in_bounds):
_appendkv_body(
b,
spec=spec,
H=H,
dtype=dtype,
new_token=new_token,
kv_head_idx=kv_head_idx,
K_new=K_new,
V_new=V_new,
K_cache=K_cache,
V_cache=V_cache,
seqlen_kv=seqlen_kv,
cu_seqlens_new=cu_seqlens_new,
cos_table=cos_table,
sin_table=sin_table,
stride_in_token=stride_in_token,
stride_in_head=stride_in_head,
stride_cache_token=stride_cache_token,
stride_cache_head=stride_cache_head,
)
b.ret()
return b.kernel
def _appendkv_body(
b,
*,
spec,
H,
dtype,
new_token,
kv_head_idx,
K_new,
V_new,
K_cache,
V_cache,
seqlen_kv,
cu_seqlens_new,
cos_table,
sin_table,
stride_in_token,
stride_in_head,
stride_cache_token,
stride_cache_head,
):
"""The per-thread appendkv body extracted so the bounds-check
wrapper above stays compact.
Address math + sequence lookup runs first; the actual K / V
copy is delegated to the vectorised helpers below so the body
stays readable and the scalar / vector choice lives in one spot.
"""
# Per-token sequence lookup. ``new_token`` is wave-non-uniform so
# the scan result is *per-thread* (no readfirstlane). Python-
# unrolling the loop (``spec.batch`` is compile-time constant)
# drops the scf.for overhead — the compiler emits an unrolled
# chain of ``v_cmp / v_cndmask`` instead of a back-edge loop.
# This mirrors the CK Tile reference where ``i_batch`` is on
# ``blockIdx.z`` and the per-batch metadata is one
# ``buffer_load_dword`` per CTA.
seq = b.const_i32(0)
for i in range(spec.batch):
cuq_next = b.global_load_i32(cu_seqlens_new, b.const_i32(i + 1))
is_in_seq = b.cmp_lt(new_token, cuq_next)
seq = b.select(is_in_seq, seq, b.add(seq, b.const_i32(1)))
seq_idx = seq
cu_base = b.global_load_i32(cu_seqlens_new, seq_idx)
local_new = b.sub(new_token, cu_base)
seqlen_cur = b.global_load_i32(seqlen_kv, seq_idx)
dst_pos = b.add(seqlen_cur, local_new)
in_row_base = b.add(
b.mul(new_token, stride_in_token), b.mul(kv_head_idx, stride_in_head)
)
cache_row_base = b.add(
b.mul(dst_pos, stride_cache_token), b.mul(kv_head_idx, stride_cache_head)
)
_appendkv_copy_k(
b,
spec=spec,
H=H,
dtype=dtype,
K_new=K_new,
K_cache=K_cache,
cos_table=cos_table,
sin_table=sin_table,
dst_pos=dst_pos,
in_row_base=in_row_base,
cache_row_base=cache_row_base,
)
_appendkv_copy_v(
b,
H=H,
dtype=dtype,
V_new=V_new,
V_cache=V_cache,
in_row_base=in_row_base,
cache_row_base=cache_row_base,
)
def _copy_row_vec(
b,
*,
H,
dtype,
src_ptr,
dst_ptr,
src_row_base,
dst_row_base,
):
"""Vectorised row copy along the head dim.
Replaces the scalar ``for d in range(H): load + store`` with
``H // _VEC`` 16-byte vector copies (each
``buffer_load_dwordx4`` + ``buffer_store_dwordx4`` on AMDGPU);
the residual ``H % _VEC`` tail falls back to scalar so any
future head_size we add can still go through this path.
Matches CK Tile's ``store_tile(k_dram_block_window, knew_tile)``
semantics in
``include/ck_tile/ops/fmha/pipeline/block_fmha_fwd_appendkv_pipeline.hpp``:
one head_dim row is moved with the widest aligned vector the
pointer's ``align=16`` declaration supports.
Delegates to :func:`rocke.helpers.io.vector_row_copy` (whose
docstring names this function as its promotion source); the helper
emits the identical ``H // _VEC`` vector copies + scalar tail. The
``vec_bytes=16`` argument maps to ``_VEC = 16 / 2`` for f16 / bf16.
"""
vector_row_copy(
b,
src=src_ptr,
dst=dst_ptr,
src_base=src_row_base,
dst_base=dst_row_base,
H=H,
dtype=dtype,
vec_bytes=_VEC * 2,
)
def _appendkv_copy_k(
b,
*,
spec,
H,
dtype,
K_new,
K_cache,
cos_table,
sin_table,
dst_pos,
in_row_base,
cache_row_base,
):
"""K-cache copy, optionally fused with rotary embedding.
No-rotary path is a straight vector copy
(:func:`_copy_row_vec`). Rotary paths read the K row with vec
loads, apply the 2x2 rotation per pair on f32, then pack +
vector-store. CK Tile's
``BlockRotaryEmbedding<RotaryEnum>::apply`` (see
``include/ck_tile/ops/fmha/block/block_rotary_embedding.hpp``)
does the same: load a knew tile vectorised, rotate, store back
vectorised.
"""
if spec.rotary is None:
_copy_row_vec(
b,
H=H,
dtype=dtype,
src_ptr=K_new,
dst_ptr=K_cache,
src_row_base=in_row_base,
dst_row_base=cache_row_base,
)
return
ty = io_ir_type(dtype)
elem_bytes = 2
layout = spec.rotary.layout
pair_count = spec.rotary.pair_count
if layout == "half" and (H // 2) % _VEC == 0:
# LLaMA-2/3/Qwen style: pair (i, i + H/2). The lo halves are
# contiguous in [0, H/2), the hi halves are contiguous in
# [H/2, H), so each side gets its own dwordx4 load + store.
half = H // 2
n_chunks = half // _VEC
for c in range(n_chunks):
d_lo = c * _VEC
d_hi = half + c * _VEC
lo_vec = b.global_load_vN(
K_new,
b.add(in_row_base, b.const_i32(d_lo)),
ty,
_VEC,
align=_VEC * elem_bytes,
)
hi_vec = b.global_load_vN(
K_new,
b.add(in_row_base, b.const_i32(d_hi)),
ty,
_VEC,
align=_VEC * elem_bytes,
)
out_lo_f32 = []
out_hi_f32 = []
for j in range(_VEC):
pair = c * _VEC + j
cos_v, sin_v = load_cos_sin(
b,
cos_table,
sin_table,
token_pos=dst_pos,
pair_idx=b.const_i32(pair),
spec=spec.rotary,
)
lo_f32 = b.cast_to_f32(b.vec_extract(lo_vec, j))
hi_f32 = b.cast_to_f32(b.vec_extract(hi_vec, j))
new_lo, new_hi = apply_rotary_pair_f32(b, lo_f32, hi_f32, cos_v, sin_v)
out_lo_f32.append(new_lo)
out_hi_f32.append(new_hi)
lo_pack = pack_f32_to(b, out_lo_f32, dtype=dtype)
hi_pack = pack_f32_to(b, out_hi_f32, dtype=dtype)
b.global_store_vN(
K_cache,
b.add(cache_row_base, b.const_i32(d_lo)),
lo_pack,
_VEC,
align=_VEC * elem_bytes,
)
b.global_store_vN(
K_cache,
b.add(cache_row_base, b.const_i32(d_hi)),
hi_pack,
_VEC,
align=_VEC * elem_bytes,
)
return
if layout == "interleaved" and H % _VEC == 0 and _VEC % 2 == 0:
# GPT-J / LLaMA-1 style: pair (2i, 2i+1). Adjacent in the head
# dim so ``_VEC = 8`` covers 4 pairs per chunk; we load the
# chunk once, rotate each pair on f32, then pack + store the
# rotated chunk (same dwordx4 store the no-rotary path uses).
pairs_per_chunk = _VEC // 2
n_chunks = H // _VEC
for c in range(n_chunks):
d = c * _VEC
vec = b.global_load_vN(
K_new,
b.add(in_row_base, b.const_i32(d)),
ty,
_VEC,
align=_VEC * elem_bytes,
)
out_f32 = []
for j in range(pairs_per_chunk):
pair = c * pairs_per_chunk + j
cos_v, sin_v = load_cos_sin(
b,
cos_table,
sin_table,
token_pos=dst_pos,
pair_idx=b.const_i32(pair),
spec=spec.rotary,
)
lo_f32 = b.cast_to_f32(b.vec_extract(vec, 2 * j))
hi_f32 = b.cast_to_f32(b.vec_extract(vec, 2 * j + 1))
new_lo, new_hi = apply_rotary_pair_f32(b, lo_f32, hi_f32, cos_v, sin_v)
out_f32.append(new_lo)
out_f32.append(new_hi)
packed = pack_f32_to(b, out_f32, dtype=dtype)
b.global_store_vN(
K_cache,
b.add(cache_row_base, b.const_i32(d)),
packed,
_VEC,
align=_VEC * elem_bytes,
)
return
# Catch-all fallback: keep the original scalar pair loop so any
# exotic head_size that doesn't align to _VEC still compiles.
for pair in range(pair_count):
lo_d, hi_d = pair_indices(spec.rotary, pair)
k_lo = load_scalar_as_f32(
b, K_new, b.add(in_row_base, b.const_i32(lo_d)), dtype=dtype
)
k_hi = load_scalar_as_f32(
b, K_new, b.add(in_row_base, b.const_i32(hi_d)), dtype=dtype
)
cos_v, sin_v = load_cos_sin(
b,
cos_table,
sin_table,
token_pos=dst_pos,
pair_idx=b.const_i32(pair),
spec=spec.rotary,
)
new_lo, new_hi = apply_rotary_pair_f32(b, k_lo, k_hi, cos_v, sin_v)
store_scalar_from_f32(
b, K_cache, b.add(cache_row_base, b.const_i32(lo_d)), new_lo, dtype=dtype
)
store_scalar_from_f32(
b, K_cache, b.add(cache_row_base, b.const_i32(hi_d)), new_hi, dtype=dtype
)
def _appendkv_copy_v(
b,
*,
H,
dtype,
V_new,
V_cache,
in_row_base,
cache_row_base,
):
"""V cache copy is always a plain row copy (no rotary on V)."""
_copy_row_vec(
b,
H=H,
dtype=dtype,
src_ptr=V_new,
dst_ptr=V_cache,
src_row_base=in_row_base,
dst_row_base=cache_row_base,
)
def fmha_appendkv_grid(
spec: FmhaAppendKvSpec, *, total_new_q: int
) -> Tuple[int, int, int]:
gx, _, _ = ceil_div_grid((total_new_q, spec.block_size))
return (gx, spec.common.shape.num_kv_heads, 1)
def fmha_appendkv_signature(spec: FmhaAppendKvSpec):
sig = (
SignatureBuilder()
.ptr("K_new", spec.common.dtype)
.ptr("V_new", spec.common.dtype)
.ptr("K_cache", spec.common.dtype)
.ptr("V_cache", spec.common.dtype)
.ptr("seqlen_kv", "i32")
.ptr("cu_seqlens_new", "i32")
)
if spec.rotary is not None:
sig = sig.ptr("cos_table", "f32").ptr("sin_table", "f32")
return (
sig.scalar("total_new_q", "i32")
.scalar("batch", "i32")
.scalar("stride_in_token", "i32")
.scalar("stride_in_head", "i32")
.scalar("stride_cache_token", "i32")
.scalar("stride_cache_head", "i32")
.build()
)