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
426 lines
15 KiB
Python
Executable File
426 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# numeric_attention.py -- Attention (FMHA) lane of the L6 numeric harness.
|
|
#
|
|
# Extracted from platform/tests/instances/differential/numeric.py so that
|
|
# library-layer tests can run the attention numeric lane without importing
|
|
# platform-internal attention kernel builders from platform code.
|
|
#
|
|
# Run:
|
|
# python library/tests/differential/numeric_attention.py [--arch gfx950]
|
|
#
|
|
# Needs GPU access (HIP). Build/compile (comgr) does not need GPU.
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
import sys
|
|
import tempfile
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
TMP = Path(tempfile.gettempdir()) / "rocke_numeric_attn"
|
|
TMP.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Shared infra (duplicated from numeric.py; keep in sync)
|
|
# NumericResult and Tol are small value types — duplication is intentional
|
|
# since platform/tests/instances/differential/numeric.py is a standalone
|
|
# script, not an importable package.
|
|
# ---------------------------------------------------------------------
|
|
@dataclass(frozen=True)
|
|
class Tol:
|
|
rtol: float
|
|
atol: float
|
|
|
|
|
|
@dataclass
|
|
class NumericResult:
|
|
family: str
|
|
name: str
|
|
status: str # GREEN | DRIFT | REJECTED | BUILD_FAIL | LAUNCH_FAIL
|
|
dtype: str = ""
|
|
shape: Tuple[int, ...] = ()
|
|
max_abs_diff: float = 0.0
|
|
max_rel_diff: float = 0.0
|
|
rtol: float = 0.0
|
|
atol: float = 0.0
|
|
margin: float = 0.0 # worst (|d| - (atol+rtol|ref|)); <=0 => pass
|
|
detail: str = ""
|
|
extra: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
def passed(self) -> bool:
|
|
return self.status == "GREEN"
|
|
|
|
|
|
def _np_dtype(dtype: str):
|
|
"""Map a spec dtype key to a numpy host dtype.
|
|
|
|
Only fp16/fp32 are supported torch-free: numpy has no native bfloat16 and
|
|
this harness does not pull in ml_dtypes. Every current ATTN_CONFIG is f16,
|
|
so bf16 is a clean, explicit error rather than dead scaffolding -- add a
|
|
real bf16 host encoding here if a bf16 config is ever introduced.
|
|
"""
|
|
import numpy as np
|
|
|
|
try:
|
|
return {"fp16": np.float16, "fp32": np.float32}[dtype]
|
|
except KeyError:
|
|
raise NotImplementedError(
|
|
f"dtype {dtype!r} has no torch-free numpy host encoding "
|
|
"(numpy lacks bfloat16 and this harness avoids ml_dtypes); "
|
|
"all current ATTN_CONFIGS are f16"
|
|
) from None
|
|
|
|
|
|
def _compare(out, ref_f32, tol: Tol) -> Tuple[float, float, float]:
|
|
"""Return (max_abs_diff, max_rel_diff, worst allclose margin)."""
|
|
import numpy as np
|
|
|
|
out_f32 = out.astype(np.float32)
|
|
diff = np.abs(out_f32 - ref_f32)
|
|
max_abs = float(diff.max())
|
|
denom = np.clip(np.abs(ref_f32), 1e-12, None)
|
|
max_rel = float((diff / denom).max())
|
|
allowed = tol.atol + tol.rtol * np.abs(ref_f32)
|
|
margin = float((diff - allowed).max())
|
|
return max_abs, max_rel, margin
|
|
|
|
|
|
# map spec dtype -> TOL-table key (mirrors _ELEM_TOL_KEY in numeric.py)
|
|
_ELEM_TOL_KEY = {"f16": "fp16", "bf16": "bf16"}
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Attention lane (FMHA forward, unified tiled MFMA body)
|
|
# ---------------------------------------------------------------------
|
|
# Builds kernels.common.fmha_mfma.build_fmha_fwd_mfma through the
|
|
# *comgr* (LLVM-IR) path -- the same Python engine the rest of the L6
|
|
# numeric harness uses -- and compares against a dense fp32
|
|
# softmax-attention reference. The ABI mirrors
|
|
# examples/common/fmha_fwd_verify_hip.py:
|
|
#
|
|
# args: (Q, K, V, Out : ptr, scale_log2 : f32, Sq, Sk : i32,
|
|
# stride_q_token, stride_q_head, stride_k_token, stride_k_head,
|
|
# stride_v_token, stride_v_head, stride_o_token, stride_o_head : i32)
|
|
# layout: Q (B,Sq,Hq,D) / K,V (B,Sk,Hk,D) / Out (B,Sq,Hq,D) row-major;
|
|
# the batch axis is folded in by the grid z dim (block_id_z).
|
|
# grid : fmha_fwd_mfma_grid(spec, batch=B); block = (wave_size,1,1).
|
|
#
|
|
# scale_log2 = (1/sqrt(D)) * log2(e): the kernel does the softmax in
|
|
# base-2 (exp2), so the host pre-scales the QK scale into log2 space.
|
|
@dataclass(frozen=True)
|
|
class AttnCfg:
|
|
name: str
|
|
batch: int
|
|
heads: int
|
|
kv_heads: int # == heads -> MHA
|
|
seqlen_q: int
|
|
seqlen_k: int
|
|
head_size: int
|
|
dtype: str = "f16"
|
|
causal: bool = False
|
|
|
|
|
|
ATTN_CONFIGS: List[AttnCfg] = [
|
|
AttnCfg("fmha_mha_b2_h4_s64_d64", 2, 4, 4, 64, 64, 64),
|
|
AttnCfg("fmha_mha_b1_h8_s128_d64", 1, 8, 8, 128, 128, 64),
|
|
AttnCfg("fmha_causal_b2_h4_s64_d64", 2, 4, 4, 64, 64, 64, causal=True),
|
|
AttnCfg("fmha_gqa_b1_h8kv2_s64_d64", 1, 8, 2, 64, 64, 64),
|
|
]
|
|
|
|
# softmax-attention carries an exp + a length-Sk normalization on top of two
|
|
# matmuls; the accumulation order differs from the dense reference, so use the
|
|
# attention parity gate's tolerance (2e-2), matching the example harness.
|
|
_ATTN_TOL = Tol(rtol=0.0, atol=2e-2)
|
|
|
|
|
|
def run_attn_config(cfg: AttnCfg, arch: str = "gfx950") -> NumericResult:
|
|
import math as _m
|
|
|
|
import numpy as np
|
|
|
|
from rocke.core.arch import ArchTarget
|
|
from rocke.helpers.compile import compile_kernel
|
|
from rocke.helpers.spec import SignatureBuilder
|
|
from kernels import FmhaCommonSpec, FmhaShape
|
|
from kernels.common.fmha_mfma import (
|
|
FmhaMfmaSpec,
|
|
build_fmha_fwd_mfma,
|
|
fmha_fwd_mfma_grid,
|
|
is_valid_spec,
|
|
)
|
|
from rocke.numeric.references import dense_attention_reference
|
|
from rocke.runtime.host_buffers import as_u8_buffer
|
|
from rocke.runtime.hip_module import Runtime
|
|
from rocke.runtime.launcher import DeviceMem, KernelLauncher, LaunchConfig
|
|
|
|
tol_key = _ELEM_TOL_KEY.get(cfg.dtype, cfg.dtype)
|
|
res = NumericResult(
|
|
family="attention",
|
|
name=cfg.name,
|
|
status="GREEN",
|
|
dtype=tol_key,
|
|
shape=(cfg.batch, cfg.seqlen_q, cfg.heads, cfg.head_size),
|
|
)
|
|
tol = _ATTN_TOL
|
|
res.rtol, res.atol = tol.rtol, tol.atol
|
|
|
|
common = FmhaCommonSpec(
|
|
FmhaShape(
|
|
head_size=cfg.head_size,
|
|
num_query_heads=cfg.heads,
|
|
num_kv_heads=cfg.kv_heads,
|
|
block_size_q=16,
|
|
block_size_k=64,
|
|
),
|
|
dtype=cfg.dtype,
|
|
mask_mode="causal" if cfg.causal else "none",
|
|
)
|
|
spec = FmhaMfmaSpec(
|
|
common=common,
|
|
seqlen_q=cfg.seqlen_q,
|
|
seqlen_k=cfg.seqlen_k,
|
|
name=f"rocke_fmha_num_{cfg.name}",
|
|
)
|
|
|
|
target = ArchTarget.from_gfx(arch)
|
|
try:
|
|
ok, why = is_valid_spec(spec, arch)
|
|
except Exception as e: # noqa: BLE001
|
|
res.status = "REJECTED"
|
|
res.detail = f"validate raised: {e}"
|
|
return res
|
|
if not ok:
|
|
res.status = "REJECTED"
|
|
res.detail = f"is_valid_spec: {why}"
|
|
return res
|
|
|
|
try:
|
|
kern = build_fmha_fwd_mfma(spec, arch=arch)
|
|
art = compile_kernel(kern, arch=arch)
|
|
except Exception as e: # noqa: BLE001
|
|
res.status = "BUILD_FAIL"
|
|
res.detail = f"build/compile raised: {e}"
|
|
return res
|
|
res.extra["kernel_name"] = art.kernel_name
|
|
res.extra["hsaco_bytes"] = art.hsaco_bytes
|
|
|
|
B, Hq, Hk, D = cfg.batch, cfg.heads, cfg.kv_heads, cfg.head_size
|
|
Sq, Sk = cfg.seqlen_q, cfg.seqlen_k
|
|
np_dtype = _np_dtype(tol_key)
|
|
rng = np.random.default_rng(0xA11E)
|
|
Q = (rng.standard_normal((B, Sq, Hq, D)) * 0.3).astype(np_dtype)
|
|
K = (rng.standard_normal((B, Sk, Hk, D)) * 0.3).astype(np_dtype)
|
|
V = (rng.standard_normal((B, Sk, Hk, D)) * 0.3).astype(np_dtype)
|
|
Out = np.zeros((B, Sq, Hq, D), dtype=np_dtype)
|
|
|
|
scale_log2 = float(1.0 / _m.sqrt(D) * _m.log2(_m.e))
|
|
sig = (
|
|
SignatureBuilder()
|
|
.ptr("Q", cfg.dtype)
|
|
.ptr("K", cfg.dtype)
|
|
.ptr("V", cfg.dtype)
|
|
.ptr("Out", cfg.dtype)
|
|
.scalar("scale", "f32")
|
|
.scalar("Sq", "i32")
|
|
.scalar("Sk", "i32")
|
|
.scalar("sqt", "i32")
|
|
.scalar("sqh", "i32")
|
|
.scalar("skt", "i32")
|
|
.scalar("skh", "i32")
|
|
.scalar("svt", "i32")
|
|
.scalar("svh", "i32")
|
|
.scalar("sot", "i32")
|
|
.scalar("soh", "i32")
|
|
.build()
|
|
)
|
|
grid = fmha_fwd_mfma_grid(spec, batch=B)
|
|
block = (target.wave_size, 1, 1)
|
|
|
|
# Torch-free device I/O: DeviceMem RAII buffers (freed when they leave
|
|
# scope), numpy host arrays uploaded as raw bytes, and the DeviceMem
|
|
# objects passed straight into the launcher values (pack_args reads their
|
|
# .ptr()). fence=True makes the launch stream-synchronize before returning,
|
|
# so the d2h readback observes a finished kernel.
|
|
rt = Runtime()
|
|
try:
|
|
q_dev = DeviceMem(Q.nbytes)
|
|
k_dev = DeviceMem(K.nbytes)
|
|
v_dev = DeviceMem(V.nbytes)
|
|
o_dev = DeviceMem(Out.nbytes)
|
|
rt.memcpy_h2d(q_dev.ptr(), as_u8_buffer(Q), Q.nbytes)
|
|
rt.memcpy_h2d(k_dev.ptr(), as_u8_buffer(K), K.nbytes)
|
|
rt.memcpy_h2d(v_dev.ptr(), as_u8_buffer(V), V.nbytes)
|
|
rt.memset(o_dev.ptr(), 0, Out.nbytes)
|
|
values = {
|
|
"Out": o_dev,
|
|
"Q": q_dev,
|
|
"K": k_dev,
|
|
"V": v_dev,
|
|
"scale": scale_log2,
|
|
"Sq": Sq,
|
|
"Sk": Sk,
|
|
"sqt": Hq * D,
|
|
"sqh": D,
|
|
"skt": Hk * D,
|
|
"skh": D,
|
|
"svt": Hk * D,
|
|
"svh": D,
|
|
"sot": Hq * D,
|
|
"soh": D,
|
|
}
|
|
launcher = KernelLauncher(
|
|
hsaco=art.hsaco, kernel_name=art.kernel_name, signature=sig
|
|
)
|
|
launcher(values, config=LaunchConfig(grid=grid, block=block, fence=True))
|
|
rt.memcpy_d2h(as_u8_buffer(Out), o_dev.ptr(), Out.nbytes)
|
|
except Exception as e: # noqa: BLE001
|
|
res.status = "LAUNCH_FAIL"
|
|
res.detail = f"device I/O or launch raised: {e}"
|
|
return res
|
|
|
|
# Reference per batch (expand KV heads for GQA). out_dtype=None keeps the
|
|
# reference in fp32 -- the full-precision compare this harness has always
|
|
# used (do not truncate the reference to the kernel dtype).
|
|
ref = np.empty((B, Sq, Hq, D), dtype=np.float32)
|
|
for bi in range(B):
|
|
if Hk != Hq:
|
|
rep = Hq // Hk
|
|
Kb = np.repeat(K[bi], rep, axis=1)
|
|
Vb = np.repeat(V[bi], rep, axis=1)
|
|
else:
|
|
Kb, Vb = K[bi], V[bi]
|
|
ref[bi] = dense_attention_reference(Q[bi], Kb, Vb, causal=cfg.causal)
|
|
|
|
max_abs, max_rel, margin = _compare(Out, ref, tol)
|
|
res.max_abs_diff = max_abs
|
|
res.max_rel_diff = max_rel
|
|
res.margin = margin
|
|
res.status = "GREEN" if margin <= 0.0 and math.isfinite(margin) else "DRIFT"
|
|
res.detail = (
|
|
f"causal={cfg.causal} grid={grid} block={block} "
|
|
f"max_abs={max_abs:.3e} max_rel={max_rel:.3e} "
|
|
f"atol={tol.atol:.0e} margin={margin:.3e}"
|
|
)
|
|
return res
|
|
|
|
|
|
# ---------------------------------------------------------------------
|
|
# Driver
|
|
# ---------------------------------------------------------------------
|
|
def _check_gpu() -> Optional[str]:
|
|
"""Gate this harness on a visible HIP device via the torch-free probe.
|
|
|
|
The harness is torch-free: data generation is numpy and device I/O goes
|
|
through DeviceMem + the HIP runtime, so it gates on the resource it actually
|
|
uses -- ``rocke.runtime.hip_module.get_device_arch()``. This is the same
|
|
probe the test *gates* use. (It inverts the old torch-era rationale: when
|
|
the driver drove ``torch.cuda`` in-process, probing HIP first would dlopen
|
|
the system HIP before torch bound its own; with torch gone there is no such
|
|
ordering hazard and the HIP probe is the correct, direct check.)
|
|
"""
|
|
from rocke.runtime.hip_module import get_device_arch
|
|
|
|
if get_device_arch(0) is None:
|
|
return (
|
|
"no HIP device visible (get_device_arch() is None) -- on a GPU box, "
|
|
"ensure the user is in the render/video groups with access to "
|
|
"/dev/kfd and /dev/dri (e.g. run under sudo -E)"
|
|
)
|
|
return None
|
|
|
|
|
|
def run_all(arch: str = "gfx950", only: str = "") -> List[NumericResult]:
|
|
results: List[NumericResult] = []
|
|
subs = [s for s in only.split(",") if s]
|
|
|
|
def want(family: str, name: str) -> bool:
|
|
if not subs:
|
|
return True
|
|
return any(s in family or s in name for s in subs)
|
|
|
|
for cfg in ATTN_CONFIGS:
|
|
if want("attention", cfg.name):
|
|
results.append(run_attn_config(cfg, arch=arch))
|
|
return results
|
|
|
|
|
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--arch", default="gfx950")
|
|
ap.add_argument("--only", default="", help="comma-separated family/name substrings")
|
|
ap.add_argument("--json", default=str(TMP / "numeric_attn_dashboard.json"))
|
|
args = ap.parse_args(argv)
|
|
|
|
gpu_err = _check_gpu()
|
|
if gpu_err is not None:
|
|
sys.stderr.write(f"GPU unavailable: {gpu_err}\n")
|
|
return 3
|
|
|
|
from rocke.runtime.hip_module import get_device_arch, get_device_name
|
|
|
|
dev_arch = get_device_arch(0)
|
|
dev_name = get_device_name(0) or "?"
|
|
print(f"L6 NUMERIC ATTN arch={args.arch} device={dev_arch} ({dev_name})")
|
|
results = run_all(arch=args.arch, only=args.only)
|
|
|
|
rows: List[Dict[str, Any]] = []
|
|
npass = nfail = nrej = nskip = nerr = 0
|
|
for r in results:
|
|
rows.append(
|
|
{
|
|
"family": r.family,
|
|
"name": r.name,
|
|
"status": r.status,
|
|
"dtype": r.dtype,
|
|
"shape": list(r.shape),
|
|
"max_abs_diff": r.max_abs_diff,
|
|
"max_rel_diff": r.max_rel_diff,
|
|
"rtol": r.rtol,
|
|
"atol": r.atol,
|
|
"margin": r.margin,
|
|
"detail": r.detail,
|
|
"extra": r.extra,
|
|
}
|
|
)
|
|
if r.status == "GREEN":
|
|
npass += 1
|
|
elif r.status == "DRIFT":
|
|
nfail += 1
|
|
elif r.status == "REJECTED":
|
|
nrej += 1
|
|
elif r.status == "SKIPPED":
|
|
nskip += 1
|
|
else:
|
|
nerr += 1
|
|
tag = r.status
|
|
line = f" {tag:11s} {r.family}/{r.name}"
|
|
if r.status in ("GREEN", "DRIFT"):
|
|
line += (
|
|
f" {r.dtype} {tuple(r.shape)} "
|
|
f"max_abs={r.max_abs_diff:.3e} max_rel={r.max_rel_diff:.3e} "
|
|
f"margin={r.margin:.3e} (rtol={r.rtol:.0e} atol={r.atol:.0e})"
|
|
)
|
|
elif r.detail:
|
|
line += f" {r.detail}"
|
|
print(line)
|
|
|
|
Path(args.json).write_text(json.dumps(rows, indent=2))
|
|
print(
|
|
f"\n=== L6 NUMERIC ATTN SUMMARY ===\n"
|
|
f" PASS={npass} FAIL={nfail} REJECTED={nrej} "
|
|
f"SKIPPED={nskip} ERROR={nerr}"
|
|
)
|
|
return 1 if (nfail or nerr) else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|