Files
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

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())