Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions projects/composablekernel/dispatcher/codegen/codegen_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class CommonTypeMappings:
"fp8": "fp8_t",
"bf8": "bf8_t",
"int8": "int8_t",
"int32": "int32_t",
}

DTYPE_TO_CK_QUALIFIED = {
Expand All @@ -127,6 +128,7 @@ class CommonTypeMappings:
"fp8": "ck_tile::fp8_t",
"bf8": "ck_tile::bf8_t",
"int8": "int8_t",
"int32": "int32_t",
}

DTYPE_TO_DISPATCHER = {
Expand All @@ -136,6 +138,7 @@ class CommonTypeMappings:
"fp8": "DataType::FP8",
"bf8": "DataType::BF8",
"int8": "DataType::INT8",
"int32": "DataType::INT32",
}

# GEMM-specific layout mappings ("r"/"c" for row/column major).
Expand Down Expand Up @@ -202,8 +205,26 @@ class CommonTypeMappings:

@staticmethod
def get_output_dtype(dtype: str) -> str:
"""Get output datatype (fp8/bf8 -> fp16)."""
return "fp16" if dtype in ("fp8", "bf8") else dtype
"""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"


# ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,12 +414,13 @@ def _types(self, config: KernelConfig, kernel_name: str) -> str:
def _kernel_local_types(self, config: KernelConfig) -> str:
"""Generate data type and layout definitions inside kernel namespace"""
output_dtype = self.tm.get_output_dtype(self.datatype)
acc_dtype = self.tm.get_acc_dtype(self.datatype)

return f"""
// Data types (inside namespace to avoid conflicts across layouts)
using ADataType = {self.tm.DTYPE_TO_CK[self.datatype]};
using BDataType = {self.tm.DTYPE_TO_CK[self.datatype]};
using AccDataType = float;
using AccDataType = {self.tm.DTYPE_TO_CK[acc_dtype]};
using CDataType = {self.tm.DTYPE_TO_CK[output_dtype]};

// Layouts (inside namespace to avoid conflicts when mixing layouts)
Expand Down Expand Up @@ -452,6 +453,7 @@ def _selected_kernel_struct(self, config: KernelConfig, kernel_name: str) -> str
t = config.tile
tr = config.trait
output_dtype = self.tm.get_output_dtype(self.datatype)
acc_dtype = self.tm.get_acc_dtype(self.datatype)

# Generate unique struct name and namespace from kernel name
struct_name = f"Kernel_{kernel_name}"
Expand All @@ -467,7 +469,7 @@ def _selected_kernel_struct(self, config: KernelConfig, kernel_name: str) -> str
// Data types (inside namespace to avoid conflicts across different kernels)
using ADataType = {self.tm.DTYPE_TO_CK[self.datatype]};
using BDataType = {self.tm.DTYPE_TO_CK[self.datatype]};
using AccDataType = float;
using AccDataType = {self.tm.DTYPE_TO_CK[acc_dtype]};
using CDataType = {self.tm.DTYPE_TO_CK[output_dtype]};

// Layouts (inside namespace to avoid conflicts when mixing layouts like RCR + RRR)
Expand Down Expand Up @@ -522,8 +524,8 @@ def _selected_kernel_struct(self, config: KernelConfig, kernel_name: str) -> str
constexpr const char* KERNEL_NAME = {ns_name}::KERNEL_NAME;
using ADataType = {self.tm.DTYPE_TO_CK_QUALIFIED[self.datatype]};
using BDataType = {self.tm.DTYPE_TO_CK_QUALIFIED[self.datatype]};
using CDataType = {self.tm.DTYPE_TO_CK_QUALIFIED[self.tm.get_output_dtype(self.datatype)]};
using AccDataType = float;
using CDataType = {self.tm.DTYPE_TO_CK_QUALIFIED[output_dtype]};
using AccDataType = {self.tm.DTYPE_TO_CK_QUALIFIED[acc_dtype]};

