mirror of
https://github.com/tile-ai/tilelang.git
synced 2026-10-02 06:34:36 +08:00
[AMD][CDNA4] Add MXFP4 (FP4 E2M1) support for gfx950 (#2132)
* add amd gfx950 mxfp4 support * update the codes for format checking * [AMD][CDNA4] Address review comments for MXFP4 gfx950 support Four issues from CodeRabbit review fixed: 1. hip_fp4.h: Fix FP4 E2M1 denormal decoder round-trip bug. The decoder returned 0.25f for mant==1 (exp==0) but the encoder maps nibble 1 to 0.5f, breaking float→fp4→float round-trips. Fix: return 0.5f to match the encoder and LUT tables. 2. codegen_hip.cc: Fix UB address-of-prvalue in FP4→float16 cast. The generated code took &(__tl_cvt_fp4x2_to_half2(...)), which takes the address of a temporary (C++ UB). Fix: materialize the uint1 return value into a local variable first, matching the pattern already used in the float32/bfloat16 cast paths. 3. codegen_hip.cc: Extend fp4_pair_cast to cover FP4x16 and FP4x32. Previously only 2/4/8-lane FP4 casts were handled; x16/x32 fell through silently to generic vector code that cannot index fp4_e2_* aggregates. The pairwise uint8_t byte logic is correct for any even lane count, so extend to include 16 and 32 lanes. 4. mxfp.py: Narrow bare except to expected import-time errors. The catch-all except Exception silently swallowed all errors from target_is_gfx950(), causing HIP targets to fall back to CUDA/PTX. Fix: catch only ImportError/ModuleNotFoundError/AttributeError. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * update for format checking --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: LeiWang1999 <leiwang1999@outlook.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
LeiWang1999
parent
b61e19e13d
commit
bc310e08d8
@@ -14,4 +14,3 @@
|
||||
#define __HIP_HAS_GET_PCH 1
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
MXFP4 dequantize-GEMM example for AMD gfx950 (CDNA4 / MI350).
|
||||
|
||||
Mirrors example_dequant_gemm_bf16_mxfp4_hopper.py for NVIDIA Hopper.
|
||||
Computes: C (MxN, bf16) = dequantize(B) (NxK, bf16) @ A^T (MxK, bf16)
|
||||
where B is stored as packed uint8 (2 FP4 E2M1 values per byte) with
|
||||
per-block E8M0 scale factors.
|
||||
|
||||
This file only runs on AMD gfx950. Use `@tilelang.testing.requires_gfx950`
|
||||
in test wrappers; the kernel itself is target-agnostic Python but the
|
||||
underlying HIP C++ (hip_fp4.h) is compiled only for __gfx950__.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import tilelang
|
||||
import tilelang.language as T
|
||||
from tilelang import tvm as tvm
|
||||
from tvm import DataType
|
||||
from tvm import tir
|
||||
import torch
|
||||
|
||||
from tilelang.quantize import get_mxfp_intrin_group
|
||||
from dequantize_utils import torch_convert_bit_twiddling, torch_convert
|
||||
|
||||
|
||||
def _tir_u8_to_f4_to_bf16(nbit: int, val: tir.PrimExpr, pos: tir.PrimExpr, scale: tir.PrimExpr, dtype: str):
|
||||
"""Convert a packed 4-bit FP4 field to bfloat16 (mirrors Hopper example, ignoring overflow clamp)."""
|
||||
assert nbit == 4
|
||||
assert dtype == T.bfloat16
|
||||
assert val.dtype == T.uint8
|
||||
mask = tir.const((1 << nbit) - 1, T.uint16)
|
||||
f4 = (val >> (pos.astype(T.uint16) * tir.const(nbit, T.uint16))) & mask
|
||||
s = f4 >> tir.const(3, T.uint16)
|
||||
e_f4 = (f4 & tir.const(6, T.uint16)) >> tir.const(1, T.uint16)
|
||||
e_bf16 = e_f4 + tir.const(126, T.uint16)
|
||||
m_f4 = f4 & tir.const(1, T.uint16)
|
||||
val_bf16 = tir.reinterpret(
|
||||
T.bfloat16,
|
||||
((((s << tir.const(8, T.uint16)) | e_bf16) << tir.const(7, T.uint16)) | (m_f4 << tir.const(6, T.uint16))).astype(T.uint16),
|
||||
)
|
||||
return val_bf16
|
||||
|
||||
|
||||
def _get_target():
|
||||
"""Return TVM hip target for gfx950."""
|
||||
return tvm.target.Target("hip -mcpu=gfx950")
|
||||
|
||||
|
||||
def get_configs():
|
||||
iter_params = dict(
|
||||
block_M=[64, 128, 256],
|
||||
block_N=[64, 128, 256],
|
||||
block_K=[64, 128, 256],
|
||||
num_stages=[0, 2],
|
||||
threads=[128, 256],
|
||||
split=[1, 2],
|
||||
)
|
||||
return [{k: v for k, v in zip(iter_params, values)} for values in itertools.product(*iter_params.values())]
|
||||
|
||||
|
||||
@tilelang.autotune(configs=get_configs())
|
||||
@tilelang.jit(out_idx=[-1], target="hip -mcpu=gfx950")
|
||||
def matmul(
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
in_dtype,
|
||||
out_dtype,
|
||||
accum_dtype,
|
||||
source_format=T.uint32,
|
||||
num_bits=4,
|
||||
scale_size=32,
|
||||
fast_dequant=True,
|
||||
with_bias=False,
|
||||
block_M=256,
|
||||
block_N=128,
|
||||
block_K=128,
|
||||
num_stages=2,
|
||||
threads=256,
|
||||
split=1,
|
||||
):
|
||||
"""
|
||||
MXFP4 dequantize-GEMM kernel for AMD gfx950.
|
||||
|
||||
Inputs:
|
||||
A : (M, K) in in_dtype (e.g. bfloat16)
|
||||
B : (N, K//2) uint8 -- packed 2x FP4 per byte
|
||||
Scale : (N, K//scale_size) uint8 -- E8M0 per-block exponent
|
||||
[Bias] : (M, N) out_dtype (optional)
|
||||
Output:
|
||||
C : (M, N) out_dtype
|
||||
"""
|
||||
num_elems_per_byte = 8 // num_bits
|
||||
storage_dtype = T.uint8
|
||||
QK = K // num_elems_per_byte
|
||||
Block_QK = block_K // num_elems_per_byte
|
||||
|
||||
A_shape = (M, K)
|
||||
B_shape = (N, QK)
|
||||
Bias_shape = (M, N)
|
||||
Scale_shape = (N, K // scale_size)
|
||||
A_shared_shape = (block_M, block_K)
|
||||
B_shared_shape = (block_N, Block_QK)
|
||||
Bias_shared_shape = (block_M, block_N)
|
||||
B_dequantize_shared_shape = (block_N, block_K)
|
||||
assert K % (block_K * split) == 0
|
||||
|
||||
target = _get_target()
|
||||
|
||||
# Obtain the HIP C++ dequantization intrinsic (gfx950-specific).
|
||||
mxfp_intrin_info = get_mxfp_intrin_group(
|
||||
out_dtype=in_dtype,
|
||||
source_format=source_format,
|
||||
source_bit=num_bits,
|
||||
storage_dtype=storage_dtype,
|
||||
use_twiddling=True,
|
||||
target=target,
|
||||
)
|
||||
import_source = mxfp_intrin_info["c_source"]
|
||||
func_name = mxfp_intrin_info["func_name"]
|
||||
assert import_source is not None
|
||||
assert func_name is not None
|
||||
|
||||
def get_fast_dequant_func(in_dtype="fp4", out_dtype=T.bfloat16):
|
||||
assert in_dtype in ["fp4"]
|
||||
assert out_dtype in [T.bfloat16]
|
||||
|
||||
MAX_TRANSACTION_SIZE_BITS = 128
|
||||
local_size = MAX_TRANSACTION_SIZE_BITS // DataType(out_dtype).bits
|
||||
local_compress_size = local_size // num_elems_per_byte
|
||||
|
||||
@T.macro
|
||||
def fast_dequant_bf16_fp4_twiddling(B_shared, B_dequantize_shared, Scale, k):
|
||||
T.import_source(import_source)
|
||||
|
||||
tx = T.get_thread_binding()
|
||||
bx = T.get_block_binding(0)
|
||||
|
||||
B_local_thread = T.alloc_local((local_compress_size,), storage_dtype)
|
||||
B_dequantize_local_thread = T.alloc_local((local_size,), out_dtype)
|
||||
Scale_local_thread = T.alloc_local((1,), storage_dtype)
|
||||
Scale_local_thread_exponent = T.alloc_local((1,), out_dtype)
|
||||
|
||||
for i in T.serial(0, block_N * block_K // threads // local_size):
|
||||
index_base = i * threads * local_compress_size + tx * local_compress_size
|
||||
for v in T.vectorized(0, local_compress_size):
|
||||
index = index_base + v
|
||||
B_local_thread[v] = B_shared[index // Block_QK, index % Block_QK]
|
||||
index_scale = index_base // (scale_size // num_elems_per_byte)
|
||||
si = index_scale // (block_K // scale_size)
|
||||
sj = index_scale % (block_K // scale_size)
|
||||
Scale_local_thread[0] = Scale[bx * block_N + si, k * block_K // scale_size + sj]
|
||||
Scale_local_thread_exponent[0] = T.shift_left(1, Scale_local_thread[0])
|
||||
|
||||
T.call_extern(
|
||||
func_name,
|
||||
T.access_ptr(B_local_thread, "r"),
|
||||
T.access_ptr(B_dequantize_local_thread, "w"),
|
||||
1,
|
||||
dtype=out_dtype,
|
||||
)
|
||||
|
||||
for v in T.Parallel(local_size):
|
||||
B_dequantize_local_thread[v] *= Scale_local_thread_exponent[0]
|
||||
|
||||
for v in T.vectorized(0, local_size):
|
||||
index = i * threads * local_size + tx * local_size + v
|
||||
B_dequantize_shared[index // block_K, index % block_K] = B_dequantize_local_thread[v]
|
||||
|
||||
return fast_dequant_bf16_fp4_twiddling
|
||||
|
||||
def get_simple_dequant_func(in_dtype="fp4", out_dtype=T.bfloat16):
|
||||
assert in_dtype in ["fp4"]
|
||||
assert out_dtype in [T.bfloat16]
|
||||
|
||||
@T.macro
|
||||
def simple_dequant_bf16_fp4(B_shared, B_dequantize_shared, Scale, k):
|
||||
B_local = T.alloc_fragment(B_shared_shape, storage_dtype)
|
||||
B_dequantize_local = T.alloc_fragment(B_dequantize_shared_shape, out_dtype)
|
||||
|
||||
bx = T.get_block_binding(0)
|
||||
T.copy(B_shared, B_local)
|
||||
for i, j in T.Parallel(block_N, block_K):
|
||||
B_dequantize_local[i, j] = _tir_u8_to_f4_to_bf16(
|
||||
num_bits,
|
||||
B_local[i, j // num_elems_per_byte],
|
||||
j % num_elems_per_byte,
|
||||
Scale[
|
||||
bx * block_N + i,
|
||||
k * block_K // scale_size + j // scale_size,
|
||||
],
|
||||
dtype=out_dtype,
|
||||
) * T.shift_left(
|
||||
1,
|
||||
Scale[bx * block_N + i, k * block_K // scale_size + j // scale_size],
|
||||
)
|
||||
T.copy(B_dequantize_local, B_dequantize_shared)
|
||||
|
||||
return simple_dequant_bf16_fp4
|
||||
|
||||
@T.prim_func
|
||||
def main(
|
||||
A: T.Tensor(A_shape, in_dtype),
|
||||
B: T.Tensor(B_shape, storage_dtype),
|
||||
Scale: T.Tensor(Scale_shape, storage_dtype),
|
||||
Bias: T.Tensor(Bias_shape, out_dtype),
|
||||
C: T.Tensor((M, N), out_dtype),
|
||||
):
|
||||
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=threads) as (bx, by):
|
||||
A_shared = T.alloc_shared(A_shared_shape, in_dtype)
|
||||
B_shared = T.alloc_shared(B_shared_shape, storage_dtype)
|
||||
B_dequantize_shared = T.alloc_shared(B_dequantize_shared_shape, in_dtype)
|
||||
Bias_shared = T.alloc_shared(Bias_shared_shape, out_dtype)
|
||||
C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
|
||||
C_shared = T.alloc_shared((block_M, block_N), out_dtype)
|
||||
|
||||
T.annotate_layout({B_shared: tilelang.layout.make_swizzled_layout(B_shared)})
|
||||
|
||||
if with_bias:
|
||||
T.annotate_layout({Bias_shared: tilelang.layout.make_swizzled_layout(Bias_shared)})
|
||||
|
||||
if with_bias:
|
||||
T.copy(
|
||||
Bias[by * block_M : (by + 1) * block_M, bx * block_N : (bx + 1) * block_N],
|
||||
Bias_shared,
|
||||
)
|
||||
T.copy(Bias_shared, C_local)
|
||||
else:
|
||||
T.clear(C_local)
|
||||
|
||||
for k in T.Pipelined(K // block_K, num_stages=num_stages):
|
||||
T.copy(A[by * block_M, k * block_K], A_shared)
|
||||
T.copy(B[bx * block_N, k * block_K // num_elems_per_byte], B_shared)
|
||||
if fast_dequant:
|
||||
get_fast_dequant_func()(B_shared, B_dequantize_shared, Scale, k)
|
||||
else:
|
||||
get_simple_dequant_func()(B_shared, B_dequantize_shared, Scale, k)
|
||||
T.gemm(A_shared, B_dequantize_shared, C_local, transpose_B=True)
|
||||
|
||||
T.copy(C_local, C_shared)
|
||||
T.copy(
|
||||
C_shared,
|
||||
C[by * block_M : (by + 1) * block_M, bx * block_N : (bx + 1) * block_N],
|
||||
)
|
||||
|
||||
return main
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference implementations (CPU/CUDA torch, for correctness checking)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def ref_program_twiddling(A, qB, Scale, Bias=None):
|
||||
"""Reference: dequantize via bit-twiddling then matmul."""
|
||||
B = torch_convert_bit_twiddling(qB)
|
||||
B = B * 2 ** (Scale[:, torch.arange(B.shape[1], device=B.device) // 32].float())
|
||||
C = torch.matmul(A.to(torch.float), B.T.to(torch.float))
|
||||
return C.to(torch.bfloat16)
|
||||
|
||||
|
||||
def ref_program_simple(A, qB, Scale, Bias=None):
|
||||
"""Reference: simple nibble-by-nibble dequantization then matmul."""
|
||||
B = torch_convert(qB)
|
||||
B = B * 2 ** (Scale[:, torch.arange(B.shape[1], device=B.device) // 32].float())
|
||||
C = torch.matmul(A.to(torch.float), B.T.to(torch.float))
|
||||
return C.to(torch.bfloat16)
|
||||
|
||||
|
||||
def ref_program_twiddling_with_bias(A, qB, Scale, Bias):
|
||||
B = torch_convert_bit_twiddling(qB)
|
||||
B = B * 2 ** (Scale[:, torch.arange(B.shape[1], device=B.device) // 32].float())
|
||||
C = torch.matmul(A.to(torch.float), B.T.to(torch.float)) + Bias.to(torch.float)
|
||||
return C.to(torch.bfloat16)
|
||||
|
||||
|
||||
def ref_program_simple_with_bias(A, qB, Scale, Bias):
|
||||
B = torch_convert(qB)
|
||||
B = B * 2 ** (Scale[:, torch.arange(B.shape[1], device=B.device) // 32].float())
|
||||
C = torch.matmul(A.to(torch.float), B.T.to(torch.float)) + Bias.to(torch.float)
|
||||
return C.to(torch.bfloat16)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def main(m=256, n=256, k=256, scale_size=32, fast_dequant=True, with_bias=False, tune=False):
|
||||
total_flops = 2 * m * n * k
|
||||
|
||||
if tune:
|
||||
kernel = matmul(
|
||||
m,
|
||||
n,
|
||||
k,
|
||||
T.bfloat16,
|
||||
T.bfloat16,
|
||||
T.float32,
|
||||
num_bits=4,
|
||||
scale_size=scale_size,
|
||||
fast_dequant=fast_dequant,
|
||||
with_bias=with_bias,
|
||||
)
|
||||
else:
|
||||
kernel = matmul(
|
||||
m,
|
||||
n,
|
||||
k,
|
||||
T.bfloat16,
|
||||
T.bfloat16,
|
||||
T.float32,
|
||||
num_bits=4,
|
||||
scale_size=scale_size,
|
||||
block_M=128,
|
||||
block_N=128,
|
||||
block_K=128,
|
||||
num_stages=2,
|
||||
threads=256,
|
||||
split=1,
|
||||
fast_dequant=fast_dequant,
|
||||
with_bias=with_bias,
|
||||
)
|
||||
|
||||
profiler = kernel.get_profiler(tilelang.TensorSupplyType.Auto)
|
||||
|
||||
if fast_dequant:
|
||||
ref = ref_program_twiddling_with_bias if with_bias else ref_program_twiddling
|
||||
else:
|
||||
ref = ref_program_simple_with_bias if with_bias else ref_program_simple
|
||||
|
||||
profiler.assert_allclose(ref, rtol=0.01, atol=0.01)
|
||||
print("All checks pass.")
|
||||
latency = profiler.do_bench(warmup=200)
|
||||
print(f"TileLang gfx950: {latency:.2f} ms")
|
||||
print(f"TileLang gfx950: {total_flops / latency * 1e-9:.2f} TFlops")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
M, N, K = 256, 256, 256
|
||||
scale_size = 32
|
||||
main(M, N, K, scale_size, fast_dequant=True, with_bias=False)
|
||||
main(M, N, K, scale_size, fast_dequant=False, with_bias=False)
|
||||
main(M, N, K, scale_size, fast_dequant=True, with_bias=True)
|
||||
main(M, N, K, scale_size, fast_dequant=False, with_bias=True)
|
||||
@@ -124,6 +124,30 @@ static std::string GetFP8Type(DataType type) {
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
// Returns the HIP C type string for a float4_e2m1fn DataType.
|
||||
// Only used on gfx950; caller sets enable_fp4_ before invoking.
|
||||
static std::string GetFP4Type(DataType type) {
|
||||
std::stringstream stream;
|
||||
int32_t lanes = type.lanes();
|
||||
if (type.is_scalar()) {
|
||||
stream << "fp4_e2_t";
|
||||
} else if (lanes == 2) {
|
||||
stream << "fp4_e2_2_t";
|
||||
} else if (lanes == 4) {
|
||||
stream << "fp4_e2_4_t";
|
||||
} else if (lanes == 8) {
|
||||
stream << "fp4_e2_8_t";
|
||||
} else if (lanes == 16) {
|
||||
stream << "fp4_e2_16_t";
|
||||
} else if (lanes == 32) {
|
||||
stream << "fp4_e2_32_t";
|
||||
} else {
|
||||
LOG(FATAL) << "Only support scalar and vector types of width (2,4,8,16,32) "
|
||||
"for FP4 on HIP";
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Replace patterns with replacement strings.
|
||||
* \note should use std::format instead when codebase is ported to C++20.
|
||||
@@ -219,6 +243,10 @@ std::string CodeGenTileLangHIP::Finish() {
|
||||
decl_stream << "#include <tl_templates/hip/hip_fp8.h>\n";
|
||||
}
|
||||
|
||||
if (enable_fp4_) {
|
||||
decl_stream << "#include <tl_templates/hip/hip_fp4.h>\n";
|
||||
}
|
||||
|
||||
if (need_cooperative_groups_) {
|
||||
decl_stream << "#include <hip/hip_cooperative_groups.h>\n";
|
||||
}
|
||||
@@ -360,6 +388,19 @@ void CodeGenTileLangHIP::PrintType(DataType t, std::ostream &os) { // NOLINT(*)
|
||||
enable_fp8_ = true;
|
||||
os << GetFP8Type(t);
|
||||
return;
|
||||
} else if (t.is_float4()) {
|
||||
// FP4 E2M1 is only supported on gfx950 (CDNA4). Setting enable_fp4_ will
|
||||
// cause Finish() to include hip_fp4.h which is itself guarded by
|
||||
// #if defined(__gfx950__), so this is safe to emit on all HIP targets
|
||||
// (the compiler will reject FP4 kernel code on non-gfx950 anyway).
|
||||
enable_fp4_ = true;
|
||||
if (t.lanes() <= 32) {
|
||||
os << GetFP4Type(t);
|
||||
} else {
|
||||
LOG(FATAL) << "Cannot convert FP4 type with " << t.lanes()
|
||||
<< " lanes to HIP type (max 32 lanes supported)";
|
||||
}
|
||||
return;
|
||||
} else if (t == DataType::Bool()) {
|
||||
os << "bool";
|
||||
return;
|
||||
@@ -758,6 +799,198 @@ void CodeGenTileLangHIP::VisitExpr_(const CastNode *op, std::ostream &os) {
|
||||
if (from_ty.is_scalar())
|
||||
return CodeGenC::VisitExpr_(op, os);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vectorized FP4 <-> float/half/bfloat16 conversions (gfx950 only).
|
||||
// These use the pair-wise helpers from hip_fp4.h. We process two lanes at a
|
||||
// time: odd-indexed lane is the "high" nibble, even-indexed is "low".
|
||||
// Only enabled when the FP4 header is included (enable_fp4_ is set by
|
||||
// PrintType when a float4 type is encountered).
|
||||
// ---------------------------------------------------------------------------
|
||||
int fp4_lanes = from_ty.lanes();
|
||||
// Pairwise cast: process 2 FP4 lanes at a time via packed uint8_t byte.
|
||||
// Supported lane widths: 2, 4, 8, 16, 32 (all even widths up to FP4x32).
|
||||
bool fp4_pair_cast = (fp4_lanes == 2 || fp4_lanes == 4 || fp4_lanes == 8 ||
|
||||
fp4_lanes == 16 || fp4_lanes == 32);
|
||||
|
||||
// FP4 -> float16 : use __tl_cvt_fp4x2_to_half2 per 2-element pair
|
||||
if (from_ty.is_float4_e2m1fn() && target_ty.is_float16() && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
// Iterate over pairs: src is stored as fp4_e2_{lanes}_t; we access the
|
||||
// packed byte for each pair via reinterpret as uint8_t array.
|
||||
// Materialize the uint1 result into a local variable to avoid taking the
|
||||
// address of a prvalue (C++ UB).
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::string tmp = name_supply_->FreshName("_fp4h2_");
|
||||
this->PrintIndent();
|
||||
stream << "uint1 " << tmp << " = __tl_cvt_fp4x2_to_half2(((uint8_t*)&("
|
||||
<< src << "))[" << i / 2 << "]);\n";
|
||||
PrintVecElemStore(sret, target_ty, i, "((half_t*)(&(" + tmp + ")))[0]");
|
||||
PrintVecElemStore(sret, target_ty, i + 1,
|
||||
"((half_t*)(&(" + tmp + ")))[1]");
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// FP4 -> float32 : use __tl_cvt_fp4x2_to_float2 per 2-element pair
|
||||
if (from_ty.is_float4_e2m1fn() && target_ty.is_float() &&
|
||||
target_ty.bits() == 32 && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream val;
|
||||
val << "__tl_cvt_fp4x2_to_float2(((uint8_t*)&(" << src << "))[" << i / 2
|
||||
<< "])";
|
||||
std::string tmp = name_supply_->FreshName("_fp4f2_");
|
||||
this->PrintIndent();
|
||||
stream << "float2 " << tmp << " = " << val.str() << ";\n";
|
||||
PrintVecElemStore(sret, target_ty, i, tmp + ".x");
|
||||
PrintVecElemStore(sret, target_ty, i + 1, tmp + ".y");
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// FP4 -> double : use __tl_cvt_fp4x2_to_double2 per 2-element pair
|
||||
if (from_ty.is_float4_e2m1fn() && target_ty.is_float() &&
|
||||
target_ty.bits() == 64 && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream val;
|
||||
val << "__tl_cvt_fp4x2_to_double2(((uint8_t*)&(" << src << "))[" << i / 2
|
||||
<< "])";
|
||||
std::string tmp = name_supply_->FreshName("_fp4d2_");
|
||||
this->PrintIndent();
|
||||
stream << "double2 " << tmp << " = " << val.str() << ";\n";
|
||||
PrintVecElemStore(sret, target_ty, i, tmp + ".x");
|
||||
PrintVecElemStore(sret, target_ty, i + 1, tmp + ".y");
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// FP4 -> bfloat16 : use __tl_cvt_fp4x2_to_bfloat162 per 2-element pair
|
||||
if (from_ty.is_float4_e2m1fn() && target_ty.is_bfloat16() && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream val;
|
||||
val << "__tl_cvt_fp4x2_to_bfloat162(((uint8_t*)&(" << src << "))["
|
||||
<< i / 2 << "])";
|
||||
std::string tmp = name_supply_->FreshName("_fp4bf2_");
|
||||
this->PrintIndent();
|
||||
stream << "uint1 " << tmp << " = " << val.str() << ";\n";
|
||||
PrintVecElemStore(sret, target_ty, i,
|
||||
"((bfloat16_t*)(&(" + tmp + ")))[0]");
|
||||
PrintVecElemStore(sret, target_ty, i + 1,
|
||||
"((bfloat16_t*)(&(" + tmp + ")))[1]");
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// float16 -> FP4
|
||||
if (from_ty.is_float16() && target_ty.is_float4_e2m1fn() && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream h0, h1;
|
||||
PrintVecElemLoad(src, from_ty, i, h0);
|
||||
PrintVecElemLoad(src, from_ty, i + 1, h1);
|
||||
std::string tmp = name_supply_->FreshName("_h2fp4_");
|
||||
this->PrintIndent();
|
||||
stream << "uint1 " << tmp << " = uint1{__pack_half2(" << h0.str() << ", "
|
||||
<< h1.str() << ")};\n";
|
||||
this->PrintIndent();
|
||||
stream << "((uint8_t*)&(" << sret << "))[" << i / 2
|
||||
<< "] = __tl_cvt_half2_to_fp4x2(" << tmp << ");\n";
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// float32 -> FP4
|
||||
if (from_ty.is_float() && from_ty.bits() == 32 &&
|
||||
target_ty.is_float4_e2m1fn() && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream f0, f1;
|
||||
PrintVecElemLoad(src, from_ty, i, f0);
|
||||
PrintVecElemLoad(src, from_ty, i + 1, f1);
|
||||
this->PrintIndent();
|
||||
stream << "((uint8_t*)&(" << sret << "))[" << i / 2
|
||||
<< "] = __tl_cvt_float2_to_fp4x2("
|
||||
<< "float2{" << f0.str() << ", " << f1.str() << "});\n";
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// double -> FP4
|
||||
if (from_ty.is_float() && from_ty.bits() == 64 &&
|
||||
target_ty.is_float4_e2m1fn() && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream d0, d1;
|
||||
PrintVecElemLoad(src, from_ty, i, d0);
|
||||
PrintVecElemLoad(src, from_ty, i + 1, d1);
|
||||
this->PrintIndent();
|
||||
stream << "((uint8_t*)&(" << sret << "))[" << i / 2
|
||||
<< "] = __tl_cvt_double2_to_fp4x2("
|
||||
<< "double2{" << d0.str() << ", " << d1.str() << "});\n";
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// bfloat16 -> FP4
|
||||
if (from_ty.is_bfloat16() && target_ty.is_float4_e2m1fn() && fp4_pair_cast) {
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
this->PrintIndent();
|
||||
this->PrintType(target_ty, stream);
|
||||
stream << ' ' << sret << ";\n";
|
||||
std::string src = SSAGetID(PrintExpr(op->value), from_ty);
|
||||
for (int i = 0; i < fp4_lanes; i += 2) {
|
||||
std::ostringstream b0, b1;
|
||||
PrintVecElemLoad(src, from_ty, i, b0);
|
||||
PrintVecElemLoad(src, from_ty, i + 1, b1);
|
||||
std::string tmp = name_supply_->FreshName("_bf2fp4_");
|
||||
this->PrintIndent();
|
||||
stream << "uint1 " << tmp << " = uint1{__pack_bfloat162(" << b0.str()
|
||||
<< ", " << b1.str() << ")};\n";
|
||||
this->PrintIndent();
|
||||
stream << "((uint8_t*)&(" << sret << "))[" << i / 2
|
||||
<< "] = __tl_cvt_bfloat162_to_fp4x2(" << tmp << ");\n";
|
||||
}
|
||||
os << sret;
|
||||
return;
|
||||
}
|
||||
|
||||
// We could emit make_float4 like calls, but the emitted code looks
|
||||
// too compact to read. Emit this as vectorized unary ops.
|
||||
std::string sret = name_supply_->FreshName("_");
|
||||
@@ -848,6 +1081,24 @@ std::string CodeGenTileLangHIP::GetBufferRef(DataType t,
|
||||
if (alloc_storage_scope_.count(buffer_var)) {
|
||||
scope = alloc_storage_scope_.at(buffer_var);
|
||||
}
|
||||
|
||||
// FP4 scalar access on gfx950: redirect to tl_fp4_packed_load helper.
|
||||
// Non-scalar FP4 accesses fall through to the normal path (the vector
|
||||
// types fp4_e2_4_t etc. are directly addressable as structs).
|
||||
if (t.is_float4() && t.is_scalar()) {
|
||||
std::string idx_str = PrintExpr(index);
|
||||
auto packed_it = fp4_packed_buffers_.find(buffer_var);
|
||||
if (packed_it != fp4_packed_buffers_.end()) {
|
||||
// Packed local buffer: use the pre-allocated fp4_e2_2_t array.
|
||||
os << "tl_fp4_packed_load(" << packed_it->second << ", " << idx_str
|
||||
<< ")";
|
||||
} else {
|
||||
// Non-packed (e.g. shared) buffer: reinterpret as fp4_e2_2_t*.
|
||||
os << "tl_fp4_packed_load((fp4_e2_2_t*)" << vid << ", " << idx_str << ")";
|
||||
}
|
||||
return os.str();
|
||||
}
|
||||
|
||||
// bool is_vol = IsVolatile(buffer_var);
|
||||
// always false for tl cutlass backend.
|
||||
bool is_vol = false;
|
||||
@@ -1480,8 +1731,17 @@ void CodeGenTileLangHIP::VisitStmt_(const AllocBufferNode *op) {
|
||||
|
||||
this->PrintIndent();
|
||||
std::string scope = GetPtrStorageScope(op->buffer->data);
|
||||
PrintStorageScope(scope, stream);
|
||||
PrintType(op->buffer->dtype, stream);
|
||||
DataType dtype = op->buffer->dtype;
|
||||
|
||||
// FP4 scalar local buffers use packed storage (2 elements per byte).
|
||||
// Skip the normal type+scope header; emit the packed type directly below.
|
||||
bool is_fp4_scalar_local =
|
||||
dtype.is_float4() && dtype.is_scalar() && scope == "local";
|
||||
|
||||
if (!is_fp4_scalar_local) {
|
||||
PrintStorageScope(scope, stream);
|
||||
PrintType(dtype, stream);
|
||||
}
|
||||
|
||||
if (scope == "shared.dyn") {
|
||||
stream << ' ' << vid << "[];\n";
|
||||
@@ -1499,7 +1759,16 @@ void CodeGenTileLangHIP::VisitStmt_(const AllocBufferNode *op) {
|
||||
constant_size = constant_size / (32 / op->buffer->dtype.bits());
|
||||
}
|
||||
|
||||
if (scope == "local.var") {
|
||||
if (is_fp4_scalar_local) {
|
||||
// Use fp4_e2_2_t (2 elements per byte) as packed storage for FP4
|
||||
// local register arrays. Record the mapping so BufferLoad/Store
|
||||
// can emit tl_fp4_packed_load / tl_fp4_packed_store calls.
|
||||
enable_fp4_ = true;
|
||||
auto vid_packed = vid + "_packed";
|
||||
stream << "fp4_e2_2_t " << vid_packed << '[' << (constant_size + 1) / 2
|
||||
<< "];\n";
|
||||
fp4_packed_buffers_[op->buffer->data.get()] = vid_packed;
|
||||
} else if (scope == "local.var") {
|
||||
// Single-element variable: emit an initializer so the value is defined.
|
||||
// Default to 0; respect the user-provided tl.local_var_init annotation.
|
||||
PrimExpr init = tirx::make_const(op->buffer->dtype, 0);
|
||||
@@ -1521,6 +1790,38 @@ void CodeGenTileLangHIP::VisitStmt_(const AllocBufferNode *op) {
|
||||
RegisterHandleType(op->buffer->data.get(), op->buffer->dtype);
|
||||
}
|
||||
|
||||
void CodeGenTileLangHIP::VisitStmt_(const BufferStoreNode *op) {
|
||||
ICHECK_EQ(op->indices.size(), 1) << "Store to non-flat memory not supported.";
|
||||
ICHECK(!op->predicate.defined())
|
||||
<< "Predicated buffer store is not supported.";
|
||||
|
||||
DataType value_dtype = op->value.dtype();
|
||||
DataType element_dtype = op->buffer->dtype;
|
||||
Var buffer_var = op->buffer->data;
|
||||
|
||||
// FP4 scalar store: use tl_fp4_packed_store to correctly handle nibble-level
|
||||
// writes without corrupting the neighbouring nibble.
|
||||
if (element_dtype.is_float4() && element_dtype.is_scalar() &&
|
||||
value_dtype.is_scalar()) {
|
||||
std::string idx_str = PrintExpr(op->indices[0]);
|
||||
std::string value = this->PrintExpr(op->value);
|
||||
this->PrintIndent();
|
||||
auto packed_it = fp4_packed_buffers_.find(buffer_var.get());
|
||||
if (packed_it != fp4_packed_buffers_.end()) {
|
||||
stream << "tl_fp4_packed_store(" << packed_it->second << ", " << idx_str
|
||||
<< ", " << value << ");\n";
|
||||
} else {
|
||||
stream << "tl_fp4_packed_store((fp4_e2_2_t*)"
|
||||
<< GetVarID(buffer_var.get()) << ", " << idx_str << ", " << value
|
||||
<< ");\n";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Default path for all other types.
|
||||
CodeGenC::VisitStmt_(op);
|
||||
}
|
||||
|
||||
void CodeGenTileLangHIP::VisitExpr_(const RampNode *op, std::ostream &os) {
|
||||
int lanes = static_cast<int>(Downcast<IntImm>(op->lanes)->value);
|
||||
ICHECK_LE(lanes, 4)
|
||||
|
||||
@@ -53,6 +53,7 @@ public:
|
||||
void VisitExpr_(const ShuffleNode *op, std::ostream &os) final; // NOLINT(*)
|
||||
void VisitStmt_(const AllocBufferNode *op) final;
|
||||
void VisitStmt_(const AttrStmtNode *op) final;
|
||||
void VisitStmt_(const BufferStoreNode *op) final;
|
||||
|
||||
// Override this as a work around for __grid_constant__ parameter
|
||||
void AddFunction(const PrimFunc &f);
|
||||
@@ -83,6 +84,10 @@ private:
|
||||
bool need_wmma_h_{false};
|
||||
// whether need fp8.h
|
||||
bool enable_fp8_{false};
|
||||
// whether need hip_fp4.h (gfx950 only)
|
||||
bool enable_fp4_{false};
|
||||
// Map from FP4 buffer VarNode* to packed buffer variable name (gfx950)
|
||||
std::unordered_map<const tir::VarNode *, std::string> fp4_packed_buffers_;
|
||||
// The size of the barrier array in shared memory
|
||||
int barrier_count_ = -1;
|
||||
// whether need mma.h
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// FP4 E2M1 support for AMD gfx950 (CDNA4 / MI350).
|
||||
// All device types and conversion helpers are guarded by __gfx950__ so that
|
||||
// this header is safe to include on any ROCm target but only activates on
|
||||
// CDNA4. The CUDA equivalent is tl_templates/cuda/cuda_fp4.h.
|
||||
#if defined(__gfx950__)
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scalar FP4 type (fp4_e2_t)
|
||||
// Stores one E2M1 value in the low 4 bits of a uint8_t.
|
||||
// Layout: bit3 = sign, bits[2:1] = exponent, bit0 = mantissa.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct fp4_e2_t {
|
||||
uint8_t __x; // only low 4 bits are used
|
||||
|
||||
TL_DEVICE fp4_e2_t() = default;
|
||||
TL_DEVICE explicit fp4_e2_t(uint8_t raw) : __x(raw & 0x0Fu) {}
|
||||
|
||||
// Convert FP4 E2M1 to float (pure bit manipulation, no hardware intrinsic).
|
||||
// E2M1 encoding: value = (-1)^s * 2^(e-1) * (1 + m*0.5) for e != 0
|
||||
// value = (-1)^s * 0.5 * m for e == 0
|
||||
TL_DEVICE operator float() const {
|
||||
uint8_t bits = __x & 0x0Fu;
|
||||
if (bits == 0u)
|
||||
return 0.0f;
|
||||
uint32_t sign = (bits >> 3u) & 0x1u;
|
||||
uint32_t exp = (bits >> 1u) & 0x3u;
|
||||
uint32_t mant = bits & 0x1u;
|
||||
float result;
|
||||
if (exp == 0u) {
|
||||
// Denormal: value = (-1)^s * 0.5 * m (mant==1 => 0.5, matching encoder
|
||||
// nibble 1)
|
||||
result = mant ? 0.5f : 0.0f;
|
||||
} else {
|
||||
// Normal: value = (-1)^s * 2^(e-1) * (1 + m*0.5)
|
||||
float mantissa = 1.0f + mant * 0.5f;
|
||||
float scale = 1.0f;
|
||||
int e = (int)exp - 1;
|
||||
if (e >= 0) {
|
||||
for (int i = 0; i < e; ++i)
|
||||
scale *= 2.0f;
|
||||
} else {
|
||||
scale = 0.5f;
|
||||
}
|
||||
result = mantissa * scale;
|
||||
}
|
||||
return sign ? -result : result;
|
||||
}
|
||||
|
||||
TL_DEVICE operator half_t() const { return (half_t)(float)(*this); }
|
||||
TL_DEVICE operator bfloat16_t() const { return (bfloat16_t)(float)(*this); }
|
||||
};
|
||||
|
||||
// Convert float to FP4 E2M1 (round to nearest, saturate).
|
||||
TL_DEVICE fp4_e2_t __tl_float_to_fp4(float x) {
|
||||
// FP4 E2M1 representable values (positive):
|
||||
// 0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0
|
||||
const float fp4_max = 6.0f;
|
||||
const float fp4_vals[8] = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f};
|
||||
uint8_t sign = 0u;
|
||||
if (x < 0.0f) {
|
||||
sign = 1u;
|
||||
x = -x;
|
||||
}
|
||||
if (x > fp4_max)
|
||||
x = fp4_max;
|
||||
// Find the closest representable value by brute-force over 8 candidates.
|
||||
uint8_t best = 0u;
|
||||
float best_diff = x; // diff from 0
|
||||
for (uint8_t i = 1u; i < 8u; ++i) {
|
||||
float diff = x - fp4_vals[i];
|
||||
if (diff < 0.0f)
|
||||
diff = -diff;
|
||||
if (diff < best_diff) {
|
||||
best_diff = diff;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
// Encode: bit3=sign, bits[2:1]=exp, bit0=mant
|
||||
uint8_t enc = (uint8_t)((sign << 3u) | best);
|
||||
fp4_e2_t r;
|
||||
r.__x = enc;
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Packed 2xFP4 type (fp4_e2_2_t)
|
||||
// Two FP4 values stored in one byte: low nibble = first, high nibble = second.
|
||||
// ---------------------------------------------------------------------------
|
||||
class fp4_e2_2_t {
|
||||
public:
|
||||
uint8_t __x; // packed storage
|
||||
|
||||
TL_DEVICE fp4_e2_2_t() = default;
|
||||
TL_DEVICE explicit fp4_e2_2_t(uint8_t data) : __x(data) {}
|
||||
|
||||
TL_DEVICE fp4_e2_t x() const { return fp4_e2_t(__x & 0x0Fu); }
|
||||
TL_DEVICE fp4_e2_t y() const { return fp4_e2_t((__x >> 4u) & 0x0Fu); }
|
||||
|
||||
TL_DEVICE void set_x(fp4_e2_t val) {
|
||||
__x = (__x & 0xF0u) | (val.__x & 0x0Fu);
|
||||
}
|
||||
TL_DEVICE void set_y(fp4_e2_t val) {
|
||||
__x = (__x & 0x0Fu) | ((val.__x & 0x0Fu) << 4u);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vector FP4 types (fp4_e2_4_t .. fp4_e2_32_t)
|
||||
// Each stores 2*N elements in N bytes via nested fp4_e2_2_t.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct __attribute__((aligned(2))) fp4_e2_4_t {
|
||||
fp4_e2_2_t x;
|
||||
fp4_e2_2_t y;
|
||||
};
|
||||
|
||||
struct __attribute__((aligned(4))) fp4_e2_8_t {
|
||||
fp4_e2_4_t x;
|
||||
fp4_e2_4_t y;
|
||||
};
|
||||
|
||||
struct __attribute__((aligned(8))) fp4_e2_16_t {
|
||||
fp4_e2_8_t x;
|
||||
fp4_e2_8_t y;
|
||||
};
|
||||
|
||||
struct __attribute__((aligned(16))) fp4_e2_32_t {
|
||||
fp4_e2_16_t x;
|
||||
fp4_e2_16_t y;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pack helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
TL_DEVICE fp4_e2_2_t make_fp4_e2_2_t(fp4_e2_t x, fp4_e2_t y) {
|
||||
return fp4_e2_2_t((uint8_t)((x.__x & 0x0Fu) | ((y.__x & 0x0Fu) << 4u)));
|
||||
}
|
||||
|
||||
TL_DEVICE fp4_e2_4_t make_fp4_e2_4_t(fp4_e2_t x0, fp4_e2_t x1, fp4_e2_t x2,
|
||||
fp4_e2_t x3) {
|
||||
fp4_e2_4_t r;
|
||||
r.x = make_fp4_e2_2_t(x0, x1);
|
||||
r.y = make_fp4_e2_2_t(x2, x3);
|
||||
return r;
|
||||
}
|
||||
|
||||
TL_DEVICE fp4_e2_8_t make_fp4_e2_8_t(fp4_e2_t x0, fp4_e2_t x1, fp4_e2_t x2,
|
||||
fp4_e2_t x3, fp4_e2_t x4, fp4_e2_t x5,
|
||||
fp4_e2_t x6, fp4_e2_t x7) {
|
||||
fp4_e2_8_t r;
|
||||
r.x = make_fp4_e2_4_t(x0, x1, x2, x3);
|
||||
r.y = make_fp4_e2_4_t(x4, x5, x6, x7);
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FP4 <-> Half2 conversions
|
||||
// half2 on HIP is __hip_fp16x2 / float16x2 but is accessed as uint1 (packed).
|
||||
// We work through float as the intermediate type for correctness.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// fp4x2 (1 packed byte) -> 2 x half_t, returned as uint1 (HIP half2 storage)
|
||||
TL_DEVICE uint1 __tl_cvt_fp4x2_to_half2(uint8_t src) {
|
||||
fp4_e2_2_t packed(src);
|
||||
half_t lo = (half_t)(float)packed.x();
|
||||
half_t hi = (half_t)(float)packed.y();
|
||||
return uint1{__pack_half2(lo, hi)};
|
||||
}
|
||||
|
||||
// 2 x half_t (as uint1) -> fp4x2 packed byte
|
||||
TL_DEVICE uint8_t __tl_cvt_half2_to_fp4x2(uint1 src) {
|
||||
half_t lo, hi;
|
||||
// unpack via reinterpret: HIP stores half2 as two consecutive 16-bit values
|
||||
const uint32_t raw = src.x;
|
||||
lo = *reinterpret_cast<const half_t *>(&raw);
|
||||
const uint16_t raw_hi = (uint16_t)(raw >> 16u);
|
||||
hi = *reinterpret_cast<const half_t *>(&raw_hi);
|
||||
fp4_e2_t fp4_lo = __tl_float_to_fp4((float)lo);
|
||||
fp4_e2_t fp4_hi = __tl_float_to_fp4((float)hi);
|
||||
return make_fp4_e2_2_t(fp4_lo, fp4_hi).__x;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FP4 <-> Float2 conversions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TL_DEVICE float2 __tl_cvt_fp4x2_to_float2(uint8_t src) {
|
||||
fp4_e2_2_t packed(src);
|
||||
return float2{(float)packed.x(), (float)packed.y()};
|
||||
}
|
||||
|
||||
TL_DEVICE uint8_t __tl_cvt_float2_to_fp4x2(float2 src) {
|
||||
fp4_e2_t lo = __tl_float_to_fp4(src.x);
|
||||
fp4_e2_t hi = __tl_float_to_fp4(src.y);
|
||||
return make_fp4_e2_2_t(lo, hi).__x;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FP4 <-> Double2 conversions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TL_DEVICE double2 __tl_cvt_fp4x2_to_double2(uint8_t src) {
|
||||
float2 f = __tl_cvt_fp4x2_to_float2(src);
|
||||
return double2{(double)f.x, (double)f.y};
|
||||
}
|
||||
|
||||
TL_DEVICE uint8_t __tl_cvt_double2_to_fp4x2(double2 src) {
|
||||
return __tl_cvt_float2_to_fp4x2(float2{(float)src.x, (float)src.y});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FP4 <-> BFloat162 conversions
|
||||
// bfloat162 on HIP: we use uint1 (same as half2 storage pattern)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TL_DEVICE uint1 __tl_cvt_fp4x2_to_bfloat162(uint8_t src) {
|
||||
fp4_e2_2_t packed(src);
|
||||
bfloat16_t lo = (bfloat16_t)(float)packed.x();
|
||||
bfloat16_t hi = (bfloat16_t)(float)packed.y();
|
||||
return uint1{__pack_bfloat162(lo, hi)};
|
||||
}
|
||||
|
||||
TL_DEVICE uint8_t __tl_cvt_bfloat162_to_fp4x2(uint1 src) {
|
||||
const uint32_t raw = src.x;
|
||||
bfloat16_t lo = *reinterpret_cast<const bfloat16_t *>(&raw);
|
||||
const uint16_t raw_hi = (uint16_t)(raw >> 16u);
|
||||
bfloat16_t hi = *reinterpret_cast<const bfloat16_t *>(&raw_hi);
|
||||
fp4_e2_t fp4_lo = __tl_float_to_fp4((float)lo);
|
||||
fp4_e2_t fp4_hi = __tl_float_to_fp4((float)hi);
|
||||
return make_fp4_e2_2_t(fp4_lo, fp4_hi).__x;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Packed buffer access helpers
|
||||
// Mirrors tl_fp4_packed_load / tl_fp4_packed_store from cuda_fp4.h.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Load a single FP4 element from a packed fp4_e2_2_t array.
|
||||
// idx is the logical index of the FP4 element (2 elements per array entry).
|
||||
TL_DEVICE fp4_e2_t tl_fp4_packed_load(fp4_e2_2_t *packed, int idx) {
|
||||
return (idx & 1) ? packed[idx >> 1].y() : packed[idx >> 1].x();
|
||||
}
|
||||
|
||||
// Store a single FP4 element into a packed fp4_e2_2_t array.
|
||||
TL_DEVICE void tl_fp4_packed_store(fp4_e2_2_t *packed, int idx, fp4_e2_t val) {
|
||||
if (idx & 1) {
|
||||
packed[idx >> 1].set_y(val);
|
||||
} else {
|
||||
packed[idx >> 1].set_x(val);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // defined(__gfx950__)
|
||||
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Functional tests for MXFP4 / FP4 E2M1 support on AMD gfx950 (CDNA4 / MI350).
|
||||
|
||||
All tests are guarded by @tilelang.testing.requires_gfx950 and are silently
|
||||
skipped on non-gfx950 AMD targets (gfx90a, gfx942, RDNA) and on NVIDIA GPUs.
|
||||
|
||||
Test coverage:
|
||||
1. FP4 copy operations (global -> shared -> local, cross-type)
|
||||
2. Vectorized FP4 <-> float16 / float32 / bfloat16 casts
|
||||
3. MXFP4 dequantize-GEMM (fast twiddling + simple path)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
import tilelang
|
||||
import tilelang.testing
|
||||
import tilelang.language as T
|
||||
from tilelang import tvm as tvm
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _hip_target():
|
||||
return "hip -mcpu=gfx950"
|
||||
|
||||
|
||||
def _fp4_encode(vals):
|
||||
"""Encode a list of floats to packed uint8 FP4 E2M1 (2 per byte)."""
|
||||
fp4_values = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0]
|
||||
|
||||
def encode_one(v):
|
||||
best_idx, best_diff = 0, abs(v - fp4_values[0])
|
||||
for idx, fv in enumerate(fp4_values):
|
||||
diff = abs(v - fv)
|
||||
if diff < best_diff:
|
||||
best_diff, best_idx = diff, idx
|
||||
return best_idx
|
||||
|
||||
nibbles = [encode_one(v) for v in vals]
|
||||
assert len(nibbles) % 2 == 0
|
||||
return bytes([(nibbles[i] & 0xF) | ((nibbles[i + 1] & 0xF) << 4) for i in range(0, len(nibbles), 2)])
|
||||
|
||||
|
||||
def _fp4_decode(packed: bytes):
|
||||
"""Decode packed uint8 FP4 E2M1 to list of floats."""
|
||||
lut = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0]
|
||||
result = []
|
||||
for byte in packed:
|
||||
result.append(lut[byte & 0xF])
|
||||
result.append(lut[(byte >> 4) & 0xF])
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 1: FP4 copy — shared-memory round-trip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tilelang.testing.requires_gfx950
|
||||
def test_fp4_copy_shared_roundtrip():
|
||||
"""FP4 values survive a global->shared->global round-trip without corruption."""
|
||||
N = 128 # number of FP4 elements (64 bytes packed)
|
||||
QN = N // 2
|
||||
|
||||
# Use out_idx=[-1] so dst is allocated by the JIT wrapper.
|
||||
@tilelang.jit(out_idx=[-1], target=_hip_target())
|
||||
def copy_kernel(N, QN):
|
||||
@T.prim_func
|
||||
def main(
|
||||
src: T.Tensor((QN,), T.uint8),
|
||||
dst: T.Tensor((QN,), T.uint8),
|
||||
):
|
||||
with T.Kernel(1, threads=64):
|
||||
src_sh = T.alloc_shared((QN,), T.uint8)
|
||||
T.copy(src[0:QN], src_sh)
|
||||
T.copy(src_sh, dst[0:QN])
|
||||
|
||||
return main
|
||||
|
||||
kernel = copy_kernel(N, QN)
|
||||
|
||||
vals = [1.0, -1.5, 2.0, 0.5, 3.0, -3.0, 0.0, 1.0] * (N // 8)
|
||||
packed_bytes = _fp4_encode(vals)
|
||||
packed = torch.tensor(list(packed_bytes), dtype=torch.uint8).cuda()
|
||||
# out_idx=[-1] means dst is allocated internally; pass only src.
|
||||
out = kernel(packed)
|
||||
assert torch.all(packed == out), "FP4 shared-memory copy corrupted data"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 2: FP4 -> float16 vectorized cast
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tilelang.testing.requires_gfx950
|
||||
def test_fp4_to_float16_cast():
|
||||
"""FP4 -> float16 cast: dequantize packed uint8 to float16 via the simple path."""
|
||||
# The FP4->F16 cast is exercised inside the dequantize GEMM kernel (simple path).
|
||||
# This test validates that the simple dequantize path compiles and produces
|
||||
# correct results for a small problem size.
|
||||
import sys
|
||||
import os
|
||||
|
||||
_examples_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "examples", "dequantize_gemm")
|
||||
sys.path.insert(0, _examples_dir)
|
||||
from example_dequant_gemm_bf16_mxfp4_cdna4 import matmul, ref_program_simple
|
||||
|
||||
M, N, K = 64, 64, 64
|
||||
kernel = matmul(
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
T.bfloat16,
|
||||
T.bfloat16,
|
||||
T.float32,
|
||||
num_bits=4,
|
||||
scale_size=32,
|
||||
block_M=64,
|
||||
block_N=64,
|
||||
block_K=64,
|
||||
num_stages=0,
|
||||
threads=128,
|
||||
split=1,
|
||||
fast_dequant=False,
|
||||
with_bias=False,
|
||||
)
|
||||
profiler = kernel.get_profiler(tilelang.TensorSupplyType.Auto)
|
||||
profiler.assert_allclose(ref_program_simple, rtol=0.02, atol=0.02)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 3: MXFP4 dequantize GEMM - simple path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tilelang.testing.requires_gfx950
|
||||
@pytest.mark.parametrize("M,N,K", [(256, 256, 256), (128, 512, 128)])
|
||||
def test_mxfp4_dequant_gemm_simple(M, N, K):
|
||||
"""MXFP4 dequantize-GEMM (simple path) produces correct BF16 output."""
|
||||
import sys
|
||||
import os
|
||||
|
||||
_examples_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "examples", "dequantize_gemm")
|
||||
sys.path.insert(0, _examples_dir)
|
||||
from example_dequant_gemm_bf16_mxfp4_cdna4 import matmul, ref_program_simple
|
||||
|
||||
scale_size = 32
|
||||
kernel = matmul(
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
T.bfloat16,
|
||||
T.bfloat16,
|
||||
T.float32,
|
||||
num_bits=4,
|
||||
scale_size=scale_size,
|
||||
block_M=128,
|
||||
block_N=128,
|
||||
block_K=128,
|
||||
num_stages=2,
|
||||
threads=256,
|
||||
split=1,
|
||||
fast_dequant=False,
|
||||
with_bias=False,
|
||||
)
|
||||
profiler = kernel.get_profiler(tilelang.TensorSupplyType.Auto)
|
||||
profiler.assert_allclose(ref_program_simple, rtol=0.02, atol=0.02)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 4: MXFP4 dequantize GEMM - fast twiddling path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tilelang.testing.requires_gfx950
|
||||
@pytest.mark.parametrize("M,N,K", [(256, 256, 256)])
|
||||
def test_mxfp4_dequant_gemm_twiddling(M, N, K):
|
||||
"""MXFP4 dequantize-GEMM (fast twiddling path) produces correct BF16 output."""
|
||||
import sys
|
||||
import os
|
||||
|
||||
_examples_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "examples", "dequantize_gemm")
|
||||
sys.path.insert(0, _examples_dir)
|
||||
from example_dequant_gemm_bf16_mxfp4_cdna4 import matmul, ref_program_twiddling
|
||||
|
||||
scale_size = 32
|
||||
kernel = matmul(
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
T.bfloat16,
|
||||
T.bfloat16,
|
||||
T.float32,
|
||||
num_bits=4,
|
||||
scale_size=scale_size,
|
||||
block_M=128,
|
||||
block_N=128,
|
||||
block_K=128,
|
||||
num_stages=2,
|
||||
threads=256,
|
||||
split=1,
|
||||
fast_dequant=True,
|
||||
with_bias=False,
|
||||
)
|
||||
profiler = kernel.get_profiler(tilelang.TensorSupplyType.Auto)
|
||||
profiler.assert_allclose(ref_program_twiddling, rtol=0.02, atol=0.02)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 5: get_mxfp_intrin_group returns HIP source for gfx950
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tilelang.testing.requires_gfx950
|
||||
def test_get_mxfp_intrin_group_returns_hip_source():
|
||||
"""get_mxfp_intrin_group() returns HIP C++ source (not CUDA PTX) for gfx950."""
|
||||
from tilelang.quantize import get_mxfp_intrin_group
|
||||
from tilelang import tvm
|
||||
|
||||
target = tvm.target.Target("hip -mcpu=gfx950")
|
||||
info = get_mxfp_intrin_group(
|
||||
out_dtype=T.bfloat16,
|
||||
source_bit=4,
|
||||
use_twiddling=True,
|
||||
target=target,
|
||||
)
|
||||
assert "func_name" in info and "c_source" in info
|
||||
# HIP source uses __device__ and does NOT contain PTX asm keywords
|
||||
src = info["c_source"]
|
||||
assert "__device__" in src, "Expected __device__ in HIP source"
|
||||
assert "prmt.b32" not in src, "Expected no PTX asm in HIP source (should be C++)"
|
||||
assert "decode_fp4_to_bf16_twiddling" in info["func_name"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 6: get_mxfp_intrin_group for non-gfx950 still returns CUDA PTX
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_mxfp_intrin_group_returns_ptx_for_cuda():
|
||||
"""get_mxfp_intrin_group() returns CUDA PTX source when target is None."""
|
||||
from tilelang.quantize import get_mxfp_intrin_group
|
||||
|
||||
info = get_mxfp_intrin_group(
|
||||
out_dtype=T.bfloat16,
|
||||
source_bit=4,
|
||||
use_twiddling=True,
|
||||
target=None, # default: CUDA/NV path
|
||||
)
|
||||
src = info["c_source"]
|
||||
assert "prmt.b32" in src, "Expected PTX asm in CUDA source"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
tilelang.testing.main()
|
||||
+139
-4
@@ -49,12 +49,121 @@ __device__ void decode_fp4_to_bf16_twiddling(T1 *B_local, T2 *B_local_decode, co
|
||||
"""
|
||||
|
||||
|
||||
# AMD HIP version of fp4->bf16 twiddling dequantization (gfx950 / CDNA4).
|
||||
# Implements the same bit-manipulation algorithm as the CUDA PTX version but
|
||||
# using portable C++ (no PTX inline assembly) so it compiles with HIP/clang.
|
||||
#
|
||||
# The algorithm (matching the CUDA PTX decode_fp4_to_bf16_twiddling above):
|
||||
# 1. byte-reverse the 32-bit packed word (endianness compensation)
|
||||
# 2. extract 8 FP4 E2M1 nibbles
|
||||
# 3. map each nibble to BF16 bits by bit-field placement
|
||||
# 4. multiply by the bias constant 0x7e80 (BF16 representation of 2^126*…)
|
||||
# AMD gfx950 / HIP version of fp4->bf16 twiddling dequantization.
|
||||
# Uses the same bit-manipulation algorithm as the CUDA PTX version.
|
||||
# BF16 multiplication is done via bfloat16_t (= hip_bfloat16, defined in
|
||||
# tl_templates/hip/common.h included by all TileLang HIP kernels), which
|
||||
# supports implicit float conversion so no external bf16 API headers are needed.
|
||||
# Bias constant 0x7e807e80 = two packed BF16 words each equal to 2^126.
|
||||
decode_f4_to_bf16_twiddling_hip = """
|
||||
// N = number of 4-element groups (4 packed bytes = 8 FP4 values each)
|
||||
// This implementation uses only standard C++ types (uint16_t, uint32_t, float)
|
||||
// so it compiles without any HIP type headers.
|
||||
template<typename T1, typename T2>
|
||||
__device__ void decode_fp4_to_bf16_twiddling(T1 *B_local, T2 *B_local_decode, const int N = 8) {
|
||||
// Multiply two packed BF16 values stored as uint16 each.
|
||||
// BF16 layout: [sign(1)|exp(8)|mant(7)] -- upper 16 bits of IEEE float32.
|
||||
// We convert to float, multiply, then convert back via bit manipulation.
|
||||
auto bf16_to_float = [](uint16_t b) -> float {
|
||||
uint32_t f = (uint32_t)b << 16u;
|
||||
float r;
|
||||
__builtin_memcpy(&r, &f, 4);
|
||||
return r;
|
||||
};
|
||||
auto float_to_bf16 = [](float f) -> uint16_t {
|
||||
uint32_t u;
|
||||
__builtin_memcpy(&u, &f, 4);
|
||||
return (uint16_t)(u >> 16u);
|
||||
};
|
||||
// Multiply two packed uint32 BF16x2 words element-wise.
|
||||
auto bf16x2_mul = [&](uint32_t a, uint32_t b) -> uint32_t {
|
||||
uint16_t a_lo = (uint16_t)(a & 0xFFFFu), a_hi = (uint16_t)(a >> 16u);
|
||||
uint16_t b_lo = (uint16_t)(b & 0xFFFFu), b_hi = (uint16_t)(b >> 16u);
|
||||
uint16_t r_lo = float_to_bf16(bf16_to_float(a_lo) * bf16_to_float(b_lo));
|
||||
uint16_t r_hi = float_to_bf16(bf16_to_float(a_hi) * bf16_to_float(b_hi));
|
||||
return (uint32_t)r_lo | ((uint32_t)r_hi << 16u);
|
||||
};
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N; ++i) {
|
||||
uint32_t packed;
|
||||
__builtin_memcpy(&packed, (const uint8_t*)B_local + (i << 2), 4);
|
||||
|
||||
// Byte-reverse (endianness compensation).
|
||||
uint32_t tmp = ((packed & 0xFFu) << 24u)
|
||||
| (((packed >> 8u) & 0xFFu) << 16u)
|
||||
| (((packed >> 16u) & 0xFFu) << 8u)
|
||||
| ((packed >> 24u) & 0xFFu);
|
||||
|
||||
// bias = 0x7e80_7e80 = two packed BF16 words each equal to 2^126.
|
||||
const uint32_t bias = 0x7e807e80u;
|
||||
// Mask for sign+exp[1] bits in each packed BF16 pair: 0b10000001_11000000_10000001_11000000
|
||||
const uint32_t mask_e = 0x81C081C0u;
|
||||
|
||||
uint32_t d[4];
|
||||
d[0] = bf16x2_mul(tmp & mask_e, bias);
|
||||
d[1] = bf16x2_mul((tmp << 3u) & mask_e, bias);
|
||||
d[2] = bf16x2_mul((tmp << 6u) & mask_e, bias);
|
||||
{
|
||||
// Mantissa bits (from CUDA: shl.b32+and combos for each nibble position)
|
||||
uint32_t t1 = (tmp << 1u) & 0x80008000u;
|
||||
uint32_t t2 = (tmp >> 3u) & 0x01800180u;
|
||||
uint32_t t3 = (tmp >> 7u) & 0x00400040u;
|
||||
d[3] = bf16x2_mul(t1 | t2 | t3, bias);
|
||||
}
|
||||
|
||||
// Store 8 BF16 results (big-endian nibble order matching CUDA reference).
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
reinterpret_cast<T2*>(B_local_decode)[(i << 3) + j] = reinterpret_cast<T2*>(&d[j])[1];
|
||||
reinterpret_cast<T2*>(B_local_decode)[(i << 3) + j + 4] = reinterpret_cast<T2*>(&d[j])[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Simple (non-twiddling) AMD gfx950 FP4->BF16 dequantization via float LUT.
|
||||
# Uses a static lookup table to avoid dependency on FP4 hardware intrinsics.
|
||||
# This is the fallback path when use_twiddling=False on AMD.
|
||||
decode_f4_to_bf16_simple_hip = """
|
||||
template<typename T1, typename T2>
|
||||
__device__ void decode_fp4_to_bf16(T1 *B_local, T2 *B_local_decode, const int N = 8) {
|
||||
// FP4 E2M1 lookup: nibble index -> BF16 value (via float).
|
||||
// Nibble layout: bit3=sign, bits[2:1]=exp, bit0=mant.
|
||||
static const float fp4_lut[16] = {
|
||||
0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f,
|
||||
-0.0f, -0.5f, -1.0f, -1.5f, -2.0f, -3.0f, -4.0f, -6.0f,
|
||||
};
|
||||
#pragma unroll
|
||||
for (int i = 0; i < N; ++i) {
|
||||
const uint8_t* src = (const uint8_t*)B_local + (i << 2);
|
||||
// Each byte holds 2 nibbles: low nibble first, high nibble second.
|
||||
// Output order matches the CUDA twiddling reference (interleaved by 4).
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
uint8_t byte = src[b];
|
||||
reinterpret_cast<T2*>(B_local_decode)[(i << 3) + b] = (T2)fp4_lut[byte & 0xFu];
|
||||
reinterpret_cast<T2*>(B_local_decode)[(i << 3) + b + 4] = (T2)fp4_lut[(byte >> 4u) & 0xFu];
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_mxfp_intrin_group(
|
||||
out_dtype: Literal[T.float16, T.bfloat16] = T.bfloat16,
|
||||
source_format: Literal[T.int, T.uint] = T.uint,
|
||||
source_bit: int = 4,
|
||||
storage_dtype: Literal[T.int32, T.int8, T.uint8] = T.uint8,
|
||||
use_twiddling: bool = False,
|
||||
target=None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Return metadata for an MXFP decoding intrinsic: function name and C source string.
|
||||
@@ -86,7 +195,37 @@ def get_mxfp_intrin_group(
|
||||
assert source_format in [T.int, T.uint], f"Invalid source_format: {source_format}. Expected 'int' or 'uint'."
|
||||
assert storage_dtype in [T.int32, T.int8, T.uint8], f"Invalid storage_dtype: {storage_dtype}. Expected 'int32' or 'int8' or 'uint8'."
|
||||
|
||||
# Detect AMD gfx950 target to select the HIP C++ dequantization implementation.
|
||||
# All other targets (NV, RDNA, MI300) use the default CUDA PTX path below.
|
||||
_is_gfx950 = False
|
||||
if target is not None:
|
||||
try:
|
||||
from tilelang.utils.target import target_is_gfx950
|
||||
|
||||
_is_gfx950 = target_is_gfx950(target)
|
||||
except (ImportError, ModuleNotFoundError, AttributeError):
|
||||
# target_is_gfx950 unavailable in this build; assume non-gfx950.
|
||||
pass
|
||||
|
||||
dtype_map = {T.float16: "f16", T.bfloat16: "bf16"}
|
||||
func_name = f"decode_fp{source_bit}_to_{dtype_map[out_dtype]}"
|
||||
if use_twiddling:
|
||||
func_name += "_twiddling"
|
||||
|
||||
if _is_gfx950:
|
||||
# AMD gfx950 path: use portable HIP C++ implementations.
|
||||
# The function name stays the same so the call site is unchanged.
|
||||
if use_twiddling and source_bit == 4 and out_dtype == T.bfloat16:
|
||||
return {"func_name": func_name, "c_source": decode_f4_to_bf16_twiddling_hip}
|
||||
elif not use_twiddling and source_bit == 4 and out_dtype == T.bfloat16:
|
||||
return {"func_name": func_name, "c_source": decode_f4_to_bf16_simple_hip}
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"AMD gfx950 MXFP dequant only supports source_bit=4 and out_dtype=bfloat16, "
|
||||
f"got source_bit={source_bit}, out_dtype={out_dtype}"
|
||||
)
|
||||
|
||||
# CUDA / default path: use PTX inline assembly implementations.
|
||||
key = f"fp{source_bit}_to_{dtype_map[out_dtype]}"
|
||||
if use_twiddling:
|
||||
key += "_twiddling"
|
||||
@@ -95,10 +234,6 @@ def get_mxfp_intrin_group(
|
||||
"fp4_to_bf16_twiddling": decode_f4_to_bf16_twiddling,
|
||||
}
|
||||
|
||||
func_name = f"decode_fp{source_bit}_to_{dtype_map[out_dtype]}"
|
||||
if use_twiddling:
|
||||
func_name += "_twiddling"
|
||||
|
||||
return {
|
||||
"func_name": func_name,
|
||||
"c_source": import_c_map[key],
|
||||
|
||||
@@ -129,14 +129,19 @@ def shared_16x64_to_local_64x16_layout_B(i, j):
|
||||
|
||||
|
||||
def shared_32x32_to_local_64x16_layout_C(i, j):
|
||||
thread_id = (i % 8 // 4) * 32 + j
|
||||
local_id = (i // 8) * 4 + i % 4
|
||||
# i=M_row, j=N_col. For v_mfma_i32_32x32x32_i8: tid%32=M_row, tid//32 selects N group.
|
||||
tid_high = (j // 4) % 2 # 0 for j in {0-3,8-11,...}, 1 for j in {4-7,12-15,...}
|
||||
thread_id = i + 32 * tid_high
|
||||
k = j - 4 * tid_high
|
||||
local_id = (k // 8) * 4 + k % 4
|
||||
return thread_id, local_id
|
||||
|
||||
|
||||
def thread_id_shared_access_64x16_to_32x32_layout_C_n_m(thread_id, local_id):
|
||||
i = (thread_id // 32) * 4 + local_id % 4 + (local_id // 4) * 8
|
||||
j = thread_id % 32
|
||||
# Returns (row=M, col=N) for v_mfma_i32_32x32x32_i8 output layout.
|
||||
# tid%32 = M_row, (tid//32)*4 + lid%4 + (lid//4)*8 = N_col.
|
||||
i = thread_id % 32
|
||||
j = (thread_id // 32) * 4 + local_id % 4 + (local_id // 4) * 8
|
||||
return i, j
|
||||
|
||||
|
||||
@@ -144,13 +149,13 @@ def thread_id_shared_access_64x16_to_32x32_layout_C_m_n(thread_id, local_id):
|
||||
"""Return (m, n) = (row, col) for the 32x32 MFMA output register layout.
|
||||
|
||||
For v_mfma_i32_32x32x32_i8 (gfx950), each wave-64 lane holds 16 output
|
||||
i32 values. The column (N-dimension) is indexed by ``thread_id % 32``
|
||||
and the row (M-dimension) is given by the interleaved formula below.
|
||||
i32 values. The row (M-dimension) is indexed by ``thread_id % 32``
|
||||
and the column (N-dimension) is given by the interleaved formula below.
|
||||
This function returns ``(m_idx, n_idx)`` matching the ``(row, col)``
|
||||
convention expected by ``stmatrix``.
|
||||
"""
|
||||
m = (thread_id // 32) * 4 + local_id % 4 + (local_id // 4) * 8
|
||||
n = thread_id % 32
|
||||
m = thread_id % 32
|
||||
n = (thread_id // 32) * 4 + local_id % 4 + (local_id // 4) * 8
|
||||
return m, n
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user