forked from ccf-ai-infra/TileOPs-Metax
209 lines
7.2 KiB
Python
209 lines
7.2 KiB
Python
"""Unit tests for trace payload support."""
|
|
import pytest
|
|
import tilelang
|
|
import tilelang.language as T
|
|
import torch
|
|
|
|
from tileops.trace import trace
|
|
|
|
# Mark all tests in this file as 'full' tier
|
|
pytestmark = pytest.mark.full
|
|
|
|
|
|
@pytest.fixture
|
|
def preserve_trace_state():
|
|
"""Save and restore trace state to avoid breaking global --trace-kernel."""
|
|
original_enabled = trace.enabled
|
|
original_output = trace.output
|
|
try:
|
|
yield
|
|
finally:
|
|
# Restore original state
|
|
if original_enabled:
|
|
trace.enable(output=original_output)
|
|
else:
|
|
trace.disable()
|
|
|
|
|
|
def test_payload_api_signature(preserve_trace_state):
|
|
"""Test that payload parameter is accepted by trace APIs."""
|
|
trace.disable()
|
|
|
|
# Test range() accepts payload
|
|
@tilelang.jit(out_idx=trace.out_idx(1))
|
|
def build1():
|
|
@T.prim_func
|
|
def kernel(out: T.Buffer((16,), "float32")):
|
|
with T.Kernel(1, threads=16), trace.range("test", payload=0):
|
|
pass
|
|
return trace.finalize(kernel)
|
|
|
|
# Should compile without error
|
|
kernel = build1()
|
|
assert kernel is not None
|
|
|
|
|
|
def test_payload_with_range_start_end(preserve_trace_state, tmp_path):
|
|
"""Test payload with explicit range_start/range_end with decode."""
|
|
trace.enable(output=str(tmp_path))
|
|
|
|
@tilelang.jit(out_idx=trace.out_idx(1))
|
|
def build():
|
|
@T.prim_func
|
|
def kernel(out: T.Buffer((16,), "float32")):
|
|
with T.Kernel(1, threads=16):
|
|
tx = T.get_thread_binding()
|
|
# Test range_start accepts payload
|
|
tok = trace.range_start("test_range", payload=42)
|
|
out[tx] = T.float32(tx)
|
|
trace.range_end(tok)
|
|
|
|
return trace.finalize(kernel)
|
|
|
|
kernel = build()
|
|
|
|
# When tracing is enabled, kernel returns (output, slots)
|
|
result = kernel()
|
|
assert isinstance(result, (tuple, list)), "Expected (output, slots) tuple"
|
|
|
|
output_tensor, slots = result
|
|
|
|
# Verify kernel output
|
|
expected = torch.arange(16, dtype=torch.float32, device="cuda")
|
|
assert torch.allclose(output_tensor, expected)
|
|
|
|
# Decode and verify payload is 42
|
|
events = trace.decode(kernel, slots)
|
|
slices = [e for e in events if e.name == "test_range"]
|
|
|
|
# Should have exactly one slice with payload 42
|
|
assert len(slices) == 1, f"Expected exactly one test_range slice, got {len(slices)}"
|
|
assert slices[0].payload == 42, f"Expected payload 42, got {slices[0].payload}"
|
|
|
|
|
|
def test_payload_backward_compatibility(preserve_trace_state):
|
|
"""Test that existing code without payload still works."""
|
|
trace.disable()
|
|
|
|
@tilelang.jit(out_idx=trace.out_idx(1))
|
|
def build():
|
|
@T.prim_func
|
|
def kernel(out: T.Buffer((16,), "float32")):
|
|
with T.Kernel(1, threads=16):
|
|
# Old code without payload should still work
|
|
with trace.range("test"):
|
|
pass
|
|
|
|
# With lane but no payload
|
|
with trace.range("test2", lane="compute"):
|
|
pass
|
|
|
|
return trace.finalize(kernel)
|
|
|
|
kernel = build()
|
|
assert kernel is not None
|
|
|
|
|
|
def test_implicit_thread_blocks_with_payload_e2e(preserve_trace_state, tmp_path):
|
|
"""End-to-end test: writer-election fallback + constant payload.
|
|
|
|
This test verifies:
|
|
1. trace.range(..., payload=...) actually lowers to CUDA markers
|
|
2. Payload is written to slots and can be decoded
|
|
3. Writer-election fallback (__tl_thread_idx_x) works when threadIdx.x is not bound
|
|
4. Range begin/end pairs correctly into a slice
|
|
|
|
Note: __tl_thread_idx_x() is used for writer-election (determining which thread
|
|
writes markers), NOT as a payload source. The payload=42 is an explicit user-provided
|
|
constant, unrelated to thread indices.
|
|
"""
|
|
trace.enable(output=str(tmp_path))
|
|
|
|
@tilelang.jit(out_idx=trace.out_idx(1))
|
|
def build():
|
|
@T.prim_func
|
|
def kernel(out: T.Buffer((16,), "float32")):
|
|
# Use simple T.Kernel(..., threads=16) - no explicit threadIdx.x binding
|
|
# This triggers the __tl_thread_idx_x() writer-election fallback
|
|
with T.Kernel(1, threads=16):
|
|
tx = T.get_thread_binding()
|
|
# Explicit payload=42 (user-provided constant)
|
|
with trace.range("test_range", payload=42):
|
|
out[tx] = T.float32(tx)
|
|
|
|
return trace.finalize(kernel)
|
|
|
|
kernel = build()
|
|
|
|
# When tracing is enabled, kernel returns (output, slots)
|
|
# out_idx indicates output is at index 1, so kernel takes no inputs
|
|
result = kernel()
|
|
assert isinstance(result, (tuple, list)), "Expected (output, slots) tuple"
|
|
|
|
output_tensor, slots = result
|
|
|
|
# Verify kernel output
|
|
expected = torch.arange(16, dtype=torch.float32, device="cuda")
|
|
assert torch.allclose(output_tensor, expected)
|
|
|
|
# Decode and verify payload
|
|
events = trace.decode(kernel, slots)
|
|
slices = [e for e in events if e.name == "test_range"]
|
|
|
|
# Should have exactly one slice with payload 42 (the explicit user-provided value)
|
|
assert len(slices) == 1, f"Expected exactly one test_range slice, got {len(slices)}"
|
|
assert slices[0].payload == 42, f"Expected payload 42, got {slices[0].payload}"
|
|
|
|
|
|
def test_dynamic_payload_runtime_expr(preserve_trace_state, tmp_path):
|
|
"""End-to-end test: dynamic payload with explicit runtime PrimExpr (loop index).
|
|
|
|
This test verifies that payload supports runtime expressions, not just constants.
|
|
Uses a simple non-pipelined loop where the user explicitly provides the loop index
|
|
as payload. This demonstrates that payloads are user-provided tags, not implicit
|
|
values derived from thread/block indices.
|
|
|
|
The payload values [0, 1, 2, 3] come from the explicit loop index 'i', NOT from
|
|
any implicit thread ID recovery.
|
|
"""
|
|
trace.enable(output=str(tmp_path))
|
|
|
|
@tilelang.jit(out_idx=trace.out_idx(1))
|
|
def build():
|
|
@T.prim_func
|
|
def kernel(out: T.Buffer((4,), "float32")):
|
|
# Use simple T.Kernel with threads=4
|
|
with T.Kernel(1, threads=4):
|
|
tx = T.get_thread_binding()
|
|
# Explicitly use loop index as payload - this is a user-provided runtime PrimExpr
|
|
for i in range(4):
|
|
with trace.range("loop_iter", payload=i):
|
|
if tx == 0: # Only thread 0 writes
|
|
out[i] = T.float32(i)
|
|
|
|
return trace.finalize(kernel)
|
|
|
|
kernel = build()
|
|
result = kernel()
|
|
assert isinstance(result, (tuple, list)), "Expected (output, slots) tuple"
|
|
|
|
output_tensor, slots = result
|
|
|
|
# Verify kernel output
|
|
expected = torch.tensor([0.0, 1.0, 2.0, 3.0], dtype=torch.float32, device="cuda")
|
|
assert torch.allclose(output_tensor, expected)
|
|
|
|
# Decode and verify payload values
|
|
events = trace.decode(kernel, slots)
|
|
loop_slices = [e for e in events if e.name == "loop_iter"]
|
|
|
|
# Should have 4 slices with explicit user-provided payloads [0, 1, 2, 3]
|
|
assert len(loop_slices) >= 4, f"Expected at least 4 loop_iter slices, got {len(loop_slices)}"
|
|
|
|
payloads = sorted([s.payload for s in loop_slices[:4]])
|
|
assert payloads == [0, 1, 2, 3], f"Expected payloads [0, 1, 2, 3], got {payloads}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|