// KernelKey field descriptors for the force-included kernel.
// The ctypes library builds the registry KernelKey from these so the
Expand All @@ -534,7 +536,7 @@ def _selected_kernel_struct(self, config: KernelConfig, kernel_name: str) -> str
#define GEMM_KEY_DTYPE_A "{self.datatype}"
#define GEMM_KEY_DTYPE_B "{self.datatype}"
#define GEMM_KEY_DTYPE_C "{output_dtype}"
#define GEMM_KEY_DTYPE_ACC "fp32"
#define GEMM_KEY_DTYPE_ACC "{acc_dtype}"
#define GEMM_KEY_LAYOUT_A "{self.layout[0]}"
#define GEMM_KEY_LAYOUT_B "{self.layout[1]}"
#define GEMM_KEY_LAYOUT_C "{self.layout[2]}"
Expand Down Expand Up @@ -815,6 +817,7 @@ def generate(
"""Generate dispatcher wrapper"""
kernel_name = KernelNaming.generate(config, self.datatype, self.layout)
output_dtype = self.tm.get_output_dtype(self.datatype)
acc_dtype = self.tm.get_acc_dtype(self.datatype)
rel_path = kernel_path.relative_to(output_dir)

return f"""// SPDX-License-Identifier: MIT
Expand Down Expand Up @@ -849,7 +852,7 @@ def generate(
key.signature.dtype_a = {self.tm.DTYPE_TO_DISPATCHER[self.datatype]};
key.signature.dtype_b = {self.tm.DTYPE_TO_DISPATCHER[self.datatype]};
key.signature.dtype_c = {self.tm.DTYPE_TO_DISPATCHER[output_dtype]};
key.signature.dtype_acc = DataType::FP32;
key.signature.dtype_acc = {self.tm.DTYPE_TO_DISPATCHER[acc_dtype]};
key.signature.layout_a = {self.tm.LAYOUT_TO_DISPATCHER[self.layout[0]]};
key.signature.layout_b = {self.tm.LAYOUT_TO_DISPATCHER[self.layout[1]]};
key.signature.layout_c = {self.tm.LAYOUT_TO_DISPATCHER[self.layout[2]]};
Expand Down
133 changes: 127 additions & 6 deletions projects/composablekernel/dispatcher/python/gemm_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,107 @@ def _bf16_u16_to_fp32(u16: np.ndarray) -> np.ndarray:
return (u16.astype(np.uint32) << 16).view(np.float32)


# ---------------------------------------------------------------------------
# fp8 (E4M3) / bf8 (E5M2) -- FNUZ ("NANOO") encoding used by gfx942/MI300.
#
# numpy has no native 8-bit float, and the C ABI only cares about the 1-byte
# memory layout (sizeof(fp8_t) == sizeof(bf8_t) == 1). We carry the value as a
# uint8 bit pattern. As with bf16, the DECODE is the load-bearing half: it must
# return the exact value the device's fp8_t/bf8_t represents for a byte, so the
# NumPy reference multiplies bit-for-bit what the GPU multiplies. The ENCODE only
# needs to land on the nearest representable byte.
#
# FNUZ format (gfx942): bias = 2^(exp_bits-1); the all-1s exponent is a normal
# number (no Inf), the sole NaN is the sign=1/exp=0/mant=0 byte (0x80), and there
# is no negative zero. gfx950/MI350 uses the OCP fp8 format instead; this codec
# targets the gfx942 default and the OCP path needs separate handling.
# ---------------------------------------------------------------------------


def _fnuz_decode_table(exp_bits: int, mant_bits: int) -> np.ndarray:
"""Build the 256-entry byte -> fp32 value table for an 8-bit FNUZ float."""
bias = (1 << (exp_bits - 1))
mant_max = 1 << mant_bits
sign_shift = exp_bits + mant_bits
exp_mask = (1 << exp_bits) - 1
table = np.zeros(256, dtype=np.float32)
for b in range(256):
sign = (b >> sign_shift) & 1
exp = (b >> mant_bits) & exp_mask
mant = b & (mant_max - 1)
if exp == 0 and mant == 0:
# +0 (0x00); the negative-zero slot (0x80) is the lone NaN.
table[b] = np.float32(np.nan) if sign else np.float32(0.0)
continue
if exp == 0:
val = (mant / mant_max) * (2.0 ** (1 - bias)) # subnormal
else:
val = (1.0 + mant / mant_max) * (2.0 ** (exp - bias)) # normal
table[b] = np.float32(-val if sign else val)
return table


def _fnuz_encode(x: np.ndarray, exp_bits: int, mant_bits: int) -> np.ndarray:
"""Encode fp32 -> nearest 8-bit FNUZ float, returned as a uint8 bit pattern."""
table = _fnuz_decode_table(exp_bits, mant_bits)
sign_byte = np.uint8(1 << (exp_bits + mant_bits)) # 0x80

# Positive half (bytes 0..127) holds every non-negative magnitude, sorted.
# Compare in float64: for very large inputs the gap between the two top
# magnitudes is below fp32 resolution, which would tie and mis-saturate.
pos_mag = table[: int(sign_byte)].astype(np.float64)
order = np.argsort(pos_mag)
sorted_mag = pos_mag[order]
sorted_byte = order.astype(np.uint8)

xf = np.ascontiguousarray(x, dtype=np.float32)
ax = np.abs(xf).astype(np.float64)
# Both neighbours come from the raw insertion point: raw==size saturates to
# the top magnitude (lo==hi), raw==0 pins to zero, otherwise compare the two.
raw = np.searchsorted(sorted_mag, ax)
hi = np.clip(raw, 0, sorted_mag.size - 1)
lo = np.clip(raw - 1, 0, sorted_mag.size - 1)
pick_lo = np.abs(sorted_mag[lo] - ax) <= np.abs(sorted_mag[hi] - ax)
chosen = np.where(pick_lo, lo, hi)
out = sorted_byte[chosen]

# Apply sign, but never the 0x80 (-0 == NaN) slot: zeros stay +0.
is_zero = sorted_mag[chosen] == 0
out = np.where((xf < 0) & ~is_zero, out | sign_byte, out)
out = np.where(np.isnan(xf), sign_byte, out) # NaN inputs -> NaN byte
return out.astype(np.uint8).reshape(np.shape(x))


def _fp32_to_fp8_u8(x: np.ndarray) -> np.ndarray:
"""Encode fp32 -> fp8 E4M3 (FNUZ) bit pattern in a uint8 array."""
return _fnuz_encode(x, exp_bits=4, mant_bits=3)


def _fp8_u8_to_fp32(u8: np.ndarray) -> np.ndarray:
"""Decode an fp8 E4M3 (FNUZ) bit pattern back to fp32."""
return _fnuz_decode_table(4, 3)[u8.astype(np.intp)]


def _fp32_to_bf8_u8(x: np.ndarray) -> np.ndarray:
"""Encode fp32 -> bf8 E5M2 (FNUZ) bit pattern in a uint8 array."""
return _fnuz_encode(x, exp_bits=5, mant_bits=2)


def _bf8_u8_to_fp32(u8: np.ndarray) -> np.ndarray:
"""Decode a bf8 E5M2 (FNUZ) bit pattern back to fp32."""
return _fnuz_decode_table(5, 2)[u8.astype(np.intp)]


# Output (C) element dtype for an A/B element dtype, mirroring the codegen's
# CommonTypeMappings.get_output_dtype: fp8/bf8 accumulate into fp16, int8 into
# int32, everything else stores in its own dtype.
_OUTPUT_DTYPE = {"fp8": "fp16", "bf8": "fp16", "int8": "int32"}


def _output_dtype(dtype: str) -> str:
return _OUTPUT_DTYPE.get(dtype, dtype)


def _dtype_from_kernel_name(name: str) -> str:
"""Extract the dtype token from a kernel name like ``gemm_<dtype>_<layout>_...``."""
parts = name.split("_")
Expand Down Expand Up @@ -461,20 +562,39 @@ def run(
B_lay = B if lb == "r" else B.T
C_shape = (M, N) if lc == "r" else (N, M)

# Build A/B host buffers in the kernel's element dtype. The encode
# helpers (bf16/fp8/bf8) already force a contiguous float32 source, so an
# outer ascontiguousarray would only add a redundant copy; the native
# numpy dtypes (fp16/int8) still need it.
if dtype == "bf16":
# _fp32_to_bf16_u16 already forces a contiguous float32 buffer, so
# an outer ascontiguousarray here would only add a redundant copy.
A_h = _fp32_to_bf16_u16(A_lay)
B_h = _fp32_to_bf16_u16(B_lay)
C_h = np.zeros(C_shape, dtype=np.uint16)
elif dtype == "fp8":
A_h = _fp32_to_fp8_u8(A_lay)
B_h = _fp32_to_fp8_u8(B_lay)
elif dtype == "bf8":
A_h = _fp32_to_bf8_u8(A_lay)
B_h = _fp32_to_bf8_u8(B_lay)
elif dtype == "int8":
A_h = np.ascontiguousarray(A_lay, dtype=np.int8)
B_h = np.ascontiguousarray(B_lay, dtype=np.int8)
else: # fp16 (default)
A_h = np.ascontiguousarray(A_lay, dtype=np.float16)
B_h = np.ascontiguousarray(B_lay, dtype=np.float16)
C_h = np.zeros(C_shape, dtype=np.float16)

# The C buffer's element size must equal sizeof(CDataType): fp8/bf8
# accumulate into fp16, int8 into int32, otherwise the input dtype.
out_dtype = _output_dtype(dtype)
_C_NP = {"fp16": np.float16, "bf16": np.uint16, "int32": np.int32}
C_h = np.zeros(C_shape, dtype=_C_NP.get(out_dtype, np.float16))

status, time_ms = self.lib.run(A_h, B_h, C_h, M, N, K)

C_dec = _bf16_u16_to_fp32(C_h) if dtype == "bf16" else C_h
# Decode the output back to a comparable numeric array.
if out_dtype == "bf16":
C_dec = _bf16_u16_to_fp32(C_h)
else: # fp16 / int32 are already directly comparable
C_dec = C_h
C_out = C_dec if lc == "r" else C_dec.T

tflops = (problem.flops / (time_ms * 1e-3)) / 1e12 if time_ms > 0 else 0.0
Expand Down Expand Up @@ -850,7 +970,8 @@ def expand_sweep(
c = GemmKernelConfig(
dtype_a=dtype,
dtype_b=dtype,
dtype_c=dtype,
dtype_c=_output_dtype(dtype),
dtype_acc=("int32" if dtype == "int8" else "fp32"),
layout_a=_LAYOUT_WORD[la],
layout_b=_LAYOUT_WORD[lb],
layout_c=_LAYOUT_WORD[lc],
Expand Down
Loading
Loading