Files
tilelang/testing/python/language/test_tilelang_language_copy.py
T
Chennes 92072ab271 [BugFix] Cast to the destination dtype in the scalar T.copy path (#2771)
* [BugFix] Cast to the destination dtype in the scalar T.copy path

The scalar fast path in T.copy lowers a single-element copy directly to a
BufferStore. Unlike the general region path, which casts the loaded value to
the destination dtype in src/op/copy.cc, it stored the source value unchanged.
A cross-dtype scalar copy therefore failed the strict BufferStore dtype check.

This affects the common gather pattern of copying an int64 index tensor
(torch's default for argsort and topk) into an int32 on-device scratch buffer.

Mirror the general path by casting the source value to the destination
BufferLoad dtype when they differ, preserving any index lanes.

Add regression coverage for int64-to-int32 narrowing and int16-to-int32
widening.

Fixes #2689

* [BugFix] Convert the test index dtype with as_torch()

The TileLang dtype subclasses str, so getattr(torch, index_dtype) works, but
as_torch() states the conversion directly instead of relying on that detail.
2026-07-27 17:41:45 +08:00

443 lines
15 KiB
Python

import tilelang
import tilelang.language as T
import torch
import tilelang.testing
from tilelang import tvm
from tilelang.layout import make_gemm_fragment_8x8_transposed
print(torch.__version__)
# add decorator @tilelang.jit if you want to return a torch function
# @tilelang.jit
def tilelang_copy(M, N, block_M, block_N, src_dtype=T.float16, dst_dtype=T.float16):
@T.prim_func
def main(
A: T.Tensor((M, N), src_dtype),
B: T.Tensor((M, N), dst_dtype),
):
# Initialize Kernel Context
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
T.copy(
A[by * block_M : (by + 1) * block_M, bx * block_N : (bx + 1) * block_N],
B[by * block_M : (by + 1) * block_M, bx * block_N : (bx + 1) * block_N],
)
return main
def run_tilelang_copy(M=1024, N=1024, block_M=128, block_N=128, dtype=T.float16):
program = tilelang_copy(M, N, block_M, block_N, src_dtype=dtype, dst_dtype=dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
source = kernel.get_kernel_source()
print(source)
a = torch.randn(M, N, device="cuda", dtype=getattr(torch, dtype))
b = kernel(a)
torch.testing.assert_close(b, a, rtol=1e-2, atol=1e-2)
def test_tilelang_copy():
run_tilelang_copy(M=1024, N=1024, block_M=128, block_N=128)
run_tilelang_copy(M=1024, N=576, block_M=32, block_N=576)
run_tilelang_copy(M=1024, N=576, block_M=32, block_N=576, dtype=T.float32)
def run_tilelang_copy_cross_dtype(M=256, N=256, block_M=128, block_N=128, src_dtype=T.float16, dst_dtype=T.bfloat16):
program = tilelang_copy(M, N, block_M, block_N, src_dtype=src_dtype, dst_dtype=dst_dtype)
kernel = tilelang.compile(program, out_idx=[1])
a = torch.randn(M, N, device="cuda", dtype=getattr(torch, src_dtype))
b = kernel(a)
torch.testing.assert_close(b, a.to(getattr(torch, dst_dtype)), rtol=1e-2, atol=1e-2)
@tilelang.testing.requires_cuda
def test_tilelang_copy_cross_dtype():
run_tilelang_copy_cross_dtype(src_dtype=T.float16, dst_dtype=T.bfloat16)
run_tilelang_copy_cross_dtype(src_dtype=T.bfloat16, dst_dtype=T.float16)
def tilelang_copy_oob_safe_value(
M,
N,
padded_M,
padded_N,
block_M,
block_N,
src_dtype=T.float32,
dst_dtype=T.float32,
pad_value=None,
):
@T.prim_func
def main(
A: T.Tensor((M, N), src_dtype),
B: T.Tensor((padded_M, padded_N), dst_dtype),
):
with T.Kernel(T.ceildiv(padded_N, block_N), T.ceildiv(padded_M, block_M), threads=128) as (bx, by):
if pad_value is not None:
T.annotate_safe_value({A: pad_value})
T.copy(
A[by * block_M : (by + 1) * block_M, bx * block_N : (bx + 1) * block_N],
B[by * block_M : (by + 1) * block_M, bx * block_N : (bx + 1) * block_N],
)
return main
def run_tilelang_copy_oob_safe_value(
M=70,
N=75,
block_M=32,
block_N=32,
src_dtype=T.float32,
dst_dtype=T.float32,
pad_value=None,
):
padded_M = tilelang.cdiv(M, block_M) * block_M
padded_N = tilelang.cdiv(N, block_N) * block_N
program = tilelang_copy_oob_safe_value(
M,
N,
padded_M,
padded_N,
block_M,
block_N,
src_dtype,
dst_dtype,
pad_value,
)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
a = torch.randn(M, N, device="cuda", dtype=getattr(torch, src_dtype))
b = kernel(a)
torch_dst_dtype = getattr(torch, dst_dtype)
fallback_value = 0 if pad_value is None else pad_value
ref_b = torch.full((padded_M, padded_N), fallback_value, device="cuda", dtype=torch_dst_dtype)
ref_b[:M, :N] = a.to(torch_dst_dtype)
torch.testing.assert_close(b[:M, :N], ref_b[:M, :N], rtol=1e-2, atol=1e-2)
torch.testing.assert_close(b[M:, :N], ref_b[M:, :N], rtol=1e-2, atol=1e-2)
torch.testing.assert_close(b[:M, N:], ref_b[:M, N:], rtol=1e-2, atol=1e-2)
torch.testing.assert_close(b[M:, N:], ref_b[M:, N:], rtol=1e-2, atol=1e-2)
@tilelang.testing.requires_cuda
def test_tilelang_copy_oob_safe_value_uses_annotation():
run_tilelang_copy_oob_safe_value(pad_value=-99)
@tilelang.testing.requires_cuda
def test_tilelang_copy_oob_safe_value_defaults_to_zero():
run_tilelang_copy_oob_safe_value(pad_value=None)
@tilelang.testing.requires_cuda
def test_tilelang_copy_oob_safe_value_casts_to_destination_dtype():
run_tilelang_copy_oob_safe_value(src_dtype=T.float32, dst_dtype=T.float16, pad_value=-99)
def tilelang_elementwise_copy_oob_safe_value(M, N, padded_M, padded_N, dtype=T.float32, pad_value=-99):
@T.prim_func
def main(
A: T.Tensor((M, N), dtype),
B: T.Tensor((padded_M, padded_N), dtype),
):
with T.Kernel(1, threads=128):
T.annotate_safe_value({A: pad_value})
for i, j in T.Parallel(padded_M, padded_N):
T.copy(A[i, j], B[i, j])
return main
@tilelang.testing.requires_cuda
def test_tilelang_elementwise_copy_oob_safe_value_non_regression():
M, N = 70, 75
padded_M, padded_N = 96, 96
program = tilelang_elementwise_copy_oob_safe_value(M, N, padded_M, padded_N)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
a = torch.randn(M, N, device="cuda", dtype=torch.float32)
b = kernel(a)
ref_b = torch.full((padded_M, padded_N), -99, device="cuda", dtype=torch.float32)
ref_b[:M, :N] = a
torch.testing.assert_close(b, ref_b, rtol=1e-2, atol=1e-2)
def tilelang_copy_oob_safe_value_lexical_scope():
M = N = 60
BM = BN = 64
@T.prim_func
def main(A: T.Tensor((M, N), "int32"), Out: T.Tensor((BM, BN), "int32")):
with T.Kernel(1, threads=128):
inner = T.alloc_shared((BM, BN), "int32")
outer = T.alloc_shared((BM, BN), "int32")
with T.sblock("outer"):
T.reads()
T.writes()
T.annotate_safe_value({A: -1})
with T.sblock("inner"):
T.reads()
T.writes()
T.annotate_safe_value({A: -99})
T.copy(A[0:BM, 0:BN], inner)
T.copy(A[0:BM, 0:BN], outer)
for i, j in T.Parallel(BM, BN):
Out[i, j] = outer[i, j]
return main
@tilelang.testing.requires_cuda
def test_tilelang_copy_oob_safe_value_lexical_scope():
M = N = 60
BM = BN = 64
kernel = tilelang.compile(
tilelang_copy_oob_safe_value_lexical_scope(),
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
a = torch.arange(M * N, dtype=torch.int32, device="cuda").reshape(M, N)
out = kernel(a)
assert out.shape == (BM, BN)
assert "-1" in kernel.get_kernel_source()
torch.testing.assert_close(out[:M, :N].cpu(), a.cpu(), rtol=0, atol=0)
assert out[60, :8].cpu().tolist() == [-1] * 8
assert out[:8, 60].cpu().tolist() == [-1] * 8
assert out[60, 60].item() == -1
def tilelang_copy_with_stride(M, N, NN, block_M, block_N, dtype=T.float16):
@T.prim_func
def main(
A: T.StridedTensor((M, N), (NN, 1), dtype),
B: T.Tensor((M, N), dtype),
):
# Initialize Kernel Context
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
for i, j in T.Parallel(block_M, block_N):
B[by * block_M + i, bx * block_N + j] = A[by * block_M + i, bx * block_N + j]
return main
def run_tilelang_copy_with_stride(M=1024, N=1024, NN=2048, block_M=128, block_N=128, dtype=T.float16):
if isinstance(NN, int):
assert NN > N, "NN must be greater than N"
program = tilelang_copy_with_stride(M, N, NN, block_M, block_N, dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
if isinstance(NN, T.Var):
NN = N * 2
a = torch.randn(M, NN, device="cuda", dtype=getattr(torch, dtype))
b = kernel(a[:, :N])
torch.testing.assert_close(b, a[:, :N], rtol=1e-2, atol=1e-2)
def test_tilelang_copy_with_stride():
run_tilelang_copy_with_stride(M=1024, N=1024, NN=2048, block_M=128, block_N=128)
run_tilelang_copy_with_stride(M=1024, N=1024, NN=T.dynamic("NN"), block_M=128, block_N=128)
def tilelang_copy_bufferload(num_tokens, dtype=T.float16):
@T.prim_func
def main(
indices: T.Tensor((num_tokens,), T.int32),
x: T.Tensor((num_tokens,), dtype),
):
with T.Kernel(num_tokens, threads=32) as pid:
idx = T.alloc_local([1], T.int32)
T.copy(indices[pid], idx[0])
x[idx[0]] = x[idx[0]] + 1
return main
def run_tilelang_copy_bufferload(num_tokens=128, dtype=T.float16):
program = tilelang_copy_bufferload(num_tokens, dtype)
# test compilation only
tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
def test_tilelang_copy_bufferload():
run_tilelang_copy_bufferload(num_tokens=128)
def tilelang_copy_bufferload_cross_dtype(num_tokens, index_dtype=T.int64):
@T.prim_func
def main(
indices: T.Tensor((num_tokens,), index_dtype),
out: T.Tensor((num_tokens,), T.int32),
):
with T.Kernel(num_tokens, threads=1) as pid:
idx = T.alloc_local([1], T.int32)
T.copy(indices[pid], idx[0])
out[pid] = idx[0]
return main
def run_tilelang_copy_bufferload_cross_dtype(num_tokens=128, index_dtype=T.int64):
program = tilelang_copy_bufferload_cross_dtype(num_tokens, index_dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
# Keep the range within the (possibly narrow) index dtype.
hi = 30000 if index_dtype == T.int16 else 1_000_000
indices = torch.randint(0, hi, (num_tokens,), dtype=index_dtype.as_torch(), device="cuda")
out = kernel(indices)
torch.testing.assert_close(out, indices.to(torch.int32))
@tilelang.testing.requires_cuda
def test_tilelang_copy_bufferload_cross_dtype():
# int64 index into an int32 scratch -- the gather from #2689.
run_tilelang_copy_bufferload_cross_dtype(num_tokens=128, index_dtype=T.int64)
run_tilelang_copy_bufferload_cross_dtype(num_tokens=128, index_dtype=T.int16)
def tilelang_copy_buffer_load_with_parallel(M, N, block_M, block_N, dtype=T.float16):
@T.prim_func
def main(
A: T.Tensor((M, N), dtype),
B: T.Tensor((M, N), dtype),
):
# Initialize Kernel Context
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
for i, j in T.Parallel(block_M, block_N):
T.copy(A[by * block_M + i, bx * block_N + j], B[by * block_M + i, bx * block_N + j])
return main
def run_tilelang_copy_buffer_load_with_parallel(M=1024, N=1024, block_M=128, block_N=128, dtype=T.float16):
program = tilelang_copy_buffer_load_with_parallel(M, N, block_M, block_N, dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
a = torch.randn(M, N, device="cuda", dtype=getattr(torch, dtype))
b = kernel(a)
torch.testing.assert_close(b, a, rtol=1e-2, atol=1e-2)
def test_tilelang_copy_buffer_load_with_parallel():
run_tilelang_copy_buffer_load_with_parallel(M=1024, N=1024, block_M=128, block_N=128)
def tilelang_copy_shape_mismatched(M, N, src_dtype=T.float16, dst_dtype=T.float16):
@T.prim_func
def main(
A: T.Tensor((M, N), src_dtype),
B: T.Tensor((M, N), dst_dtype),
):
# Initialize Kernel Context
with T.Kernel(1, threads=128):
T.copy(A[:, :2], B[:, :3])
return main
def run_tilelang_copy_shape_mismatched(M=1024, N=1024, dtype=T.float16):
program = tilelang_copy_shape_mismatched(M, N, src_dtype=dtype, dst_dtype=dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
pass_configs={tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True},
)
a = torch.randn(M, N, device="cuda", dtype=getattr(torch, dtype))
b = kernel(a)
torch.testing.assert_close(b[:, :1], a[:, :1], rtol=1e-2, atol=1e-2)
def test_tilelang_copy_shape_mismatched():
run_tilelang_copy_shape_mismatched(M=128, N=128)
def run_tilelang_copy_fp8_e8m0(M=1024, N=1024, block_M=128, block_N=128, src_dtype=T.float8_e8m0fnu, dst_dtype=T.float8_e8m0fnu):
program = tilelang_copy(M, N, block_M, block_N, src_dtype=src_dtype, dst_dtype=dst_dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
)
source = kernel.get_kernel_source()
assert "fp8_e8_t" in source
dummy_input = torch.randint(0, 100, (M, N), device="cuda", dtype=torch.int8).view(torch.float8_e8m0fnu)
output = kernel(dummy_input)
assert output is not None
@tilelang.testing.requires_cuda
@tilelang.testing.requires_cuda_compute_version_ge(10, 0)
def test_tilelang_copy_fp8_e8m0():
run_tilelang_copy_fp8_e8m0(src_dtype=T.float8_e8m0fnu, dst_dtype=T.float8_e8m0fnu)
def run_tilelang_copy_fp4(M=1024, N=1024, block_M=128, block_N=128, src_dtype=T.float4_e2m1fn, dst_dtype=T.float4_e2m1fn):
program = tilelang_copy(M, N, block_M, block_N, src_dtype=src_dtype, dst_dtype=dst_dtype)
kernel = tilelang.compile(
program,
out_idx=[1],
)
source = kernel.get_kernel_source()
assert "fp4_e2_t" in source
# For FP4, use same shape as kernel expects, since int8 is used as storage type
dummy_input = torch.randint(0, 100, (M, N // 2), device="cuda", dtype=torch.int8)
output = kernel(dummy_input)
if src_dtype == dst_dtype:
assert torch.allclose(output.view(torch.int8), dummy_input)
assert output is not None
@tilelang.testing.requires_cuda
@tilelang.testing.requires_cuda_compute_version_ge(10, 0)
def test_tilelang_copy_fp4():
run_tilelang_copy_fp4(src_dtype=T.float4_e2m1fn, dst_dtype=T.float4_e2m1fn)
run_tilelang_copy_fp4(src_dtype=T.float4_e2m1fn, dst_dtype=T.float16)
run_tilelang_copy_fp4(src_dtype=T.float4_e2m1fn, dst_dtype=T.bfloat16)
@tilelang.testing.requires_cuda
def test_tilelang_copy_uses_stmatrix_m16n8_for_sm100_int8_shared_store():
@T.prim_func
def main(A: T.Tensor((16, 8), T.int8)):
with T.Kernel(1, threads=32):
frag = T.alloc_fragment((16, 8), T.int8)
smem = T.alloc_shared((16, 8), T.int8)
T.annotate_layout({frag: make_gemm_fragment_8x8_transposed().repeat([2, 1], repeat_on_thread=False)})
for i, j in T.Parallel(16, 8):
frag[i, j] = A[i, j]
T.copy(frag, smem)
target = {"kind": "cuda", "arch": "sm_100a"}
with tvm.target.Target(target):
artifact = tilelang.lower(main, target=target)
assert "tl::ptx_stmatrix_m16n8_x1_trans" in artifact.kernel_source
if __name__ == "__main__":
tilelang.testing.main()