# Copyright (c) Advanced Micro Devices, Inc., or its affiliates. # SPDX-License-Identifier: MIT """Driver for the gfx1151 WMMA FMHA optimization campaign. Builds an :class:`~fmha_singlewave.SingleWaveCfg`, gates it on a numpy reference, times it with HIP events, and tallies the disassembly's memory/matmul instruction mix. Used both ad-hoc (``verify_and_time(cfg, shape)``) and as a sweep entry point. """ from __future__ import annotations import argparse import collections import ctypes import math import struct from dataclasses import dataclass from rocke.helpers import compile_kernel from rocke.runtime.hip_module import Runtime from rocke.runtime.launcher import time_launches from .bench_v_staging import _find_objdump, _ref_attention from .fmha_singlewave import SingleWaveCfg, build_wmma_fmha_singlewave, singlewave_grid @dataclass(frozen=True) class Shape: batch: int = 4 heads: int = 8 kv_heads: int = 0 seqlen_q: int = 512 seqlen_k: int = 512 head_size: int = 128 causal: bool = False @property def kvh(self): return self.kv_heads or self.heads def _mem_counts(hsaco: bytes, name: str, objdump): if objdump is None: return {} import os import subprocess import tempfile from pathlib import Path tmp = Path(tempfile.gettempdir()) / (name + ".hsaco") tmp.write_bytes(hsaco) try: out = subprocess.run( [objdump, "-d", str(tmp)], capture_output=True, text=True ).stdout finally: try: os.remove(tmp) except OSError: pass cats = ( ("wmma", lambda m: "wmma" in m), ("gld", lambda m: m.startswith("global_load")), ("gst", lambda m: m.startswith("global_store")), ("dsld", lambda m: m.startswith("ds_load")), ("dsst", lambda m: m.startswith("ds_store")), ("bperm", lambda m: m.startswith("ds_bpermute")), ) c = collections.Counter() total = 0 for line in out.splitlines(): s = line.strip() if not s.startswith(("s_", "v_", "ds_", "global_", "buffer_")): continue m = s.split()[0] # s_code_end is end-of-program padding the AMDGPU backend inserts to a # fixed boundary; it never issues, so excluding it keeps the static # instruction count a measure of real executed work (it otherwise shifts # with code-size alignment and pollutes A/B instruction-count parity). if m == "s_code_end": continue total += 1 for nm, pred in cats: if pred(m): c[nm] += 1 break res = {nm: c.get(nm, 0) for nm, _ in cats} res["instr"] = total return res def _resource_counts(hsaco: bytes): """Decode VGPR/SGPR/spill/LDS from the AMDGPU msgpack note (no readelf).""" raw = bytes(hsaco) def after(key): i = raw.find(key.encode()) if i < 0: return None j = i + len(key) b0 = raw[j] if b0 < 0x80: return b0 if b0 == 0xCC: return raw[j + 1] if b0 == 0xCD: return int.from_bytes(raw[j + 1 : j + 3], "big") if b0 == 0xCE: return int.from_bytes(raw[j + 1 : j + 5], "big") return None return { "vgpr": after(".vgpr_count"), "sgpr": after(".sgpr_count"), "vspill": after(".vgpr_spill_count"), "lds": after(".group_segment_fixed_size"), } def verify_and_time( cfg: SingleWaveCfg, shape: Shape, *, warmup=15, iters=100, tol=2e-2, objdump=None, arch="gfx1151", ): import numpy as np art = compile_kernel(build_wmma_fmha_singlewave(cfg, arch=arch), arch=arch) isa = _mem_counts(art.hsaco, art.kernel_name, objdump) isa.update(_resource_counts(art.hsaco)) B, Hq, Hk, D = shape.batch, shape.heads, shape.kvh, shape.head_size Sq, Sk = shape.seqlen_q, shape.seqlen_k rng = np.random.default_rng(0xA11E) Q = (rng.standard_normal((B, Sq, Hq, D)) * 0.3).astype(np.float16) Kk = (rng.standard_normal((B, Sk, Hk, D)) * 0.3).astype(np.float16) Vv = (rng.standard_normal((B, Sk, Hk, D)) * 0.3).astype(np.float16) Out = np.zeros((B, Sq, Hq, D), dtype=np.float16) scale_log2 = float(1.0 / math.sqrt(D) * math.log2(math.e)) grid = singlewave_grid(cfg, seqlen_q=Sq, batch=B) block = (cfg.block_size, 1, 1) rt = Runtime() module = rt.load_module(art.hsaco) fn = module.get_function(art.kernel_name) def u8(a): return (ctypes.c_uint8 * int(a.nbytes)).from_buffer(np.ascontiguousarray(a)) qd, kd, vd, od = (rt.alloc(x.nbytes) for x in (Q, Kk, Vv, Out)) rt.memcpy_h2d(qd, u8(Q), Q.nbytes) rt.memcpy_h2d(kd, u8(Kk), Kk.nbytes) rt.memcpy_h2d(vd, u8(Vv), Vv.nbytes) rt.memset(od, 0, Out.nbytes) packed = struct.pack( " best["tflops"]): best = r if best: print("\nBEST:", _fmt(best)) if __name__ == "__main__": raise SystemExit(main())