mirror of
https://github.com/ROCm/composable_kernel.git
synced 2026-07-15 19:44:39 +00:00
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.
149 lines
5.0 KiB
Python
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()
|