mirror of
https://github.com/tile-ai/tilelang.git
synced 2026-10-21 06:40:23 +08:00
* [BugFix] Complete CuTeDSL scan lowering for TileKernels TileKernels exercises CuTeDSL scan and routing patterns that were not fully covered by the existing Python backend helpers. The MoE get_fused_mapping kernel computes expert base offsets with T.cumsum over a 256-entry shared buffer. CuTeDSL CumSum1D only performed a single warp-local prefix scan, while the CUDA scan template uses the first warp to scan all 32-element segments and carry the result between segments. As a result, expert prefixes reset at later warp-sized chunks, positions for experts such as 32 and 64 overlapped earlier expert ranges, and pos_to_expert validation failed on H100. Bring the CuTeDSL scan helpers in line with the CUDA InclusiveScanLine behavior: add reusable one-warp line scans with carry across 32-element segments, route CumSum1D and CumMax1D through them, and use the same line scan for CumSum2D/CumMax2D on both row-wise and column-wise axes. This removes the previous dim=0 H<=32 limitation and fixes multi-segment cummax as well as cumsum. The same TileKernels path also relies on CuTeDSL codegen accepting non-flat buffer accesses after TIR lowering. Add a shared index linearization helper for loads, stores, address_of/reinterpret, and atomic pointer generation, and lower tl.match_any_sync through the existing CuTeDSL warp intrinsic. Thread binding now falls back to the IterVar name when thread_tag has already been normalized away. Verified on H100 CUDA/CuTeDSL with CUDA_VISIBLE_DEVICES=0 and TILELANG_TARGET=cutedsl: - cmake --build build -j$(nproc) - ruff format --check tilelang/contrib/cutedsl/reduce.py - ruff check tilelang/contrib/cutedsl/reduce.py - git diff --check - pytest testing/python/target/test_tilelang_codegen_cutedsl_scan.py testing/python/language/test_tilelang_language_scan.py -q -s --tb=short: 13 passed - pytest testing/python/language/test_tilelang_language_warp_vote.py::test_match_any_sync -q -s --tb=short: 1 passed - pytest tests/moe/test_get_fused_mapping.py::test_get_fused_mapping[num_send_tokens=4001-num_topk=2-num_experts=72-num_ep_ranks=1-alignment=64] -q -s --tb=short: 1 passed Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Resolve CuTeDSL dynamic shapes from live tensors CuTeDSL records dynamic symbolic dimensions in the same ordering as the CUDA wrapper, but it previously kept only the first buffer shape or stride location for each symbol. That is not sufficient for kernels with optional tensor arguments. TileKernels reduce_fused declares num_tokens on topk_weights, token_topk_to_pos, and out; when with_weights is false the topk_weights argument is None, so the first recorded source is not a live tensor. The previous adapter fallback substituted 0 when that first source was None. On H100 with TILELANG_TARGET=cutedsl this passed num_tokens=0 to the generated launcher even though token_topk_to_pos had shape (4001, 2), producing grid=[0, 1, 1] and a CUDA error 1 launch failure in reduce_fused. Keep all shape/stride candidates for each dynamic symbol and resolve the runtime value from the first candidate backed by a real torch.Tensor. Reuse the same resolution helper for allocated output shapes and for dynamic arguments passed to the generated CuTeDSL module, and fail clearly if no live tensor source exists. Verified with a CuTeDSL adapter regression test for repeated dynamic shape symbols with a None first candidate, and with the TileKernels reduce_fused H100 failure case that previously launched with num_tokens=0. Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Permit dead optional CuTeDSL stride symbols TileKernels no-scale MoE expand paths can keep a dynamic stride symbol in the generated CuTeDSL host wrapper ABI even when the optional scale tensor is absent from the lowered device kernel variant. In that case the runtime argument is intentionally None, so the stride-only symbol has no live tensor from which the adapter can read torch stride metadata. The previous dynamic-symbol fix made missing live tensor sources strict to avoid unsafe shape fallbacks such as launching with num_tokens=0 when the first candidate tensor was optional. That strict behavior is still required for shape symbols because they control output allocation and launch dimensions. Limit the fallback to symbols whose candidates are stride-only. Such values are ABI placeholders for optional strided tensor parameters in pruned variants; returning 0 preserves the generated call signature without weakening dynamic shape resolution. Add adapter regressions covering both the optional stride fallback and the strict missing-shape error. Verified on H100 with TILELANG_TARGET=cutedsl: - ruff format/check on the touched files - pytest testing/python/jit/test_tilelang_jit_cutedsl_host_codegen.py::test_cutedsl_adapter_resolves_dynamic_symbol_from_live_tensor_candidate testing/python/jit/test_tilelang_jit_cutedsl_host_codegen.py::test_cutedsl_adapter_allows_optional_stride_symbol_without_live_tensor -q - pytest tests/moe/test_expand_to_fused.py::test_expand_to_fused[num_send_tokens=4001-num_topk=2-num_experts=9-num_ep_ranks=8-hidden=576] -vv -s --tb=short Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Fix CuTeDSL optional ABI and early-return codegen TileKernels full functional coverage with TILELANG_TARGET=cutedsl exposed additional CuTeDSL gaps in top2_sum_gate after the broader TileKernels fixes. First, the host adapter still appended dynamic shape symbols for optional tensors that had been compiled out by static flags. In top2_sum_gate, to_physical_map/logical_count can be None when logical expert mapping is disabled, but their shape-only symbol remains in the wrapper ABI. Keep output allocation strict, but allow dead host ABI dynamic shape arguments to use a zero placeholder when no live tensor source exists. Second, CuTeDSL cannot emit dynamic thread returns directly, so codegen rewrites if cond: thread_return(); rest into if not cond: rest. The existing recognizer only handled a bare thread_return then body, and missed the common if cond: stores; thread_return() pattern. Preserve the pre-return stores and guard the following statements so masked tokens cannot be overwritten by the fallthrough path. Third, vector binary codegen reused SSA aliases for operands containing BufferLoad nodes. That is not valid for mutable local tensors after an intervening store. The softmax top2_sum_gate path restored raw logits to scores_local, but the subsequent scores_local + bias expression reused an older softmax load and ranked by softmax(logits)+bias instead of logits+bias. Avoid SSA alias reuse for vector operands containing BufferLoad so the generated code reloads the current tensor value. Verified on H100 with the local TileLang build and TileKernels CuTeDSL backend: - cmake --build build -j 32 - ruff format/check on touched Python files - pytest testing/python/jit/test_tilelang_jit_cutedsl_host_codegen.py::{dynamic symbol candidate tests} -q: 3 passed - tests/moe/test_top2_sum_gate.py::test_top2_sum_gate[num_groups=0-num_topk_groups=0-num_routed_experts=72-num_shared_experts=1-num_topk=6] with TILELANG_TARGET=cutedsl TK_FULL_TEST=1: 1 passed Co-authored-by: dingsg <shengge.ding@enflame-tech.com> * [BugFix] Promote narrow CuTeDSL shifts before wide casts TileKernels e5m6 quantization kernels pack 8 truncated fp16 values into three uint32 words. The TileLang source writes expressions such as T.cast(half_u16[i] << 20, T.uint32), which the CUDA C++ backend evaluates with normal integer promotion before the final uint32 cast. CuTeDSL uses strongly typed cutlass integer wrappers instead of CUDA C integer promotion. The previous CuTeDSL lowering cast the shift result back to the narrow TIR result type first, producing code like cutlass.Uint32(cutlass.Uint16((half_u16[0] << cutlass.Uint16(20)))). For shifts by more than the uint16 width this drops all high bits before the packed uint32 store, corrupting e5m6 payload bytes. Cast-back then faithfully decoded the corrupted payload into nan/inf values, so TileKernels full correctness failed in cast_back_e5m6 and per_token_cast_to_e5m6 cases. When a narrow integer shift is immediately cast to a wider integer type, emit the CuTeDSL shift with the left operand promoted to the target integer type before shifting. This preserves the intended packed-bit construction while leaving narrow shift expressions that are not widened on the existing path. Add a CuTeDSL codegen regression test for uint16 << 20 widened to uint32 so future changes do not reintroduce a narrow in-place shift. Verified on H100 CUDA/CuTeDSL with TILELANG_TARGET=cutedsl and TILELANG_DISABLE_CACHE=1: - cmake --build build -j 32 - python -m pytest testing/python/target/test_tilelang_codegen_cutedsl_integer_shift.py -q - TileKernels focused e5m6 quant tests: tests/quant/test_per_token_cast_to_e5m6.py tests/quant/test_cast_back_e5m6.py -n 2: 168 passed, 112 skipped Co-authored-by: dingsg <shengge.ding@enflame-tech.com> --------- Co-authored-by: dingsg <shengge.ding@enflame-tech.com>
492 lines
17 KiB
Python
492 lines
17 KiB
Python
from tilelang import tvm as tvm
|
|
import tilelang.testing
|
|
import tilelang as tl
|
|
import torch
|
|
import tilelang.language as T
|
|
|
|
|
|
def _torch_cummax(chunk, dim, reverse):
|
|
if reverse:
|
|
return torch.flip(torch.flip(chunk, dims=[dim]).cummax(dim=dim).values, dims=[dim])
|
|
return chunk.cummax(dim=dim).values
|
|
|
|
|
|
def cumsum_smem_test(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
@T.prim_func
|
|
def cumsum(
|
|
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=256) as (bx, by):
|
|
A_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
|
|
T.copy(A[by * block_M, bx * block_N], A_shared)
|
|
T.cumsum(src=A_shared, dim=dim, reverse=reverse)
|
|
T.copy(A_shared, B[by * block_M, bx * block_N])
|
|
|
|
return cumsum
|
|
|
|
|
|
def cumsum_fragment_test(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
import tilelang.language as T
|
|
|
|
@T.prim_func
|
|
def cumsum(
|
|
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=256) as (bx, by):
|
|
A_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
A_fragment = T.alloc_fragment((block_M, block_N), dtype)
|
|
|
|
T.copy(A[by * block_M, bx * block_N], A_shared)
|
|
T.copy(A_shared, A_fragment)
|
|
T.cumsum(src=A_fragment, dim=dim, reverse=reverse)
|
|
T.copy(A_fragment, B[by * block_M, bx * block_N])
|
|
|
|
return cumsum
|
|
|
|
|
|
def run_cumsum(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32, scope="smem"):
|
|
if scope == "smem":
|
|
program = cumsum_smem_test(M, N, block_M, block_N, dim, reverse, dtype)
|
|
elif scope == "fragment":
|
|
program = cumsum_fragment_test(M, N, block_M, block_N, dim, reverse, dtype)
|
|
jit_kernel = tl.compile(program, out_idx=-1)
|
|
|
|
A = torch.randn(M, N, dtype=getattr(torch, dtype)).cuda()
|
|
|
|
def ref_program(A):
|
|
ref_b = torch.empty_like(A)
|
|
for i in range(M // block_M):
|
|
for j in range(N // block_N):
|
|
ref_b[i * block_M : (i + 1) * block_M, j * block_N : (j + 1) * block_N] = A[
|
|
i * block_M : (i + 1) * block_M, j * block_N : (j + 1) * block_N
|
|
].cumsum(dim=dim)
|
|
if reverse:
|
|
ref_b[i * block_M : (i + 1) * block_M, j * block_N : (j + 1) * block_N] = (
|
|
A[i * block_M : (i + 1) * block_M, j * block_N : (j + 1) * block_N]
|
|
.flip(dims=[dim])
|
|
.cumsum(dim=dim)
|
|
.flip(dims=[dim])
|
|
)
|
|
return ref_b
|
|
|
|
tilelang_res = jit_kernel(A)
|
|
ref_res = ref_program(A)
|
|
torch.testing.assert_close(tilelang_res, ref_res, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
def cumsum_smem_test_1d(N, block_N, reverse=False, dtype=T.float32):
|
|
import tilelang.language as T
|
|
|
|
@T.prim_func
|
|
def cumsum(
|
|
A: T.Tensor((N,), dtype),
|
|
B: T.Tensor((N,), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), threads=block_N) as bx:
|
|
A_shared = T.alloc_shared((block_N,), dtype)
|
|
|
|
T.copy(A[bx * block_N], A_shared)
|
|
T.cumsum(src=A_shared, dim=0, reverse=reverse)
|
|
T.copy(A_shared, B[bx * block_N])
|
|
|
|
return cumsum
|
|
|
|
|
|
def cumsum_fragment_test_1d(N, block_N, reverse=False, dtype=T.float32):
|
|
import tilelang.language as T
|
|
|
|
@T.prim_func
|
|
def cumsum(
|
|
A: T.Tensor((N,), dtype),
|
|
B: T.Tensor((N,), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), threads=block_N) as bx:
|
|
A_shared = T.alloc_shared((block_N,), dtype)
|
|
A_fragment = T.alloc_fragment((block_N,), dtype)
|
|
|
|
T.copy(A[bx * block_N], A_shared)
|
|
T.copy(A_shared, A_fragment)
|
|
T.cumsum(src=A_fragment, dim=0, reverse=reverse)
|
|
T.copy(A_fragment, B[bx * block_N])
|
|
|
|
return cumsum
|
|
|
|
|
|
def run_cumsum_1d(N, block_N, reverse=False, dtype=T.float32, scope="smem"):
|
|
if scope == "smem":
|
|
program = cumsum_smem_test_1d(N, block_N, reverse, dtype)
|
|
elif scope == "fragment":
|
|
program = cumsum_fragment_test_1d(N, block_N, reverse, dtype)
|
|
else:
|
|
raise ValueError(f"Unknown scope {scope}")
|
|
|
|
jit_kernel = tl.compile(program, out_idx=-1)
|
|
A = torch.randn(N, dtype=getattr(torch, dtype)).cuda()
|
|
|
|
def ref_program(A):
|
|
ref_b = torch.empty_like(A)
|
|
num_blocks = (N + block_N - 1) // block_N
|
|
for j in range(num_blocks):
|
|
start = j * block_N
|
|
end = min(start + block_N, N)
|
|
chunk = A[start:end]
|
|
if reverse:
|
|
chunk = torch.flip(chunk, dims=[0])
|
|
chunk = chunk.cumsum(dim=0)
|
|
if reverse:
|
|
chunk = torch.flip(chunk, dims=[0])
|
|
ref_b[start:end] = chunk
|
|
return ref_b
|
|
|
|
tilelang_res = jit_kernel(A)
|
|
ref_res = ref_program(A)
|
|
torch.testing.assert_close(tilelang_res, ref_res, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
def test_cumsum_smem():
|
|
# Test different sizes
|
|
run_cumsum(256, 256, 64, 64)
|
|
run_cumsum(256, 256, 64, 64, dim=1)
|
|
run_cumsum(256, 256, 64, 64, dim=1, reverse=True)
|
|
run_cumsum(192, 160, 64, 32, dim=0)
|
|
run_cumsum(192, 160, 64, 32, dim=0, reverse=True)
|
|
|
|
# Test different dtypes
|
|
run_cumsum(128, 128, 64, 64, dtype=T.float32)
|
|
|
|
|
|
def test_cumsum_fragment():
|
|
run_cumsum(256, 256, 64, 64, scope="fragment")
|
|
run_cumsum(256, 256, 64, 64, dim=1, scope="fragment")
|
|
run_cumsum(256, 256, 64, 64, dim=1, reverse=True, scope="fragment")
|
|
|
|
# Test different dtypes
|
|
run_cumsum(128, 128, 64, 64, dtype=T.float32, scope="fragment")
|
|
|
|
|
|
def test_cumsum_smem_1d():
|
|
run_cumsum_1d(512, 64)
|
|
run_cumsum_1d(512, 64, reverse=True)
|
|
|
|
|
|
def test_cumsum_fragment_1d():
|
|
run_cumsum_1d(512, 64, scope="fragment")
|
|
run_cumsum_1d(512, 64, reverse=True, scope="fragment")
|
|
|
|
|
|
def cumsum_region_test_1d(N, chunk_size, reverse=False, dtype=T.float32):
|
|
"""Test cumsum with buffer region (slice) as input."""
|
|
import tilelang.language as T
|
|
|
|
@T.prim_func
|
|
def cumsum_region(
|
|
InputG_fragment: T.Tensor((N,), dtype),
|
|
OutputG_fragment: T.Tensor((N,), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, chunk_size), threads=chunk_size) as bx:
|
|
i = bx
|
|
chunk_start = i * chunk_size
|
|
# Copy region to shared memory first (cumsum only supports shared memory)
|
|
A_shared = T.alloc_shared((chunk_size,), dtype)
|
|
T.copy(InputG_fragment[chunk_start : chunk_start + chunk_size], A_shared)
|
|
# Test cumsum with region input - in-place operation on shared memory
|
|
# This demonstrates the feature: T.cumsum(region, dim=0)
|
|
T.cumsum(src=A_shared, dim=0, reverse=reverse)
|
|
# Copy result back to global memory
|
|
T.copy(A_shared, OutputG_fragment[chunk_start : chunk_start + chunk_size])
|
|
|
|
return cumsum_region
|
|
|
|
|
|
def run_cumsum_region_1d(N, chunk_size, reverse=False, dtype=T.float32):
|
|
"""Run test for cumsum with region input."""
|
|
program = cumsum_region_test_1d(N, chunk_size, reverse, dtype)
|
|
jit_kernel = tl.compile(program, out_idx=-1)
|
|
A = torch.randn(N, dtype=getattr(torch, dtype)).cuda()
|
|
|
|
def ref_program(A):
|
|
ref_b = torch.empty_like(A)
|
|
num_blocks = (N + chunk_size - 1) // chunk_size
|
|
for j in range(num_blocks):
|
|
start = j * chunk_size
|
|
end = min(start + chunk_size, N)
|
|
chunk = A[start:end].clone()
|
|
if reverse:
|
|
chunk = torch.flip(chunk, dims=[0])
|
|
chunk = chunk.cumsum(dim=0)
|
|
if reverse:
|
|
chunk = torch.flip(chunk, dims=[0])
|
|
ref_b[start:end] = chunk
|
|
return ref_b
|
|
|
|
tilelang_res = jit_kernel(A)
|
|
ref_res = ref_program(A)
|
|
torch.testing.assert_close(tilelang_res, ref_res, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
def cumsum_region_test_2d(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
"""Test cumsum with buffer region (slice) as input in 2D."""
|
|
import tilelang.language as T
|
|
|
|
@T.prim_func
|
|
def cumsum_region(
|
|
InputG_fragment: T.Tensor((M, N), dtype),
|
|
OutputG_fragment: T.Tensor((M, N), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=256) as (bx, by):
|
|
chunk_start_M = by * block_M
|
|
chunk_start_N = bx * block_N
|
|
# Copy region to shared memory first (cumsum only supports shared memory)
|
|
A_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
T.copy(
|
|
InputG_fragment[chunk_start_M : chunk_start_M + block_M, chunk_start_N : chunk_start_N + block_N],
|
|
A_shared,
|
|
)
|
|
# Test cumsum with 2D region input - in-place operation on shared memory
|
|
T.cumsum(src=A_shared, dim=dim, reverse=reverse)
|
|
# Copy result back to global memory
|
|
T.copy(
|
|
A_shared,
|
|
OutputG_fragment[chunk_start_M : chunk_start_M + block_M, chunk_start_N : chunk_start_N + block_N],
|
|
)
|
|
|
|
return cumsum_region
|
|
|
|
|
|
def run_cumsum_region_2d(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
"""Run test for cumsum with 2D region input."""
|
|
program = cumsum_region_test_2d(M, N, block_M, block_N, dim, reverse, dtype)
|
|
jit_kernel = tl.compile(program, out_idx=-1)
|
|
A = torch.randn(M, N, dtype=getattr(torch, dtype)).cuda()
|
|
|
|
def ref_program(A):
|
|
ref_b = torch.empty_like(A)
|
|
num_blocks_M = (M + block_M - 1) // block_M
|
|
num_blocks_N = (N + block_N - 1) // block_N
|
|
for i in range(num_blocks_M):
|
|
for j in range(num_blocks_N):
|
|
start_M = i * block_M
|
|
end_M = min(start_M + block_M, M)
|
|
start_N = j * block_N
|
|
end_N = min(start_N + block_N, N)
|
|
chunk = A[start_M:end_M, start_N:end_N].clone()
|
|
if reverse:
|
|
chunk = torch.flip(chunk, dims=[dim])
|
|
chunk = chunk.cumsum(dim=dim)
|
|
if reverse:
|
|
chunk = torch.flip(chunk, dims=[dim])
|
|
ref_b[start_M:end_M, start_N:end_N] = chunk
|
|
return ref_b
|
|
|
|
tilelang_res = jit_kernel(A)
|
|
ref_res = ref_program(A)
|
|
torch.testing.assert_close(tilelang_res, ref_res, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
def test_cumsum_region_1d():
|
|
"""Test cumsum with 1D region input."""
|
|
# Test normal cumsum with region input
|
|
run_cumsum_region_1d(512, 64)
|
|
# Test reverse cumsum with region input
|
|
run_cumsum_region_1d(512, 64, reverse=True)
|
|
# Test with different chunk sizes
|
|
run_cumsum_region_1d(384, 128)
|
|
# Tail coverage (non-divisible size)
|
|
run_cumsum_region_1d(250, 64)
|
|
|
|
|
|
def test_cumsum_region_2d():
|
|
"""Test cumsum with 2D region input."""
|
|
# Test 2D cumsum along dim 0
|
|
run_cumsum_region_2d(256, 256, 64, 64, dim=0)
|
|
# Test 2D cumsum along dim 1
|
|
run_cumsum_region_2d(256, 256, 64, 64, dim=1)
|
|
# Test reverse cumsum
|
|
run_cumsum_region_2d(192, 192, 64, 64, dim=1, reverse=True)
|
|
# Tail coverage (non-divisible size)
|
|
run_cumsum_region_2d(250, 250, 64, 64, dim=1)
|
|
|
|
|
|
def cummax_smem_test(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
@T.prim_func
|
|
def cummax(
|
|
A: T.Tensor((M, N), dtype),
|
|
B: T.Tensor((M, N), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=256) as (bx, by):
|
|
A_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
|
|
T.copy(A[by * block_M, bx * block_N], A_shared)
|
|
T.cummax(src=A_shared, dim=dim, reverse=reverse)
|
|
T.copy(A_shared, B[by * block_M, bx * block_N])
|
|
|
|
return cummax
|
|
|
|
|
|
def cummax_fragment_test(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
@T.prim_func
|
|
def cummax(
|
|
A: T.Tensor((M, N), dtype),
|
|
B: T.Tensor((M, N), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=256) as (bx, by):
|
|
A_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
A_fragment = T.alloc_fragment((block_M, block_N), dtype)
|
|
|
|
T.copy(A[by * block_M, bx * block_N], A_shared)
|
|
T.copy(A_shared, A_fragment)
|
|
T.cummax(src=A_fragment, dim=dim, reverse=reverse)
|
|
T.copy(A_fragment, B[by * block_M, bx * block_N])
|
|
|
|
return cummax
|
|
|
|
|
|
def cummax_smem_out_test(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32):
|
|
@T.prim_func
|
|
def cummax(
|
|
A: T.Tensor((M, N), dtype),
|
|
B: T.Tensor((M, N), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=256) as (bx, by):
|
|
A_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
B_shared = T.alloc_shared((block_M, block_N), dtype)
|
|
|
|
T.copy(A[by * block_M, bx * block_N], A_shared)
|
|
T.cummax(src=A_shared, dst=B_shared, dim=dim, reverse=reverse)
|
|
T.copy(B_shared, B[by * block_M, bx * block_N])
|
|
|
|
return cummax
|
|
|
|
|
|
def run_cummax(M, N, block_M, block_N, dim=0, reverse=False, dtype=T.float32, scope="smem"):
|
|
if scope == "smem":
|
|
program = cummax_smem_test(M, N, block_M, block_N, dim, reverse, dtype)
|
|
elif scope == "fragment":
|
|
program = cummax_fragment_test(M, N, block_M, block_N, dim, reverse, dtype)
|
|
elif scope == "smem_out":
|
|
program = cummax_smem_out_test(M, N, block_M, block_N, dim, reverse, dtype)
|
|
else:
|
|
raise ValueError(f"Unknown scope {scope}")
|
|
jit_kernel = tl.compile(program, out_idx=-1)
|
|
|
|
A = torch.randn(M, N, dtype=getattr(torch, dtype)).cuda()
|
|
|
|
def ref_program(A):
|
|
ref_b = torch.empty_like(A)
|
|
for i in range((M + block_M - 1) // block_M):
|
|
for j in range((N + block_N - 1) // block_N):
|
|
start_m = i * block_M
|
|
end_m = min(start_m + block_M, M)
|
|
start_n = j * block_N
|
|
end_n = min(start_n + block_N, N)
|
|
ref_b[start_m:end_m, start_n:end_n] = _torch_cummax(A[start_m:end_m, start_n:end_n], dim, reverse)
|
|
return ref_b
|
|
|
|
tilelang_res = jit_kernel(A)
|
|
ref_res = ref_program(A)
|
|
torch.testing.assert_close(tilelang_res, ref_res, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
def cummax_smem_test_1d(N, block_N, reverse=False, dtype=T.float32, threads=None):
|
|
if threads is None:
|
|
threads = block_N
|
|
|
|
@T.prim_func
|
|
def cummax(
|
|
A: T.Tensor((N,), dtype),
|
|
B: T.Tensor((N,), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), threads=threads) as bx:
|
|
A_shared = T.alloc_shared((block_N,), dtype)
|
|
|
|
T.copy(A[bx * block_N], A_shared)
|
|
T.cummax(src=A_shared, dim=0, reverse=reverse)
|
|
T.copy(A_shared, B[bx * block_N])
|
|
|
|
return cummax
|
|
|
|
|
|
def cummax_fragment_test_1d(N, block_N, reverse=False, dtype=T.float32):
|
|
@T.prim_func
|
|
def cummax(
|
|
A: T.Tensor((N,), dtype),
|
|
B: T.Tensor((N,), dtype),
|
|
):
|
|
with T.Kernel(T.ceildiv(N, block_N), threads=block_N) as bx:
|
|
A_shared = T.alloc_shared((block_N,), dtype)
|
|
A_fragment = T.alloc_fragment((block_N,), dtype)
|
|
|
|
T.copy(A[bx * block_N], A_shared)
|
|
T.copy(A_shared, A_fragment)
|
|
T.cummax(src=A_fragment, dim=0, reverse=reverse)
|
|
T.copy(A_fragment, B[bx * block_N])
|
|
|
|
return cummax
|
|
|
|
|
|
def run_cummax_1d(N, block_N, reverse=False, dtype=T.float32, scope="smem", negative_input=False, threads=None):
|
|
if scope == "smem":
|
|
program = cummax_smem_test_1d(N, block_N, reverse, dtype, threads)
|
|
elif scope == "fragment":
|
|
program = cummax_fragment_test_1d(N, block_N, reverse, dtype)
|
|
else:
|
|
raise ValueError(f"Unknown scope {scope}")
|
|
|
|
jit_kernel = tl.compile(program, out_idx=-1)
|
|
torch_dtype = getattr(torch, dtype)
|
|
if negative_input:
|
|
A = -torch.arange(1, N + 1, dtype=torch.float32, device="cuda").to(torch_dtype)
|
|
else:
|
|
A = torch.randn(N, dtype=torch_dtype).cuda()
|
|
|
|
def ref_program(A):
|
|
ref_b = torch.empty_like(A)
|
|
num_blocks = (N + block_N - 1) // block_N
|
|
for j in range(num_blocks):
|
|
start = j * block_N
|
|
end = min(start + block_N, N)
|
|
ref_b[start:end] = _torch_cummax(A[start:end], 0, reverse)
|
|
return ref_b
|
|
|
|
tilelang_res = jit_kernel(A)
|
|
ref_res = ref_program(A)
|
|
torch.testing.assert_close(tilelang_res, ref_res, atol=1e-3, rtol=1e-3)
|
|
|
|
|
|
def test_cummax_smem():
|
|
run_cummax(256, 256, 64, 64)
|
|
run_cummax(256, 256, 64, 64, dim=1)
|
|
run_cummax(256, 256, 64, 64, dim=1, reverse=True)
|
|
run_cummax(192, 160, 64, 32, dim=0)
|
|
run_cummax(192, 160, 64, 32, dim=0, reverse=True)
|
|
|
|
|
|
def test_cummax_fragment():
|
|
run_cummax(256, 256, 64, 64, scope="fragment")
|
|
run_cummax(256, 256, 64, 64, dim=1, scope="fragment")
|
|
run_cummax(256, 256, 64, 64, dim=1, reverse=True, scope="fragment")
|
|
|
|
|
|
def test_cummax_out_of_place():
|
|
run_cummax(128, 128, 64, 64, dim=1, scope="smem_out")
|
|
|
|
|
|
def test_cummax_smem_1d():
|
|
run_cummax_1d(512, 64)
|
|
run_cummax_1d(512, 64, reverse=True)
|
|
run_cummax_1d(80, 40, reverse=True, negative_input=True, threads=64)
|
|
|
|
|
|
def test_cummax_fragment_1d():
|
|
run_cummax_1d(512, 64, scope="fragment")
|
|
run_cummax_1d(512, 64, reverse=True, scope="fragment")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
tilelang.testing.main()
|