mirror of
https://github.com/ROCm/composable_kernel.git
synced 2026-07-11 17:51:40 +00:00
feat(ck-tile): TE to dispatcher GEMM bridge for fp8/bf8/int8 (all layouts) (#8998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Extends the Tile Engine ↔ Dispatcher GEMM **bridge** to the remaining data types TE's plain GEMM has MFMA warp tiles for, beyond the fp16/bf16 surface of #8479: - **fp8** (E4M3) and **bf8** (E5M2) → fp16 output, fp32 accumulate - **int8** → int32 output and accumulate (gfx942) All four A/B layout combinations per dtype (row-major C only, matching #8479). `fp32`/`fp64` are intentionally **excluded** — they appear in TE's dtype-string map but have no MFMA warp tiles in `GEMM_WARP_TILE_SUPPORTED_COMBINATIONS`, so no kernel can be generated/run. **Depends on the fp16/bf16 bridge in #8997** (`users/muozturk/ck-tile/gemm-bridge-all-layout-bf16-fp16`), which carries the bridge infrastructure and is not yet merged. This PR targets `develop`, so until #8997 merges its diff also includes the base bridge changes; please merge #8997 first. ## Changes - **Codegen** (`codegen_common.py`, `unified_gemm_codegen.py`): add `int32` to the dtype maps; `get_output_dtype` int8→int32; new `get_acc_dtype` (int8→int32, else fp32); derive `AccDataType`/`CDataType`, the `GEMM_KEY_DTYPE_{C,ACC}` macros, and the registry `dtype_c`/`dtype_acc` from the dtype instead of hard-coding `float`/`fp32`. - **Host harness** (`gemm_utils.py`): fp8/bf8 **FNUZ** (gfx942) uint8 codecs — exact decode (matches device `fp8_t`/`bf8_t`), nearest-representable saturating encode (same pattern as the existing bf16 helper); `GpuGemmRunner.run` encodes A/B and sizes the C buffer per dtype; `expand_sweep` sets `dtype_c`/`dtype_acc`. - **Tests**: `test_gemm_utils.py` adds CPU-only fp8/bf8 codec + output-dtype tests (all green); `test_gemm_parity.py` adds fp8/bf8/int8 cases with dtype-aware inputs/references/tolerances (int8 is bit-exact), GPU-gated like the existing cases. ## Verification done - `test_gemm_utils.py` + `test_codegen_common.py`: **54 passed** (CPU). - Codegen smoke: fp8/int8/fp16 each generate 1 kernel + 1 wrapper, 0 failed; emitted `ADataType/CDataType/AccDataType` and `GEMM_KEY_*` macros are correct (int8→int32_t acc/C; fp8→fp16_t C). - `test_gemm_parity.py` collects 60 cases and skips cleanly without a GPU. - The 16 unrelated failures in `test_examples_integration` / `test_grouped_conv_codegen` / `test_library_caching` are **pre-existing** (verified identical on the base branch; they require a built dispatcher `.a` / GPU). ## Test plan - [x] Merge #8997 (fp16/bf16 bridge), then this reduces to just the fp8/bf8/int8 delta on `develop`. - [x] On an MI300X (gfx942) node: run `python3 tests/test_gemm_parity.py` and confirm fp8/bf8/int8 parity; tune the fp8/bf8 tolerances if needed (current values are first-cut headroom). - [x] FNUZ vs OCP: the fp8/bf8 host codec targets the gfx942 FNUZ format; validate / extend for gfx950 (OCP) before enabling there.
372 lines
12 KiB
Python
372 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Shared codegen infrastructure for GEMM, grouped convolution, and FMHA code generators.
|
|
|
|
Extracted from unified_gemm_codegen.py + arch-aware expansion helpers from conv.
|
|
Both unified_gemm_codegen.py and unified_grouped_conv_codegen.py import from here
|
|
to eliminate duplication.
|
|
"""
|
|
|
|
import logging
|
|
import concurrent.futures
|
|
from dataclasses import dataclass
|
|
from typing import (
|
|
Callable,
|
|
ClassVar,
|
|
Dict,
|
|
FrozenSet,
|
|
List,
|
|
Optional,
|
|
Sequence,
|
|
Tuple,
|
|
TypeVar,
|
|
)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
T = TypeVar("T")
|
|
R = TypeVar("R")
|
|
|
|
ANY_INT = -1
|
|
|
|
|
|
# ============================================================================
|
|
# Tile and Trait Configuration (shared between GEMM and Conv)
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class TileConfig:
|
|
"""Tile configuration parameters shared by GEMM and grouped conv."""
|
|
|
|
tile_m: int
|
|
tile_n: int
|
|
tile_k: int
|
|
warp_m: int
|
|
warp_n: int
|
|
warp_k: int
|
|
warp_tile_m: int
|
|
warp_tile_n: int
|
|
warp_tile_k: int
|
|
|
|
def is_valid(self) -> bool:
|
|
if self.tile_m <= 0 or self.tile_n <= 0 or self.tile_k <= 0:
|
|
return False
|
|
return (
|
|
self.tile_m % (self.warp_m * self.warp_tile_m) == 0
|
|
and self.tile_n % (self.warp_n * self.warp_tile_n) == 0
|
|
and self.tile_k % (self.warp_k * self.warp_tile_k) == 0
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TraitConfigBase:
|
|
"""
|
|
Base kernel trait configuration shared by GEMM and grouped conv.
|
|
|
|
GEMM extends this with ``persistent``; grouped conv extends with
|
|
``double_smem_buffer`` and ``num_groups_to_merge``.
|
|
"""
|
|
|
|
pipeline: str # mem, compv3, compv4, compv5, ...
|
|
epilogue: str # cshuffle, default
|
|
scheduler: str # intrawave, interwave
|
|
pad_m: bool
|
|
pad_n: bool
|
|
pad_k: bool
|
|
|
|
# Unsupported (pipeline, epilogue, scheduler) combinations.
|
|
# Only 'mem' and 'basic_v1' pipelines support interwave; all compute
|
|
# pipelines (compv3/v4/v5/v6/async) only support intrawave.
|
|
_UNSUPPORTED: ClassVar[FrozenSet] = frozenset(
|
|
{
|
|
("compv3", "cshuffle", "interwave"),
|
|
("compv3", "default", "interwave"),
|
|
("compv4", "cshuffle", "interwave"),
|
|
("compv4", "default", "interwave"),
|
|
("compv5", "cshuffle", "interwave"),
|
|
("compv5", "default", "interwave"),
|
|
("compv6", "cshuffle", "interwave"),
|
|
("compv6", "default", "interwave"),
|
|
("comp_async", "cshuffle", "interwave"),
|
|
("comp_async", "default", "interwave"),
|
|
("basic_async_v1", "cshuffle", "interwave"),
|
|
("basic_async_v1", "default", "interwave"),
|
|
}
|
|
)
|
|
|
|
def is_valid(self) -> bool:
|
|
return (self.pipeline, self.epilogue, self.scheduler) not in self._UNSUPPORTED
|
|
|
|
|
|
# ============================================================================
|
|
# Type Mappings (centralized for both GEMM and conv codegen)
|
|
# ============================================================================
|
|
|
|
|
|
class CommonTypeMappings:
|
|
"""Centralized type mappings shared by GEMM and grouped conv codegen."""
|
|
|
|
DTYPE_TO_CK = {
|
|
"fp16": "fp16_t",
|
|
"bf16": "bf16_t",
|
|
"fp32": "float",
|
|
"fp8": "fp8_t",
|
|
"bf8": "bf8_t",
|
|
"int8": "int8_t",
|
|
"int32": "int32_t",
|
|
}
|
|
|
|
DTYPE_TO_CK_QUALIFIED = {
|
|
"fp16": "ck_tile::fp16_t",
|
|
"bf16": "ck_tile::bf16_t",
|
|
"fp32": "float",
|
|
"fp8": "ck_tile::fp8_t",
|
|
"bf8": "ck_tile::bf8_t",
|
|
"int8": "int8_t",
|
|
"int32": "int32_t",
|
|
}
|
|
|
|
DTYPE_TO_DISPATCHER = {
|
|
"fp16": "DataType::FP16",
|
|
"bf16": "DataType::BF16",
|
|
"fp32": "DataType::FP32",
|
|
"fp8": "DataType::FP8",
|
|
"bf8": "DataType::BF8",
|
|
"int8": "DataType::INT8",
|
|
"int32": "DataType::INT32",
|
|
}
|
|
|
|
# GEMM-specific layout mappings ("r"/"c" for row/column major).
|
|
# Convolution layouts (NHWGC, GKYXC, etc.) are handled by
|
|
# unified_grouped_conv_codegen.py via GroupedConvLayout / GroupedConvTypeMappings.
|
|
GEMM_LAYOUT_TO_CK = {
|
|
"r": "tensor_layout::gemm::RowMajor",
|
|
"c": "tensor_layout::gemm::ColumnMajor",
|
|
}
|
|
LAYOUT_TO_CK = GEMM_LAYOUT_TO_CK # backward compat alias
|
|
|
|
GEMM_LAYOUT_TO_DISPATCHER = {
|
|
"r": "LayoutTag::RowMajor",
|
|
"c": "LayoutTag::ColMajor",
|
|
}
|
|
LAYOUT_TO_DISPATCHER = GEMM_LAYOUT_TO_DISPATCHER # backward compat alias
|
|
|
|
# GEMM-only pipeline mappings (used by unified_gemm_codegen.py).
|
|
# Convolution pipelines are in GroupedConvTypeMappings
|
|
# (unified_grouped_conv_codegen.py). CK Tile conv supports:
|
|
# BASIC_V1, Mem, CompV3, CompV4, CompV5, CompV6, ASYNC_V1, ASYNC_V4.
|
|
# The dispatcher currently generates: mem, compv3, compv4.
|
|
# preshufflev2 is GEMM-only (weight pre-shuffle for GEMM, not conv).
|
|
PIPELINE_TO_CK = {
|
|
"mem": "GemmPipelineAgBgCrMem",
|
|
"compv3": "GemmPipelineAgBgCrCompV3",
|
|
"compv4": "GemmPipelineAgBgCrCompV4",
|
|
"compv5": "GemmPipelineAgBgCrCompV5",
|
|
"preshufflev2": "WeightPreshufflePipelineAGmemBGmemCRegV2",
|
|
}
|
|
|
|
PIPELINE_TO_BASE = {
|
|
"mem": "BaseGemmPipelineAgBgCrMem",
|
|
"compv3": "BaseGemmPipelineAgBgCrCompV3",
|
|
"compv4": "BaseGemmPipelineAgBgCrCompV4",
|
|
"compv5": "BaseGemmPipelineAgBgCrCompV5",
|
|
"preshufflev2": "BaseWeightPreshufflePipelineAGmemBGmemCRegV2",
|
|
}
|
|
|
|
PIPELINE_TO_DISPATCHER = {
|
|
"mem": "Pipeline::Mem",
|
|
"compv3": "Pipeline::CompV3",
|
|
"compv4": "Pipeline::CompV4",
|
|
"compv5": "Pipeline::CompV5",
|
|
"preshufflev2": "Pipeline::PreShuffleV2",
|
|
}
|
|
|
|
SCHEDULER_TO_CK = {
|
|
"intrawave": "GemmPipelineScheduler::Intrawave",
|
|
"interwave": "GemmPipelineScheduler::Interwave",
|
|
"default": "GemmPipelineScheduler::Default",
|
|
}
|
|
|
|
SCHEDULER_TO_DISPATCHER = {
|
|
"intrawave": "Scheduler::Intrawave",
|
|
"interwave": "Scheduler::Interwave",
|
|
"default": "Scheduler::Auto",
|
|
}
|
|
|
|
EPILOGUE_TO_DISPATCHER = {
|
|
"cshuffle": "Epilogue::CShuffle",
|
|
"default": "Epilogue::Default",
|
|
}
|
|
|
|
@staticmethod
|
|
def get_output_dtype(dtype: str) -> str:
|
|
"""Get output (C) datatype for an A/B element dtype.
|
|
|
|
Low-precision float inputs accumulate into and store as fp16
|
|
(fp8/bf8 -> fp16); int8 stores its int32 accumulator (int8 -> int32).
|
|
Everything else stores in its own dtype.
|
|
"""
|
|
if dtype in ("fp8", "bf8"):
|
|
return "fp16"
|
|
if dtype == "int8":
|
|
return "int32"
|
|
return dtype
|
|
|
|
@staticmethod
|
|
def get_acc_dtype(dtype: str) -> str:
|
|
"""Get accumulator datatype for an A/B element dtype.
|
|
|
|
Integer GEMM accumulates in int32; every float dtype accumulates in
|
|
fp32.
|
|
"""
|
|
return "int32" if dtype == "int8" else "fp32"
|
|
|
|
|
|
# ============================================================================
|
|
# Code Generation Helpers
|
|
# ============================================================================
|
|
|
|
|
|
def generate_cpp_compilation_unit(kernel_name: str) -> str:
|
|
"""Generate a .cpp compilation unit that includes a kernel header.
|
|
|
|
This is the standard pattern: one .cpp per kernel that just includes
|
|
the generated .hpp header, causing template instantiation.
|
|
"""
|
|
return (
|
|
f"// Auto-generated compilation unit for {kernel_name}\n"
|
|
f'#include "{kernel_name}.hpp"\n'
|
|
)
|
|
|
|
|
|
def parallel_generate(
|
|
generate_fn: Callable[[T], R],
|
|
items: Sequence[T],
|
|
parallel: bool = True,
|
|
) -> List[R]:
|
|
"""Run ``generate_fn`` over ``items``, optionally in parallel.
|
|
|
|
Logs per-item progress (best-of-conv pattern).
|
|
Returns a flat list of results in completion order.
|
|
"""
|
|
results: List[R] = []
|
|
if not items:
|
|
return results
|
|
|
|
if parallel and len(items) > 1:
|
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
futures = {executor.submit(generate_fn, item): item for item in items}
|
|
for future in concurrent.futures.as_completed(futures):
|
|
result = future.result()
|
|
results.append(result)
|
|
log.info("Generated: %s", futures[future])
|
|
else:
|
|
for item in items:
|
|
result = generate_fn(item)
|
|
results.append(result)
|
|
log.info("Generated: %s", item)
|
|
|
|
return results
|
|
|
|
|
|
# ============================================================================
|
|
# Arch-Aware Expansion Helpers (adopted from conv kernel_decl.hpp)
|
|
# ============================================================================
|
|
|
|
# These load from arch_specs_generated when available, falling back to
|
|
# hardcoded defaults that match the most common arch (gfx942).
|
|
|
|
_arch_data_cache: Optional[Dict] = None
|
|
|
|
|
|
def _get_arch_data() -> Dict:
|
|
"""Load arch filter data, with caching."""
|
|
global _arch_data_cache
|
|
if _arch_data_cache is not None:
|
|
return _arch_data_cache
|
|
|
|
try:
|
|
from arch_specs_generated import (
|
|
WARP_SUPPORTED_COMBINATIONS,
|
|
WARP_TILE_SUPPORTED_COMBINATIONS,
|
|
TRAIT_UNSUPPORTED_COMBINATIONS,
|
|
get_supported_archs,
|
|
)
|
|
|
|
_arch_data_cache = {
|
|
"warp_combos": WARP_SUPPORTED_COMBINATIONS,
|
|
"warp_tile_combos": WARP_TILE_SUPPORTED_COMBINATIONS,
|
|
"trait_unsupported": TRAIT_UNSUPPORTED_COMBINATIONS,
|
|
"supported_archs": get_supported_archs(),
|
|
}
|
|
except ImportError:
|
|
_arch_data_cache = {
|
|
"warp_combos": {
|
|
"gfx942": [[1, 4, 1], [2, 2, 1], [4, 1, 1]],
|
|
"gfx90a": [[1, 4, 1], [2, 2, 1], [4, 1, 1]],
|
|
},
|
|
"warp_tile_combos": {
|
|
"gfx942": {"fp16_fp16_fp32": [[16, 16, 16], [32, 32, 16]]},
|
|
"gfx90a": {"fp16_fp16_fp32": [[16, 16, 16], [32, 32, 16]]},
|
|
},
|
|
"trait_unsupported": {
|
|
("compv3", "cshuffle", "interwave"),
|
|
("compv4", "cshuffle", "interwave"),
|
|
},
|
|
"supported_archs": ["gfx90a", "gfx942", "gfx950"],
|
|
}
|
|
return _arch_data_cache
|
|
|
|
|
|
def valid_wave_configs(arch: str) -> List[List[int]]:
|
|
"""Return valid [wave_m, wave_n, wave_k] combos for *arch*."""
|
|
data = _get_arch_data()
|
|
return data["warp_combos"].get(arch, [[2, 2, 1]])
|
|
|
|
|
|
def valid_warp_configs(arch: str, dtype: str) -> List[List[int]]:
|
|
"""Return valid [warp_tile_m, warp_tile_n, warp_tile_k] combos for *arch*/*dtype*.
|
|
|
|
The dtype key is constructed as ``{dtype}_{dtype}_{acc}`` where acc is
|
|
fp32 for float types and int32 for int8.
|
|
"""
|
|
data = _get_arch_data()
|
|
acc = "int32" if dtype == "int8" else "fp32"
|
|
dtype_key = f"{dtype}_{dtype}_{acc}"
|
|
arch_tiles = data["warp_tile_combos"].get(arch, {})
|
|
return arch_tiles.get(dtype_key, [[32, 32, 16]])
|
|
|
|
|
|
def valid_trait_configs() -> List[Tuple[str, str]]:
|
|
"""Return valid (pipeline, scheduler) pairs.
|
|
|
|
Compute pipelines only support intrawave; mem supports both.
|
|
"""
|
|
return [
|
|
("compv3", "intrawave"),
|
|
("compv4", "intrawave"),
|
|
("compv5", "intrawave"),
|
|
("mem", "intrawave"),
|
|
("mem", "interwave"),
|
|
]
|
|
|
|
|
|
def needs_wave_expansion(config: dict) -> bool:
|
|
"""True if wave_m or wave_n is a wildcard (ANY_INT = -1)."""
|
|
return config.get("wave_m", 2) == ANY_INT or config.get("wave_n", 2) == ANY_INT
|
|
|
|
|
|
def needs_warp_expansion(config: dict) -> bool:
|
|
"""True if warp_m or warp_n is a wildcard (ANY_INT = -1)."""
|
|
return config.get("warp_m", 32) == ANY_INT or config.get("warp_n", 32) == ANY_INT
|
|
|
|
|
|
def needs_pipeline_expansion(config: dict) -> bool:
|
|
"""True if pipeline is a wildcard (\"*\")."""
|
|
return config.get("pipeline", "compv4") == "*"
|