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
283 lines
9.8 KiB
Python
283 lines
9.8 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
MX GEMM code generator (TileEngine -> Dispatcher bridge).
|
|
|
|
Emits ONE self-contained .hpp per concrete kernel config for the dispatcher's
|
|
ctypes path. To guarantee byte-identical kernels with Old-TE (the parity
|
|
contract), this generator does NOT re-implement the C++ header assembly -- it
|
|
directly reuses the Old-TE builder:
|
|
|
|
tile_engine/ops/gemm/mx_gemm/mx_gemm_instance_builder.py
|
|
-> MxGemmKernelBuilder._generate_kernel_instance(tile_config, trait_combo)
|
|
|
|
The generated instance defines, at GLOBAL scope, a ``SelectedKernel`` struct with
|
|
a static ``launch(const MxGemmHostArgs&, const ck_tile::stream_config&)``, the
|
|
``KERNEL_NAME`` string, and the ADataType/BDataType/CDataType/AccDataType/
|
|
ScaleType/MxGemmHostArgs/ALayout/BLayout/CLayout aliases the ctypes lib expects.
|
|
|
|
Each header is compiled per-kernel via force-include:
|
|
|
|
hipcc -include <kernel.hpp> -DCK_TILE_SINGLE_KERNEL_INCLUDE mx_gemm_ctypes_lib.cpp
|
|
|
|
mx_gemm is microscaling GEMM (fp4/fp8 A*B, per-32-K e8m0 block scales), gfx950
|
|
only. The single valid trait combo is comp_async + cshuffle + intrawave, with a
|
|
fixed 16x16x128 warp tile.
|
|
"""
|
|
|
|
import argparse
|
|
import contextlib
|
|
import json
|
|
import logging
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Iterator, Optional, Tuple
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# Robust import of the Old-TE builder (independent of CWD).
|
|
# dispatcher/codegen/ -> ../../tile_engine/ops/gemm/mx_gemm
|
|
# =============================================================================
|
|
|
|
_THIS_DIR = Path(__file__).resolve().parent
|
|
_GEMM_DIR = (_THIS_DIR / ".." / ".." / "tile_engine" / "ops" / "gemm").resolve()
|
|
_MX_GEMM_DIR = _GEMM_DIR / "mx_gemm"
|
|
|
|
for _p in (str(_GEMM_DIR), str(_MX_GEMM_DIR)):
|
|
if _p not in sys.path:
|
|
sys.path.insert(0, _p)
|
|
|
|
|
|
def _load_mx_builder():
|
|
"""Import MxGemmKernelBuilder from the Old-TE ops tree."""
|
|
import importlib.util
|
|
|
|
mx_path = _MX_GEMM_DIR / "mx_gemm_instance_builder.py"
|
|
spec = importlib.util.spec_from_file_location(
|
|
"mx_gemm_instance_builder", str(mx_path)
|
|
)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module.MxGemmKernelBuilder
|
|
|
|
|
|
# =============================================================================
|
|
# Config validation (restrict to the single valid mx_gemm combo)
|
|
# =============================================================================
|
|
|
|
KERNEL_NAME_PREFIX = "mx_gemm"
|
|
|
|
VALID_DATATYPES = {"fp4", "fp8"}
|
|
VALID_LAYOUT = "rcr"
|
|
VALID_PIPELINE = "comp_async"
|
|
VALID_EPILOGUE = "cshuffle"
|
|
VALID_SCHEDULER = "intrawave"
|
|
FIXED_WARP_TILE = (16, 16, 128)
|
|
|
|
_REQUIRED_TILE_KEYS = (
|
|
"tile_m",
|
|
"tile_n",
|
|
"tile_k",
|
|
"warp_m",
|
|
"warp_n",
|
|
"warp_k",
|
|
"warp_tile_m",
|
|
"warp_tile_n",
|
|
"warp_tile_k",
|
|
)
|
|
|
|
|
|
def _validate(cfg: dict) -> None:
|
|
datatype = cfg.get("datatype")
|
|
if datatype not in VALID_DATATYPES:
|
|
raise ValueError(
|
|
f"datatype must be one of {sorted(VALID_DATATYPES)}, got {datatype!r}"
|
|
)
|
|
|
|
layout = cfg.get("layout")
|
|
if layout != VALID_LAYOUT:
|
|
raise ValueError(f"layout must be {VALID_LAYOUT!r}, got {layout!r}")
|
|
|
|
pipeline = cfg.get("pipeline", VALID_PIPELINE)
|
|
if pipeline != VALID_PIPELINE:
|
|
raise ValueError(
|
|
f"pipeline must be {VALID_PIPELINE!r} for mx_gemm, got {pipeline!r}"
|
|
)
|
|
|
|
epilogue = cfg.get("epilogue", VALID_EPILOGUE)
|
|
if epilogue != VALID_EPILOGUE:
|
|
raise ValueError(
|
|
f"epilogue must be {VALID_EPILOGUE!r} for mx_gemm, got {epilogue!r}"
|
|
)
|
|
|
|
scheduler = cfg.get("scheduler", VALID_SCHEDULER)
|
|
if scheduler != VALID_SCHEDULER:
|
|
raise ValueError(
|
|
f"scheduler must be {VALID_SCHEDULER!r} for mx_gemm, got {scheduler!r}"
|
|
)
|
|
|
|
tc = cfg.get("tile_config")
|
|
if not isinstance(tc, dict):
|
|
raise ValueError("config must contain a 'tile_config' object")
|
|
missing = [k for k in _REQUIRED_TILE_KEYS if k not in tc]
|
|
if missing:
|
|
raise ValueError(f"tile_config missing keys: {missing}")
|
|
|
|
warp_tile = (tc["warp_tile_m"], tc["warp_tile_n"], tc["warp_tile_k"])
|
|
if tuple(warp_tile) != FIXED_WARP_TILE:
|
|
raise ValueError(
|
|
f"mx_gemm warp tile is fixed at {FIXED_WARP_TILE}, got {tuple(warp_tile)}"
|
|
)
|
|
|
|
|
|
def _tile_config_from_cfg(cfg: dict) -> dict:
|
|
tc = cfg["tile_config"]
|
|
return {k: int(tc[k]) for k in _REQUIRED_TILE_KEYS}
|
|
|
|
|
|
def _trait_combo_from_cfg(cfg: dict) -> Tuple:
|
|
"""7-tuple: (pipeline, epilogue, scheduler, pad_m, pad_n, pad_k, persistent)."""
|
|
return (
|
|
cfg.get("pipeline", VALID_PIPELINE),
|
|
cfg.get("epilogue", VALID_EPILOGUE),
|
|
cfg.get("scheduler", VALID_SCHEDULER),
|
|
bool(cfg.get("pad_m", False)),
|
|
bool(cfg.get("pad_n", False)),
|
|
bool(cfg.get("pad_k", False)),
|
|
bool(cfg.get("persistent", False)),
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Builder driver
|
|
# =============================================================================
|
|
|
|
_AUTOGEN_BANNER = """\
|
|
// Auto-generated MX GEMM kernel instance -- DO NOT EDIT.
|
|
// Regenerate via dispatcher/codegen/unified_mx_gemm_codegen.py
|
|
// This header is force-included into the ctypes translation unit:
|
|
// hipcc -include <this.hpp> -DCK_TILE_SINGLE_KERNEL_INCLUDE mx_gemm_ctypes_lib.cpp
|
|
// It defines at global scope: SelectedKernel, KERNEL_NAME, ADataType/BDataType/
|
|
// CDataType/AccDataType/ScaleType, MxGemmHostArgs, and ALayout/BLayout/CLayout.
|
|
"""
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _make_builder(cfg: dict) -> Iterator["object"]:
|
|
"""Yield an MxGemmKernelBuilder bound to a self-cleaning temp working dir.
|
|
|
|
The Old-TE __init__ loads config_json only when it is an existing FILE path,
|
|
and _generate_kernel_instance reads config.get("k_block_per_cu"). So we write
|
|
a minimal config file (carrying k_block_per_cu) and hand its path to the
|
|
builder. The builder's own side-effect .hpp lands in the working dir (ignored).
|
|
|
|
This is a context manager (not a plain factory) because it owns a temp dir:
|
|
kernel_name()/generate_kernel() are shelled frequently (e.g. --list-name), so
|
|
a leaked mkdtemp() per call would pile up mx_gemm_codegen_* dirs under /tmp.
|
|
The builder is only used inside the `with` block, so cleanup on exit is safe.
|
|
"""
|
|
MxGemmKernelBuilder = _load_mx_builder()
|
|
|
|
with tempfile.TemporaryDirectory(prefix="mx_gemm_codegen_") as work_dir_str:
|
|
work_dir = Path(work_dir_str)
|
|
tmp_cfg = {"k_block_per_cu": int(cfg.get("k_block_per_cu", 1))}
|
|
cfg_path = work_dir / "mx_gemm_codegen_config.json"
|
|
cfg_path.write_text(json.dumps(tmp_cfg))
|
|
|
|
gpu_target = cfg.get("gpu_target")
|
|
if not gpu_target:
|
|
raise ValueError(
|
|
"mx_gemm codegen requires an explicit 'gpu_target' in the config; "
|
|
"do not default to a specific GPU architecture."
|
|
)
|
|
yield MxGemmKernelBuilder(
|
|
KERNEL_NAME_PREFIX,
|
|
work_dir,
|
|
gpu_target,
|
|
cfg["datatype"],
|
|
cfg["layout"],
|
|
config_json=str(cfg_path),
|
|
)
|
|
|
|
|
|
def _fix_includes(code: str) -> str:
|
|
"""Drop the Old-TE builder's stale ``ck_tile/ops/gemm_mx.hpp`` umbrella include.
|
|
|
|
That umbrella does not exist on develop; the mx kernel + comp_async pipeline
|
|
are already pulled in by ``ck_tile/ops/gemm.hpp`` (also emitted in the same
|
|
header), and the host-side preshuffle helper by ``ck_tile/host.hpp`` in the
|
|
ctypes TU. Removing the stale line keeps the generated header self-contained.
|
|
"""
|
|
out_lines = []
|
|
for line in code.splitlines(keepends=True):
|
|
if '#include "ck_tile/ops/gemm_mx.hpp"' in line:
|
|
continue
|
|
out_lines.append(line)
|
|
return "".join(out_lines)
|
|
|
|
|
|
def _generate(cfg: dict) -> Tuple[str, str]:
|
|
"""Return (kernel_name, instance_code) for the given concrete config."""
|
|
_validate(cfg)
|
|
tile_config = _tile_config_from_cfg(cfg)
|
|
trait_combo = _trait_combo_from_cfg(cfg)
|
|
with _make_builder(cfg) as builder:
|
|
name, code = builder._generate_kernel_instance(tile_config, trait_combo)
|
|
return name, _fix_includes(code)
|
|
|
|
|
|
def kernel_name(cfg: dict) -> str:
|
|
"""Compute the kernel name without generating/writing code."""
|
|
_validate(cfg)
|
|
tile_config = _tile_config_from_cfg(cfg)
|
|
trait_combo = _trait_combo_from_cfg(cfg)
|
|
with _make_builder(cfg) as builder:
|
|
return builder._format_kernel_name(trait_combo, tile_config)
|
|
|
|
|
|
def generate_kernel(output_dir: Path, cfg: dict) -> Optional[Path]:
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
name, code = _generate(cfg)
|
|
out = output_dir / f"{name}.hpp"
|
|
out.write_text(_AUTOGEN_BANNER + code)
|
|
log.info("wrote %s", out.name)
|
|
return out
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="MX GEMM kernel header generator")
|
|
ap.add_argument("--output-dir", type=Path)
|
|
ap.add_argument("--config", type=Path, help="path to a config JSON file")
|
|
ap.add_argument("--config-json", type=str, help="inline config JSON string")
|
|
ap.add_argument(
|
|
"--list-name",
|
|
action="store_true",
|
|
help="print the kernel name only; write no file",
|
|
)
|
|
args = ap.parse_args()
|
|
|
|
if args.config_json:
|
|
cfg = json.loads(args.config_json)
|
|
elif args.config:
|
|
cfg = json.loads(Path(args.config).read_text())
|
|
else:
|
|
ap.error("one of --config-json or --config is required")
|
|
|
|
if args.list_name:
|
|
print(kernel_name(cfg))
|
|
return 0
|
|
|
|
if not args.output_dir:
|
|
ap.error("--output-dir is required unless --list-name is given")
|
|
|
|
return 0 if generate_kernel(args.output_dir, cfg) else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|