mirror of
https://github.com/tile-ai/tilelang.git
synced 2026-10-24 07:04:00 +08:00
* [BugFix] CuTeDSL backend support for TileKernels benchmarks TileKernels benchmark coverage with TILELANG_TARGET=cutedsl exercises backend paths that were either missing or not legal for CuTeDSL-generated Python/MLIR code. The failures were not limited to one kernel: packed FP4/FP8 quantization flows need sub-byte vector materialization, MoE mapping uses warp vote/shuffle and grid synchronization, and benchmark launch wrappers need dtype-aware host plumbing for generated CuTeDSL modules. Before this change the H100 benchmark run exposed several classes of failures: missing shuffle/vote intrinsics such as shfl_xor/match_any/popc in CuTeDSL lowering, invalid scalar/vector casts for FP4 E2M1 and packed UE8M0 paths, dynamic if expressions whose branches narrowed or changed integer types, unsupported sub-byte load/store/copy patterns, and a fragile one-counter software grid barrier that could reset before all waiting blocks observed release. Lower the missing TileLang operations to CuTeDSL runtime helpers, add Float4E2M1FN software pack/unpack and zero-fill support, make sub-byte vector loads/stores use packed Uint8 storage where required, cast conditional branches to the expected result dtype, and teach the CuTeDSL wrapper about float4/float8 dtype plumbing and C++ min/max expression rendering. Also replace the sync_grid single-counter barrier with a counter plus generation sense barrier so a waiting block cannot miss the release when the arrival counter is reset. This keeps the fixes in the CuTeDSL backend rather than adding TileKernels-specific workarounds. For example, TileKernels expand_to_fused_with_sf relies on TileLang safe-memory legalization for aligned SF loops; the CuTeDSL generated code now preserves the hidden_sf guard instead of requiring the benchmark source to special-case packed UE8M0 layouts. Validation on H100 (sm_90, 114 SMs) with CUDA visible device 0, Python 3.12, TILELANG_TARGET=cutedsl, TK_FULL_TEST=1, TILELANG_DISABLE_CACHE=1, and PYTHONPATH=/home/jayce.su/new/tilelang: - cmake --build build -j 16 - ruff check tilelang/contrib/cutedsl tilelang/jit/adapter/cutedsl tilelang/jit/adapter/utils.py - git diff --check - Original failing node: tests/moe/test_expand_to_fused.py::test_expand_to_fused_with_sf_benchmark[num_send_tokens=4001-num_topk=2-num_experts=4-num_ep_ranks=64-hidden=576-num_per_channels=32-use_tma_aligned_col_major_sf=True-round_sf=True-use_packed_ue8m0=True], 1 passed with -n 2 - Full-run local pressure slice around the failing MoE benchmark region, 80 passed with -n 2 in 379.53s Author-email: jayce.su@enflame-tech.com Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Complete CuTeDSL support for TileKernels benchmarks TileKernels benchmark coverage with TILELANG_TARGET=cutedsl exposes several general CuTeDSL backend gaps. These are not TileKernels-specific workarounds, H100-only specializations, or changes required only because of a particular CUDA/CUTLASS DSL version pairing. The H100 CUDA 13.2 + nvidia-cutlass-dsl-libs-base 4.5.0 environment is the reproducer that exercises the missing backend coverage; the underlying issues are generic to the emitted CuTeDSL code shapes. Root causes: - Predicated T.copy and safe memory legalization lower BufferLoad-or-zero values into conditional expressions. Emitting these values through tl.where evaluates both branches eagerly, which can issue unsafe packed loads and can force CuTeDSL/MLIR through unsupported vector select and unrealized_conversion_cast paths for FP8/FP4 storage. - Safe memory legalization can bind the zero branch through a local zero-like variable, let binding, cast, or broadcast-zero tensor. The CuTeDSL codegen must recognize those zero-like values when deciding whether a conditional load can be lowered lazily and when materializing Float4E2M1FN broadcasts. - CuTeDSL generated code has to distinguish rmem Tensor objects from TensorSSA values. Appending .load() blindly is invalid once a branch or cast already produced TensorSSA, and this showed up in the TileLang CI cast example as AttributeError: 'TensorSSA' object has no attribute 'load'. - Scalar FP8 widening and narrowing require CuTeDSL tensor casts on aligned vector values. Plain scalar construction falls back to unsupported scalar FP8 conversion paths, so the backend has to bridge scalar casts through a packed vector form. - Sub-byte vectors such as Float4E2M1FN cannot be copied lane by lane because CuTeDSL does not support sub-byte scalar dereference. The backend must preserve TensorSSA/packed-vector store form for these values, and scalarized local/shared sub-byte vector buffers must use the sub-byte packing factor as the make_tensor_at_offset div_by assumption. - Generated CuTeDSL kernels can call CUDA-template math helpers such as __habs and __float2half_rz, and math wrappers such as log1p/isfinite. Those helper names must be provided by tilelang.contrib.cutedsl.math when the Python CuTeDSL backend is selected. - sync_grid publishes block arrival through thread 0, but the memory ordering has to cover all threads in the block. A CTA sync alone does not provide device-scope ordering for global writes performed by non-zero threads. Fixes: - Recognize zero-like vars in conditional store lowering, including let bindings and bind values that are casted or broadcast-zero tensors. This keeps predicated loads lazy and avoids eager tl.where evaluation for packed and vectorized T.copy paths. - Add CuTeDSL helper bridges for Tensor/TensorSSA normalization. tl.as_tensor_ssa avoids calling .load() on TensorSSA values, and tl.as_rmem_tensor spills TensorSSA values into rmem tensors before byte recasts for sub-byte stores. - Add scalar FP8 bridge lowering that packs Float16/Float32/BFloat16 or FP8 scalar values into a four-lane rmem tensor, uses tl.cast_tensor, and reads lane zero. This matches CuTeDSL/NVGPU alignment requirements without forcing front-end kernels to perturb tile shapes just to avoid scalar casts. - Keep sub-byte vector stores in TensorSSA form instead of copying lane by lane, keep non-load vector store RHS materialized as rmem tensors so CuTeDSL .store() receives a TensorSSA value, and use 8 / element_bits as the div_by packing factor for scalarized local/shared sub-byte vector buffer views. - Gate the Float16 nvgpu.cvt_fpext fast path to floating-point source TensorSSA types only, so integer TensorSSA casts do not enter an FP-only conversion op. - Strengthen sync_grid by fencing all block threads at device scope before thread 0 publishes arrival, then waiting at block scope and fencing again before user code resumes. - Expose the missing CuTeDSL math helpers. __habs clears the sign bit for Float16/BFloat16 scalars and returns the original dtype. __float2half_rz uses inline PTX cvt.rz.f16.f32 so generated E5M6 cast-back code can preserve CUDA round-toward-zero semantics. log1p and isfinite are wired through the CuTeDSL math module and fastmath canonicalization. - Keep the defensive dynamic-symbolic None fallback visible by logging a warning before substituting 0. Version/specialization assessment: - The patch is backend-generic: it fixes how TileLang emits CuTeDSL for predicated loads, FP8 scalar casts, Tensor/TensorSSA boundaries, sub-byte vector stores, grid synchronization, and generated math helpers. It does not specialize TileKernels cases, tensor shapes, H100 SM counts, or benchmark parameters. - A future CuTeDSL version might accept some currently rejected IR forms, but relying on eager branch evaluation, missing helper symbols, incorrect TensorSSA .load() calls, or thread-0-only grid-sync ordering would still be fragile. The corrected lowering is semantically closer to TIR: predicated loads are lazy, sub-byte storage stays packed, TensorSSA values are not treated as tensors, and scalar FP8 conversion follows the vector cast path required by CuTeDSL/NVGPU. - CUDA 13.2 and CUTLASS DSL libs 4.5.0 are recorded as the verified reproducer, not as the reason for the patch. The same TileLang backend gaps can be triggered by any workload that emits these CuTeDSL patterns. Verified on H100 PCIe (sm_90, CUDA_VISIBLE_DEVICES=0 maps to NVIDIA H100 PCIe), CUDA 13.2, Python 3.12.3, PyTorch 2.12.0+cu132, nvidia-cutlass-dsl-libs-base 4.5.0, and local TileLang built from this branch: - cmake --build build -j$(nproc) - python3.12 -m py_compile tilelang/contrib/cutedsl/math.py tilelang/contrib/cutedsl/utils.py tilelang/contrib/cutedsl/grid_sync.py tilelang/jit/adapter/cutedsl/adapter.py - pre-commit run --files $(git diff --name-only origin/main...HEAD) - examples/cast/test_example_cast.py::test_example_group_per_split_token_cast_to_fp8: 1 passed - representative per_token_cast, cast_back, cast_back_e5m6, per_block_cast_lossless, per_block_cast, and per_channel_cast benchmark failures: 6 passed - TileKernels full benchmark with TILELANG_TARGET=cutedsl, TK_BENCHMARK_BACKEND=event, and pytest -n 2: 10165 passed, 160 warnings in 2:44:58 Jayce: JayceSu98 <jayce.su@enflame-tech.com> Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Avoid zero-sized fake tensors in CuTeDSL cubin generation CuTeDSL cubin generation builds fake compact tensors to compile wrapper code before the real runtime tensors are launched. TileKernels exercises benchmark and correctness cases with empty dynamic dimensions, such as zero-token inputs. Passing those zero extents directly into make_fake_compact_tensor can make the CuTeDSL fake layout invalid even though the shape is only a compilation placeholder and the real runtime tensor metadata is passed separately. Clamp only the fake tensor shape to positive extents before constructing the CuTeDSL placeholder. This keeps the cubin generation path valid for empty-input cases without changing runtime tensor shapes, strides, launch parameters, or kernel behavior. This is not a TileKernels-specific workaround or a CUDA toolkit version workaround. It fixes the CuTeDSL adapter boundary so compile-time fake tensors satisfy CuTeDSL layout construction requirements while preserving the actual runtime contract. Verified with: - python -m py_compile tilelang/jit/adapter/cutedsl/wrapper.py - git diff --check -- tilelang/jit/adapter/cutedsl/wrapper.py Jayce: JayceSu98 <jayce.su@enflame-tech.com> Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Stabilize batched BF16 reduce test input CUDA CI exposed a flaky failure in python/language/test_tilelang_language_reduce.py::test_reduce[sum-bfloat16-64x128-f2f-t256-b4]. The failing run had only one mismatched output element, with an absolute difference slightly above the 0.1 BF16 tolerance and a large relative error because the reference value was small. The runtime code on this branch only changes CuTeDSL backend paths, and the same test passed locally on the ordinary CUDA backend when run in isolation. The failure is caused by the test input depending on the worker-local torch RNG state: the module seeds once at import time, but xdist load scheduling lets each worker consume a different number of random tensors before this parameterized case runs. That makes the BF16 sum case occasionally sample an input whose rounded reduction error crosses the tolerance boundary. Make the parameterized reduce input schedule-independent by deriving a deterministic seed from the case parameters and using a local CPU torch.Generator for both integer and floating inputs. This preserves randomized coverage per case while decoupling correctness from xdist worker assignment, prior cases in the same worker, and full-suite ordering. Verified on H100 PCIe with CUDA visible device 1, Python 3.12.3, PyTorch 2.12.0+cu132, and the local TileLang dev build: - ruff check testing/python/language/test_tilelang_language_reduce.py - python -m pytest -q -s --tb=short testing/python/language/test_tilelang_language_reduce.py::test_reduce[sum-bfloat16-64x128-f2f-t256-b4] - python -m pytest --verbose --color=yes --durations=0 --showlocals --cache-clear --maxfail=3 --numprocesses=8 testing/python/language/test_tilelang_language_reduce.py - 10 consecutive reruns of the failed BF16 reduce case Jayce: JayceSu98 <jayce.su@enflame-tech.com> Co-authored-by: dingsg <shengge.ding@enflame-tech.com> --------- Co-authored-by: dingsg <shengge.ding@enflame-tech.com>
470 lines
18 KiB
Python
470 lines
18 KiB
Python
import tilelang
|
||
import tilelang as tl
|
||
import tilelang.language as T
|
||
import tilelang.testing
|
||
import pytest
|
||
import torch
|
||
|
||
tilelang.testing.set_random_seed()
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _case_seed(*parts):
|
||
seed = 17
|
||
for text in map(str, parts):
|
||
for char in text:
|
||
seed = (seed * 131 + ord(char)) % (2**31 - 1)
|
||
return seed
|
||
|
||
|
||
def _make_input(M, N, dtype, seed=42):
|
||
torch_dtype = getattr(torch, dtype)
|
||
generator = torch.Generator(device="cpu")
|
||
generator.manual_seed(seed)
|
||
if torch_dtype in (torch.int32, torch.int64):
|
||
return torch.randint(-100, 100, (M, N), dtype=torch_dtype, generator=generator).cuda()
|
||
return torch.randn(M, N, dtype=torch_dtype, generator=generator).cuda()
|
||
|
||
|
||
def _ref(A, op):
|
||
if op == "sum":
|
||
return A.sum(dim=1).to(A.dtype)
|
||
if op == "max":
|
||
return A.max(dim=1).values
|
||
if op == "min":
|
||
return A.min(dim=1).values
|
||
if op == "abssum":
|
||
return A.abs().sum(dim=1).to(A.dtype)
|
||
if op == "absmax":
|
||
return A.abs().max(dim=1).values
|
||
raise ValueError(op)
|
||
|
||
|
||
def _reduce_op(T, op, src, dst, dim, batch=1):
|
||
kwargs = {} if batch == 1 else {"batch": batch}
|
||
if op == "sum":
|
||
T.reduce_sum(src, dst, dim=dim, **kwargs)
|
||
elif op == "max":
|
||
T.reduce_max(src, dst, dim=dim, **kwargs)
|
||
elif op == "min":
|
||
T.reduce_min(src, dst, dim=dim, **kwargs)
|
||
elif op == "abssum":
|
||
T.reduce_abssum(src, dst, dim=dim, **kwargs)
|
||
elif op == "absmax":
|
||
T.reduce_absmax(src, dst, dim=dim, **kwargs)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# test_reduce (op × dtype × src_scope × dst_scope × threads × batch)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# int types only support sum (no abssum/absmax for int in tilelang)
|
||
REDUCE_CASES = [
|
||
# (op, dtype, M, N, src_scope, dst_scope, threads, batch)
|
||
("sum", T.float32, 128, 128, "fragment", "fragment", 32, 1),
|
||
("sum", T.int32, 128, 128, "fragment", "fragment", 32, 1),
|
||
("sum", T.int64, 192, 64, "fragment", "fragment", 32, 1),
|
||
("sum", T.float32, 192, 64, "fragment", "fragment", 32, 1),
|
||
("sum", T.float32, 32, 32, "fragment", "fragment", 16, 1),
|
||
("sum", T.float32, 16, 16, "fragment", "fragment", 8, 1),
|
||
("sum", T.float32, 32, 32, "shared", "shared", 32, 1),
|
||
("sum", T.float32, 32, 32, "fragment", "shared", 32, 1),
|
||
("max", T.float32, 128, 128, "fragment", "fragment", 32, 1),
|
||
("max", T.int64, 128, 128, "fragment", "fragment", 32, 1),
|
||
("max", T.float32, 32, 32, "shared", "shared", 32, 1),
|
||
("min", T.float32, 128, 128, "fragment", "fragment", 32, 1),
|
||
("min", T.int64, 128, 128, "fragment", "fragment", 32, 1),
|
||
("abssum", T.float32, 128, 128, "fragment", "fragment", 32, 1),
|
||
("abssum", T.int64, 128, 128, "fragment", "fragment", 32, 1),
|
||
("absmax", T.float32, 128, 128, "fragment", "fragment", 32, 1),
|
||
("absmax", T.int64, 128, 128, "fragment", "fragment", 32, 1),
|
||
# batch > 1: verify run_batch codegen and correctness together
|
||
("sum", T.float32, 128, 64, "shared", "fragment", 256, 2),
|
||
("sum", T.float32, 128, 64, "shared", "fragment", 256, 4),
|
||
("sum", T.float16, 64, 128, "fragment", "fragment", 256, 4),
|
||
("sum", T.bfloat16, 128, 128, "fragment", "fragment", 32, 1),
|
||
("sum", T.bfloat16, 64, 128, "fragment", "fragment", 256, 4),
|
||
("max", T.bfloat16, 128, 64, "shared", "fragment", 256, 2),
|
||
("max", T.float32, 128, 128, "fragment", "fragment", 256, 4),
|
||
("min", T.float32, 64, 128, "shared", "fragment", 128, 2),
|
||
("min", T.float16, 128, 128, "fragment", "fragment", 256, 8),
|
||
("abssum", T.float32, 128, 128, "fragment", "fragment", 256, 4),
|
||
("absmax", T.float32, 128, 128, "fragment", "fragment", 256, 4),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("op", "dtype", "M", "N", "src_scope", "dst_scope", "threads", "batch"),
|
||
REDUCE_CASES,
|
||
ids=[
|
||
f"{op}-{dtype}-{M}x{N}-{src_scope[0]}2{dst_scope[0]}-t{threads}-b{batch}"
|
||
for op, dtype, M, N, src_scope, dst_scope, threads, batch in REDUCE_CASES
|
||
],
|
||
)
|
||
def test_reduce(op, dtype, M, N, src_scope, dst_scope, threads, batch):
|
||
import re
|
||
|
||
@tilelang.jit(out_idx=-1)
|
||
def kernel(M, N, dtype, op, src_scope, dst_scope, threads, batch):
|
||
@T.prim_func
|
||
def main(A: T.Tensor((M, N), dtype), B: T.Tensor((M,), dtype)):
|
||
with T.Kernel(1, threads=threads):
|
||
if src_scope == "fragment":
|
||
src = T.alloc_fragment((M, N), dtype)
|
||
else:
|
||
src = T.alloc_shared((M, N), dtype)
|
||
if dst_scope == "fragment":
|
||
dst = T.alloc_fragment((M,), dtype)
|
||
else:
|
||
dst = T.alloc_shared((M,), dtype)
|
||
T.copy(A, src, disable_tma=src_scope == "shared")
|
||
_reduce_op(T, op, src, dst, dim=1, batch=batch)
|
||
T.copy(dst, B)
|
||
|
||
return main
|
||
|
||
jit_kernel = kernel(M, N, dtype, op, src_scope, dst_scope, threads, batch)
|
||
|
||
if batch > 1:
|
||
src = jit_kernel.get_kernel_source()
|
||
m = re.search(r",\s*(\d+)\s*,\s*\d+\s*>::run_batch\(", src)
|
||
assert m is not None, f"Expected run_batch in generated source.\n{src}"
|
||
|
||
seed = _case_seed("reduce", op, dtype, M, N, src_scope, dst_scope, threads, batch)
|
||
A = _make_input(M, N, dtype, seed)
|
||
B = jit_kernel(A)
|
||
# float16/bfloat16 accumulate more rounding error over large reductions
|
||
tol = 1e-1 if dtype in (T.float16, T.bfloat16) else 1e-2
|
||
torch.testing.assert_close(B, _ref(A, op), atol=tol, rtol=tol)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# test_reduce_clear (op × src_scope × dst_scope, clear=False)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
REDUCE_CLEAR_CASES = [
|
||
# (op, dtype, M, N, src_scope, dst_scope, init, ref_fn_extra)
|
||
# sum: init=1, ref = A.sum(dim=1) + 1
|
||
("sum", T.float32, 128, 128, "fragment", "fragment"),
|
||
("sum", T.float32, 128, 128, "fragment", "shared"),
|
||
("sum", T.float32, 32, 32, "shared", "shared"),
|
||
# max: init=-inf, ref = A.max(dim=1).values (max(-inf, x) = x)
|
||
("max", T.float16, 128, 128, "fragment", "fragment"),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("op", "dtype", "M", "N", "src_scope", "dst_scope"),
|
||
REDUCE_CLEAR_CASES,
|
||
ids=[f"{op}-{dtype}-{M}x{N}-{src_scope[0]}2{dst_scope[0]}" for op, dtype, M, N, src_scope, dst_scope in REDUCE_CLEAR_CASES],
|
||
)
|
||
def test_reduce_clear(op, dtype, M, N, src_scope, dst_scope):
|
||
@tilelang.jit(out_idx=-1)
|
||
def kernel(M, N, dtype, op, src_scope, dst_scope):
|
||
@T.prim_func
|
||
def main(A: T.Tensor((M, N), dtype), B: T.Tensor((M,), dtype)):
|
||
with T.Kernel(1, threads=32):
|
||
if src_scope == "fragment":
|
||
src = T.alloc_fragment((M, N), dtype)
|
||
else:
|
||
src = T.alloc_shared((M, N), dtype)
|
||
if dst_scope == "fragment":
|
||
dst = T.alloc_fragment((M,), dtype)
|
||
else:
|
||
dst = T.alloc_shared((M,), dtype)
|
||
T.copy(A, src, disable_tma=src_scope == "shared")
|
||
if op == "sum":
|
||
T.fill(dst, 1)
|
||
T.reduce_sum(src, dst, dim=1, clear=False)
|
||
elif op == "max":
|
||
T.fill(dst, -T.infinity(dtype))
|
||
T.reduce_max(src, dst, dim=1, clear=False)
|
||
T.copy(dst, B)
|
||
|
||
return main
|
||
|
||
torch_dtype = getattr(torch, dtype)
|
||
A = torch.randn(M, N, dtype=torch_dtype).cuda()
|
||
B = kernel(M, N, dtype, op, src_scope, dst_scope)(A)
|
||
if op == "sum":
|
||
ref = A.sum(dim=1) + 1
|
||
elif op == "max":
|
||
ref = A.max(dim=1).values
|
||
torch.testing.assert_close(B, ref, atol=1e-2, rtol=1e-2)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# T.finalize_reducer tests
|
||
# ---------------------------------------------------------------------------
|
||
|
||
_COMPILE_FLAGS = {
|
||
tl.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
|
||
tl.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
|
||
}
|
||
|
||
FINALIZE_REDUCER_CASES = [
|
||
# (op, dtype, block_M, block_N, batch)
|
||
("sum", T.float32, 128, 64, 1),
|
||
("sum", T.float32, 128, 64, 4),
|
||
("max", T.float16, 64, 128, 1),
|
||
("max", T.float16, 64, 128, 8),
|
||
("min", T.float32, 128, 128, 1),
|
||
("min", T.float32, 128, 128, 16),
|
||
]
|
||
|
||
|
||
def _make_finalize_reducer_kernel(block_M, block_N, dtype, op, batch):
|
||
@T.prim_func
|
||
def kernel(A: T.Tensor((block_M, block_N), dtype), B: T.Tensor((block_M,), dtype)):
|
||
with T.Kernel(1, threads=256):
|
||
o_reducer = T.alloc_reducer(block_M, dtype, op=op, replication="all")
|
||
T.clear(o_reducer)
|
||
A_smem = T.alloc_shared((block_M, block_N), dtype)
|
||
T.copy(A, A_smem)
|
||
A_frag = T.alloc_fragment((block_M, block_N), dtype)
|
||
T.copy(A_smem, A_frag)
|
||
for i, j in T.Parallel(block_M, block_N):
|
||
if op == "sum":
|
||
o_reducer[i] += A_frag[i, j]
|
||
elif op == "max":
|
||
o_reducer[i] = T.max(o_reducer[i], A_frag[i, j])
|
||
else:
|
||
o_reducer[i] = T.min(o_reducer[i], A_frag[i, j])
|
||
T.finalize_reducer(o_reducer, batch=batch)
|
||
T.copy(o_reducer, B)
|
||
|
||
return kernel
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("op", "dtype", "block_M", "block_N", "batch"),
|
||
FINALIZE_REDUCER_CASES,
|
||
ids=[f"{op}-{dtype}-{bM}x{bN}-b{batch}" for op, dtype, bM, bN, batch in FINALIZE_REDUCER_CASES],
|
||
)
|
||
def test_finalize_reducer_codegen(op, dtype, block_M, block_N, batch):
|
||
"""batch=1 → scalar run; batch>1 → run_batch with correct template arg."""
|
||
import re
|
||
|
||
src = tl.compile(
|
||
_make_finalize_reducer_kernel(block_M, block_N, dtype, op, batch),
|
||
out_idx=-1,
|
||
pass_configs=_COMPILE_FLAGS,
|
||
).get_kernel_source()
|
||
|
||
if batch == 1:
|
||
assert "run_batch" not in src, f"batch=1 must not emit run_batch.\n{src}"
|
||
else:
|
||
m = re.search(r",\s*(\d+)\s*,\s*\d+\s*>::run_batch\(", src)
|
||
assert m is not None, f"Expected run_batch in generated source.\n{src}"
|
||
assert int(m.group(1)) == batch, f"Expected batch={batch}, got {m.group(1)}.\n{src}"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("op", "dtype", "block_M", "block_N", "batch"),
|
||
[c for c in FINALIZE_REDUCER_CASES if c[4] == 1],
|
||
ids=[f"{op}-{dtype}-{bM}x{bN}" for op, dtype, bM, bN, batch in FINALIZE_REDUCER_CASES if batch == 1],
|
||
)
|
||
def test_finalize_reducer_correctness(op, dtype, block_M, block_N, batch):
|
||
"""Numerical correctness (batch=1 scalar path; batch>1 blocked by fragment layout bug)."""
|
||
A = torch.randn(block_M, block_N, dtype=getattr(torch, dtype)).cuda()
|
||
B = tl.compile(
|
||
_make_finalize_reducer_kernel(block_M, block_N, dtype, op, batch),
|
||
out_idx=-1,
|
||
pass_configs=_COMPILE_FLAGS,
|
||
)(A)
|
||
torch.testing.assert_close(B, _ref(A, op), atol=1e-2, rtol=1e-2)
|
||
|
||
|
||
# (batch, exc_type, match)
|
||
FINALIZE_REDUCER_INVALID_CASES = [
|
||
(0, ValueError, "batch must be >= 1"),
|
||
(-1, ValueError, "batch must be >= 1"),
|
||
(128, Exception, "exceeds total output elements"), # block_M=64, batch=128
|
||
(3, Exception, "must evenly divide"), # block_M=64, batch=3
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("batch", "exc_type", "match"),
|
||
FINALIZE_REDUCER_INVALID_CASES,
|
||
ids=["zero", "negative", "exceeds", "not-divisible"],
|
||
)
|
||
def test_finalize_reducer_invalid_batch(batch, exc_type, match):
|
||
block_M = 64
|
||
|
||
def make_kernel():
|
||
@T.prim_func
|
||
def kernel(A: T.Tensor((block_M, 64), T.float32), B: T.Tensor((block_M,), T.float32)):
|
||
with T.Kernel(1, threads=256):
|
||
o_reducer = T.alloc_reducer(block_M, T.float32, op="sum", replication="all")
|
||
T.clear(o_reducer)
|
||
A_smem = T.alloc_shared((block_M, 64), T.float32)
|
||
T.copy(A, A_smem)
|
||
A_frag = T.alloc_fragment((block_M, 64), T.float32)
|
||
T.copy(A_smem, A_frag)
|
||
for i, j in T.Parallel(block_M, 64):
|
||
o_reducer[i] += A_frag[i, j]
|
||
T.finalize_reducer(o_reducer, batch=batch)
|
||
T.copy(o_reducer, B)
|
||
|
||
return kernel
|
||
|
||
with pytest.raises(exc_type, match=match):
|
||
# batch<1 raises at prim_func definition time; others at compile time
|
||
k = make_kernel()
|
||
tl.compile(k, out_idx=-1, pass_configs=_COMPILE_FLAGS)
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_absmax_bf16_noncontiguous_packed_layout_regression():
|
||
num_tokens = 64
|
||
hidden = 2560
|
||
num_threads = 128
|
||
num_vectorize = 4
|
||
|
||
def x_layout_fn(i, j):
|
||
idx = i * hidden + j
|
||
return (
|
||
idx // num_vectorize % num_threads,
|
||
idx // (num_vectorize * num_threads) * num_vectorize + idx % num_vectorize,
|
||
)
|
||
|
||
@T.prim_func
|
||
def kernel(A: T.Tensor((num_tokens, hidden), T.bfloat16), B: T.Tensor((num_tokens,), T.float32)):
|
||
with T.Kernel(num_tokens, threads=num_threads) as (pid,):
|
||
src = T.alloc_fragment((1, hidden), T.bfloat16)
|
||
dst = T.alloc_fragment((1, 1), T.bfloat16)
|
||
T.annotate_layout({src: T.Fragment((1, hidden), forward_fn=x_layout_fn)})
|
||
T.copy(A[pid, 0], src, disable_tma=True)
|
||
src_reshaped = T.reshape(src, (1, 1, hidden))
|
||
T.reduce_absmax(src_reshaped, dst, dim=2)
|
||
B[pid] = T.cast(dst[0, 0], T.float32)
|
||
|
||
A = torch.zeros((num_tokens, hidden), dtype=torch.bfloat16, device="cuda")
|
||
A[:, 512] = -10
|
||
B = _compile(kernel)(A)
|
||
ref = A.abs().amax(dim=1).float()
|
||
torch.testing.assert_close(B, ref, atol=0, rtol=0)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# nan_propagate tests – packed (vsize=2) path for bf16/fp16
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _compile(prim_func):
|
||
return tilelang.compile(prim_func, out_idx=-1, target="cuda")
|
||
|
||
|
||
def _make_nan_reduce_kernel(reduce_fn, M, N, dtype, threads, *, nan_propagate):
|
||
@T.prim_func
|
||
def kernel(A: T.Tensor((M, N), dtype), B: T.Tensor((M,), dtype)):
|
||
with T.Kernel(1, threads=threads):
|
||
src = T.alloc_fragment((M, N), dtype)
|
||
dst = T.alloc_fragment((M,), dtype)
|
||
T.copy(A, src)
|
||
reduce_fn(src, dst, dim=1, nan_propagate=nan_propagate)
|
||
T.copy(dst, B)
|
||
|
||
return kernel
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
@tilelang.testing.requires_cuda_compute_version_ge(8, 9)
|
||
def test_reduce_packed_fp8_to_float16_absmax_runtime():
|
||
if not hasattr(torch, "float8_e4m3fn"):
|
||
pytest.skip("torch.float8_e4m3fn is not available")
|
||
|
||
@T.prim_func
|
||
def kernel(A: T.Tensor((4, 32), T.float8_e4m3fn), B: T.Tensor((4,), T.float16)):
|
||
with T.Kernel(1, threads=32):
|
||
src = T.alloc_fragment((4, 32), T.float8_e4m3fn)
|
||
dst = T.alloc_fragment((4,), T.float16)
|
||
T.copy(A, src)
|
||
T.reduce_absmax(src, dst, dim=1)
|
||
T.copy(dst, B)
|
||
|
||
k = _compile(kernel)
|
||
source = k.get_kernel_source()
|
||
assert "from_uint1<__half2>(*(fp8" not in source
|
||
|
||
base = torch.linspace(-2.0, 2.0, 128, device="cuda", dtype=torch.float16).reshape(4, 32)
|
||
base[0, 3] = -7.0
|
||
base[1, 17] = 5.5
|
||
base[2, 31] = -3.25
|
||
base[3, 0] = 4.0
|
||
A = base.to(torch.float8_e4m3fn)
|
||
B = k(A)
|
||
ref = A.to(torch.float16).abs().amax(dim=1)
|
||
torch.testing.assert_close(B, ref, atol=0, rtol=0)
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_packed_max_nan_propagate_uses_nan_intrinsics():
|
||
k = _compile(_make_nan_reduce_kernel(T.reduce_max, 128, 128, T.float16, threads=256, nan_propagate=True))
|
||
src = k.get_kernel_source()
|
||
assert "max2_nan" in src
|
||
assert "tl::MaxOpNan" in src
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_packed_min_nan_propagate_uses_nan_intrinsics():
|
||
k = _compile(_make_nan_reduce_kernel(T.reduce_min, 128, 128, T.bfloat16, threads=256, nan_propagate=True))
|
||
src = k.get_kernel_source()
|
||
assert "min2_nan" in src
|
||
assert "tl::MinOpNan" in src
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_packed_absmax_nan_propagate_uses_nan_intrinsics():
|
||
k = _compile(_make_nan_reduce_kernel(T.reduce_absmax, 128, 128, T.float16, threads=256, nan_propagate=True))
|
||
src = k.get_kernel_source()
|
||
assert "max2_nan" in src
|
||
assert "tl::MaxOpNan" in src
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_packed_max_nan_propagate_runtime():
|
||
import math
|
||
|
||
for tl_dtype, torch_dtype in [(T.float16, torch.float16), (T.bfloat16, torch.bfloat16)]:
|
||
M, N = 128, 128
|
||
A = torch.arange(N, dtype=torch.float32).to(torch_dtype).repeat(M, 1).cuda()
|
||
A[0, 7] = float("nan")
|
||
B = _compile(_make_nan_reduce_kernel(T.reduce_max, M, N, tl_dtype, threads=256, nan_propagate=True))(A)
|
||
assert not math.isnan(B[1:].float().max().item()), f"{tl_dtype}: non-NaN rows should not produce NaN"
|
||
assert math.isnan(B[0].float().item()), f"{tl_dtype}: NaN row must produce NaN"
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_packed_min_nan_propagate_runtime():
|
||
import math
|
||
|
||
for tl_dtype, torch_dtype in [(T.float16, torch.float16), (T.bfloat16, torch.bfloat16)]:
|
||
M, N = 128, 128
|
||
A = torch.arange(N, dtype=torch.float32).to(torch_dtype).repeat(M, 1).cuda()
|
||
A[1, 13] = float("nan")
|
||
B = _compile(_make_nan_reduce_kernel(T.reduce_min, M, N, tl_dtype, threads=256, nan_propagate=True))(A)
|
||
assert not math.isnan(B[0].float().item()), f"{tl_dtype}: non-NaN rows should not produce NaN"
|
||
assert math.isnan(B[1].float().item()), f"{tl_dtype}: NaN row must produce NaN"
|
||
|
||
|
||
@tilelang.testing.requires_cuda
|
||
def test_reduce_packed_max_nan_batch_runtime():
|
||
import math
|
||
|
||
for tl_dtype, torch_dtype in [(T.float16, torch.float16), (T.bfloat16, torch.bfloat16)]:
|
||
M, N = 64, 128
|
||
A = torch.arange(N, dtype=torch.float32).to(torch_dtype).repeat(M, 1).cuda()
|
||
A[2, 7] = float("nan")
|
||
B = _compile(_make_nan_reduce_kernel(T.reduce_max, M, N, tl_dtype, threads=256, nan_propagate=True))(A)
|
||
assert not math.isnan(B[0].float().item()), f"{tl_dtype}: non-NaN rows should not produce NaN"
|
||
assert math.isnan(B[2].float().item()), f"{tl_dtype}: NaN row must produce NaN"
|
||
|
||
|
||
if __name__ == "__main__":
|
||
tilelang.testing.main()
|