Files
composable_kernel/tile_engine/ops/gemm/run_one_gemm_kernel.py
Muhammed Emin Ozturk de292a24f9 [rocm-libraries] ROCm/rocm-libraries#8997 (commit 6e9bfd9)
feat(ck-tile): TE to dispatcher GEMM bridge (fp16/bf16, all
 layouts) (#8997)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

> Re-opened from #8479 with a compliant branch name
(users/muozturk/ck-tile/gemm-bridge-all-layout-bf16-fp16). Supersedes
#8479.

## Summary

This PR routes the **Tile Engine (TE) regular-GEMM sweep through the
Dispatcher**,
making the Dispatcher the single source of truth for **codegen → build →
runtime**
while the Tile Engine keeps only the **config search space** and the
**benchmark
loop**. It is the consolidated, **single-commit** GEMM bridge covering
**all four
layouts (`rcr`/`rrr`/`crr`/`ccr`)** and **both `fp16` and `bf16`**.

It is a clean re-roll of the earlier bridge work (previously split
across
#8123 + the stacked key/bf16/layouts/parity/example PRs and consolidated
in
#8261). Those branches accumulated unrelated cross-project commits
through repeated
`develop` merges; **this branch is a single clean commit off the latest
`develop`**
containing only the GEMM-bridge files. It supersedes and replaces #8123
/ #8261.

## Motivation

The Tile Engine historically owned its own codegen/build/runtime for
GEMM
(`tile_engine/ops/gemm/gemm_universal/`). The consolidation goal is for
the
**Dispatcher** to own all of that — exactly as it already does for
**FMHA** and
**Grouped Conv** — so there is one kernel-generation/build/runtime path
and the
TE shrinks to a config+benchmark frontend. This PR brings regular GEMM
in line
with that reference binding.

## The binding (mirrors the FMHA/Conv reference, six stages)

1. **Config JSON (TE side)** — the sweep search space lives in
   `tile_engine/ops/gemm/configs/` (flat op-root layout, matching the
   `fmha/` and `grouped_conv/` bridges).
2. **Codegen (Dispatcher)** —
`dispatcher/codegen/unified_gemm_codegen.py` emits
   one fully-typed `.hpp` per kernel; `GemmKernelConfig.name` reproduces
`KERNEL_NAME` **byte-for-byte** (the thread tying config → kernel →
runtime).
3. **Compile to `.so`** — a single static `gemm_ctypes_lib.cpp` is
force-included
   (`-include <kernel.hpp>`); one `.so` per kernel.
4. **Flat `extern "C"` ABI** — `dispatcher_run_gemm(A, B, C, M, N, K,
time_ms)` +
the kernel-name enumeration entry points. **Host-pointer** memory model
(the C
lib `hipMalloc`s internally) — the FMHA-forward branch of the reference.
5. **Python ctypes wrapper** — `dispatcher/python/gemm_utils.py`
   (`GemmDispatcherLib` + `GpuGemmRunner`).
6. **TE driver (3 phases)** — `gemm_full_benchmark.py` (parallel
codegen+build →
`expand_sweep` → subprocess-isolated benchmark) + the disposable
per-kernel
   worker `run_one_gemm_kernel.py`.

## What's included

**Bridge core**
- `dispatcher/codegen/unified_gemm_codegen.py` — GEMM codegen,
byte-exact naming.
- `dispatcher/bindings/ctypes/gemm_ctypes_lib.cpp` — flat C ABI,
host-pointer model.
- `dispatcher/python/gemm_utils.py` — `GemmKernelConfig`, multi-kernel
build
(`setup_multiple_gemm_dispatchers`), `expand_sweep`,
one-`.so`-per-kernel.
- `tile_engine/ops/gemm/gemm_full_benchmark.py` +
`run_one_gemm_kernel.py` —
  3-phase, multi-GPU, subprocess-isolated driver/worker.

**Feature surface (the point of this PR)**
- **All four layouts** `rcr`/`rrr`/`crr`/`ccr` (row-major C only —
ck_tile rejects
  column-major C at build) with layout-aware host transpose.
- **`fp16` + `bf16`** (bf16 via uint16 byte-encoding; dtype derived from
kernel name).
- **Trait-derived registry `KernelKey`** — replaces the earlier
hard-coded
fp16/rcr key so the registry path generalizes across dtype/layout/tile.

**Correctness & performance hygiene**
- **`--verify`** opt-in fp32 numpy-reference gate (global
`max|out-ref|/max|ref|`),
`verified`/`max_rel` columns in the CSV; a mismatch counts as a failure.
- **Tile Engine AMDGPU `-mllvm` codegen-flag parity** (without these the
kernel
  builds with different occupancy and the timing diverges) and
  **arch-validated tile filtering** against the real pipeline/scheduler.
- **Multi-GPU** fan-out across all visible GPUs (`--devices`,
device-pinned
  `HIP_VISIBLE_DEVICES` workers).

**Example & tests**
- `dispatcher/examples/gemm/python/12_te_bridge.py` — runnable
end-to-end example.
- `dispatcher/tests/test_gemm_parity.py`, `test_gemm_utils.py`, and a
parity
  regression harness.

**Cleanup**
- Removes the legacy standalone `gemm_universal` build path
  (`gemm_universal_instance_builder.py`, `*_benchmark*.{py,cpp,hpp}`,
`gemm_universal/CMakeLists.txt`) and the old
`test/ck_tile/gemm_tile_engine/`
  harness; promotes the sweep configs to the flat op-root `configs/`.

## Design decisions (consistent with the reference)

- **Host-pointer memory ownership** (C lib owns device memory) — matches
FMHA-forward; the Python runner passes host numpy arrays straight
through.
- **One `.so` per kernel** — packaging choice; the multi-kernel name ABI
is
retained (`get_kernel_name_at(0)` reports the single kernel), so the
Python
  enumeration path is unchanged from FMHA/Conv.
- **Flat `configs/`** at the op root — matches the
`fmha/`/`grouped_conv/`
convention; the not-yet-bridged variants keep their per-variant
`configs/`
  dirs, selected by `--variant`.

## Validation (gfx942 / MI300X)

- Bridge build + benchmark + `--verify` across **`fp16` and `bf16`** and
**all
  four layouts**, checked against an fp32 numpy reference (`A @ B`).
- **Name parity** holds end-to-end: each `.so`'s reported runtime name
equals
  `GemmKernelConfig(...).name`.
- bf16 passes under a widened fp16/bf16 tolerance; fp16 within the
standard
  `max_rel` gate.

## Test plan

- [ ] `gemm_full_benchmark.py --verify` over
`configs/default_ci_config.json` for
      `fp16` and `bf16`, each of `rcr`/`rrr`/`crr`/`ccr`.
- [ ] `unified_gemm_codegen.py` emits a header whose stem ==
`GemmKernelConfig.name`.
- [ ] `setup_multiple_gemm_dispatchers` builds + links each config
against
      `gemm_ctypes_lib.cpp`.
- [ ] `pytest dispatcher/tests/test_gemm_parity.py
dispatcher/tests/test_gemm_utils.py`.
- [ ] `examples/gemm/python/12_te_bridge.py` runs end to end.

## Notes

- Single clean commit off the latest `develop`; the diff is **35 files,
all under
`projects/composablekernel/`** (dispatcher + tile_engine/ops/gemm +
test/ck_tile).
- **Supersedes #8123 and #8261**, which will be closed.
- Stream-K (#8136) and grouped GEMM are separate bridge efforts, not in
this PR.
2026-07-07 01:15:38 +00:00

149 lines
5.0 KiB
Python

#!/usr/bin/env python3
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Worker script for running GEMM kernels in an isolated subprocess.
Mirrors grouped_conv's run_one_grouped_conv_kernel.py:
- Receives kernel config + problem via stdin as JSON
- Loads the .so library ONLY inside this subprocess
- Outputs timing results as JSON to stdout (one line per kernel, flushed)
- A GPU fault kills only this process; the parent driver can continue
Input JSON format:
Single: {"so_path": "...", "problem": {"M":.., "N":.., "K":..}, "kernel_name": "..."}
Batch: {"items": [{"so_path": "...", "problem": {...}, "kernel_name": "..."}, ...]}
Optional top-level keys ``verify`` (bool) and ``verify_tol`` (float) enable an
fp32 numpy reference check; when set, each OK result also carries ``verified``
and ``max_rel``.
Output JSON format (one line per kernel):
{"idx": 0, "ok": true, "ms": 0.123, "tflops": 456.7, "non_zero": 1, "kernel": "..."}
{"idx": 0, "ok": true, ..., "verified": true, "max_rel": 3.1e-4} # with --verify
{"idx": 1, "ok": false, "error": "...", "kernel": "..."}
"""
import json
import os
import sys
# Add dispatcher python paths from environment (os.pathsep-separated).
gemm_pypath = os.environ.get("GEMM_PYPATH", "")
if gemm_pypath:
for p in gemm_pypath.split(os.pathsep):
if p and p not in sys.path:
sys.path.insert(0, p)
from gemm_utils import GemmProblem, GpuGemmRunner # noqa: E402
import numpy as np # noqa: E402
def _run_one(idx, so_path, prob_dict, kernel_name, verify=False, verify_tol=2e-2):
"""Run a single kernel and emit its result as one JSON line.
When ``verify`` is set, the kernel output is checked against an fp32 numpy
reference (``A @ B``) using the global relative metric
``max|out - ref| / max|ref|``; the emitted ``verified`` field then reflects
correctness, not just liveness (``non_zero``).
"""
try:
problem = GemmProblem.from_dict(prob_dict)
# Cache host matrices per shape so batch mode doesn't regenerate huge inputs per kernel.
cache = getattr(_run_one, "_ab_cache", {})
key = (problem.M, problem.N, problem.K)
if key not in cache:
rng = np.random.RandomState(42)
cache[key] = (
(rng.randn(problem.M, problem.K) * 0.1).astype(np.float32),
(rng.randn(problem.K, problem.N) * 0.1).astype(np.float32),
)
_run_one._ab_cache = cache
A, B = cache[key]
# CRITICAL: load the library ONLY inside this subprocess.
runner = GpuGemmRunner(lib_path=so_path)
result = runner.run(A, B, problem)
if result.success:
non_zero = (
int(np.count_nonzero(result.output))
if result.output is not None
else 0
)
out = {
"idx": idx,
"ok": True,
"ms": result.time_ms,
"tflops": result.tflops,
"non_zero": non_zero,
"kernel": kernel_name,
}
if verify:
ref = A.astype(np.float32) @ B.astype(np.float32)
got = result.output.astype(np.float32)
denom = float(np.max(np.abs(ref))) or 1.0
max_rel = float(np.max(np.abs(got - ref)) / denom)
out["max_rel"] = max_rel
out["verified"] = bool(max_rel <= verify_tol)
print(json.dumps(out), flush=True)
else:
print(
json.dumps(
{
"idx": idx,
"ok": False,
"error": f"kernel returned status {result.status}",
"kernel": kernel_name,
}
),
flush=True,
)
except Exception as e:
print(
json.dumps(
{"idx": idx, "ok": False, "error": str(e), "kernel": kernel_name}
),
flush=True,
)
def main():
"""Read JSON from stdin, run kernel(s), output results."""
try:
d = json.loads(sys.stdin.buffer.read())
except Exception as e:
print(
json.dumps({"idx": 0, "ok": False, "error": f"JSON parse error: {e}"}),
flush=True,
)
sys.exit(1)
verify = bool(d.get("verify", False))
verify_tol = float(d.get("verify_tol", 2e-2))
if "items" in d:
for i, item in enumerate(d["items"]):
_run_one(
i,
item["so_path"],
item["problem"],
item.get("kernel_name", "unknown"),
verify=verify,
verify_tol=verify_tol,
)
else:
_run_one(
0,
d["so_path"],
d["problem"],
d.get("kernel_name", "unknown"),
verify=verify,
verify_tol=verify_tol,
)
if __name__ == "__main__":
main()