mirror of
https://github.com/tile-ai/tilelang.git
synced 2026-10-21 06:40:23 +08:00
math.h aliases several half-style names to cutlass::fast_* when a kernel uses selected fast-math intrinsics. CUTLASS provides only float and double overloads for fast_log, fast_sqrt, fast_sin, and fast_cos, and adds half_t overloads only for fast_exp and fast_tanh. Overload resolution can therefore leave a float result that cannot copy-initialize the CUTLASS wrapper. Once a kernel includes math.h, for example through T.__exp, these aliases also affect matching plain math calls in the same kernel. Their compilation could therefore depend on whether another intrinsic pulled math.h in. Add half_t and bfloat16_t overloads for fast_log, fast_sqrt, fast_sin, and fast_cos, plus bfloat16_t for fast_tanh. Each evaluates in float32 and converts back, following the existing fast_exp(bfloat16_t) bridge. Calls that already compiled retain the same numerical path. Add compile-and-run coverage for both wrapper types.
177 lines
4.8 KiB
Python
177 lines
4.8 KiB
Python
import re
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
import tilelang
|
|
import tilelang.language as T
|
|
import tilelang.testing
|
|
from tvm import tirx
|
|
|
|
|
|
FASTMATH_INTRINSICS = [
|
|
T.__exp,
|
|
T.__exp10,
|
|
T.__log,
|
|
T.__log2,
|
|
T.__log10,
|
|
T.__tan,
|
|
T.__cos,
|
|
T.__sin,
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"dtype",
|
|
[
|
|
"bool",
|
|
"int8",
|
|
"int16",
|
|
"int32",
|
|
"int64",
|
|
"uint8",
|
|
"uint16",
|
|
"uint32",
|
|
"uint64",
|
|
"float8_e4m3fn",
|
|
"float8_e5m2",
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("intrinsic", FASTMATH_INTRINSICS)
|
|
def test_fastmath_rejects_unsupported_dtype(intrinsic, dtype):
|
|
value = tirx.Var("value", dtype)
|
|
|
|
with pytest.raises(
|
|
TypeError,
|
|
match=rf"T\.{intrinsic.__name__} only supports floating-point inputs, "
|
|
rf"but got {re.escape(dtype)}",
|
|
):
|
|
intrinsic(value)
|
|
|
|
|
|
@pytest.mark.parametrize("dtype", ["float16", "bfloat16", "float32", "float64"])
|
|
@pytest.mark.parametrize("intrinsic", FASTMATH_INTRINSICS)
|
|
def test_fastmath_accepts_float_dtype_at_frontend(intrinsic, dtype):
|
|
value = tirx.Var("value", dtype)
|
|
result = intrinsic(value)
|
|
|
|
assert isinstance(result, tirx.Call)
|
|
assert result.dtype == dtype
|
|
|
|
|
|
# Rows written by the kernels below, in order. The "pos" domain keeps the log
|
|
# family defined and exp10 finite in float16; "sym" is mixed-sign and stays
|
|
# clear of tan's pole.
|
|
FASTMATH_ROWS = (
|
|
("__exp", torch.exp, "pos"),
|
|
("__exp10", lambda x: torch.pow(10.0, x), "pos"),
|
|
("__log", torch.log, "pos"),
|
|
("__log2", torch.log2, "pos"),
|
|
("__log10", torch.log10, "pos"),
|
|
("__sin", torch.sin, "sym"),
|
|
("__cos", torch.cos, "sym"),
|
|
("__tan", torch.tan, "sym"),
|
|
)
|
|
|
|
MIXED_ROWS = (
|
|
("log", torch.log, "pos"),
|
|
("sqrt", torch.sqrt, "pos"),
|
|
("sin", torch.sin, "sym"),
|
|
("cos", torch.cos, "sym"),
|
|
("tanh", torch.tanh, "sym"),
|
|
("__exp", torch.exp, "pos"),
|
|
)
|
|
|
|
TOLERANCE = 2e-2
|
|
|
|
|
|
def _fastmath_kernel(dtype, N):
|
|
@T.prim_func
|
|
def main(
|
|
P: T.Tensor((N,), dtype),
|
|
S: T.Tensor((N,), dtype),
|
|
B: T.Tensor((len(FASTMATH_ROWS), N), dtype),
|
|
):
|
|
with T.Kernel(1, threads=N):
|
|
i = T.get_thread_binding()
|
|
# Materialize each result: copy-initialization rejects a
|
|
# float-returning call, while a store only needs an assignment.
|
|
exp_value = T.__exp(P[i])
|
|
exp10_value = T.__exp10(P[i])
|
|
log_value = T.__log(P[i])
|
|
log2_value = T.__log2(P[i])
|
|
log10_value = T.__log10(P[i])
|
|
sin_value = T.__sin(S[i])
|
|
cos_value = T.__cos(S[i])
|
|
tan_value = T.__tan(S[i])
|
|
B[0, i] = exp_value
|
|
B[1, i] = exp10_value
|
|
B[2, i] = log_value
|
|
B[3, i] = log2_value
|
|
B[4, i] = log10_value
|
|
B[5, i] = sin_value
|
|
B[6, i] = cos_value
|
|
B[7, i] = tan_value
|
|
|
|
return main
|
|
|
|
|
|
def _mixed_kernel(dtype, N):
|
|
@T.prim_func
|
|
def main(
|
|
P: T.Tensor((N,), dtype),
|
|
S: T.Tensor((N,), dtype),
|
|
B: T.Tensor((len(MIXED_ROWS), N), dtype),
|
|
):
|
|
with T.Kernel(1, threads=N):
|
|
i = T.get_thread_binding()
|
|
log_value = T.log(P[i])
|
|
sqrt_value = T.sqrt(P[i])
|
|
sin_value = T.sin(S[i])
|
|
cos_value = T.cos(S[i])
|
|
tanh_value = T.tanh(S[i])
|
|
# This single fast-math op pulls math.h into the whole kernel,
|
|
# rebinding every half-style name above to a cutlass::fast_* macro.
|
|
exp_value = T.__exp(P[i])
|
|
B[0, i] = log_value
|
|
B[1, i] = sqrt_value
|
|
B[2, i] = sin_value
|
|
B[3, i] = cos_value
|
|
B[4, i] = tanh_value
|
|
B[5, i] = exp_value
|
|
|
|
return main
|
|
|
|
|
|
def _run(kernel_fn, rows, dtype):
|
|
N = 128
|
|
kernel = tilelang.compile(kernel_fn(dtype, N), target="cuda")
|
|
torch_dtype = getattr(torch, dtype)
|
|
p = (torch.arange(N, device="cuda") * 0.03125 + 0.3).to(torch_dtype)
|
|
s = ((torch.arange(N, device="cuda") - 63.5) * 0.02).to(torch_dtype)
|
|
b = torch.empty(len(rows), N, device="cuda", dtype=torch_dtype)
|
|
kernel(p, s, b)
|
|
|
|
inputs = {"pos": p, "sym": s}
|
|
for row, (name, ref_fn, domain) in enumerate(rows):
|
|
ref = ref_fn(inputs[domain].float())
|
|
torch.testing.assert_close(
|
|
b[row].float(),
|
|
ref.to(torch_dtype).float(),
|
|
atol=TOLERANCE,
|
|
rtol=TOLERANCE,
|
|
msg=lambda base, name=name: f"T.{name} mismatch\n{base}",
|
|
)
|
|
|
|
|
|
@tilelang.testing.requires_cuda
|
|
@pytest.mark.parametrize("dtype", ["float16", "bfloat16"])
|
|
def test_fastmath_16bit_intrinsics(dtype):
|
|
_run(_fastmath_kernel, FASTMATH_ROWS, dtype)
|
|
|
|
|
|
@tilelang.testing.requires_cuda
|
|
@pytest.mark.parametrize("dtype", ["float16", "bfloat16"])
|
|
def test_plain_math_alongside_fastmath(dtype):
|
|
_run(_mixed_kernel, MIXED_ROWS, dtype)
|