859498dc01
## Summary This PR adds the initial TIRx support needed for low-level programming of Blackwell-class GPU architectures. As part of the ongoing TIRx refactor, it introduces TVMScript support for directly scripting advanced hardware features without relying on scheduling as the primary programming interface. The change keeps existing `s_tir` script support intact while making direct scripting a first-class path for TIRx programs. ## Main Changes - Add TIRx operator dispatch and layout infrastructure. - Add TVMScript support for new low-level TIRx operations. - Add analysis, transform, and lowering support for TIRx IR nodes. - Add CUDA/Blackwell-oriented codegen and intrinsic coverage. - Add Python and C++ integration points for TIRx scripting and runtime support. ## Validation - `pre-commit run --all-files` - `ninja -C build -j32` - `CUDA_VISIBLE_DEVICES=2 pytest tests/python/tirx/ -n 16` - `1723 passed, 47 skipped, 32 warnings` - `CUDA_VISIBLE_DEVICES=2 python -m pytest -v tests/python/all-platform-minimal-test` - `37 passed, 105 skipped` - `TVM_TEST_TARGETS=llvm python -m pytest -v tests/python/tirx-analysis tests/python/tirx-base tests/python/tirx-transform -n 16` - `664 passed, 25 skipped, 9 xfailed, 1 xpassed` ## Local CI Notes Some full CI-equivalent jobs were not locally reproducible because this machine is missing parts of the Apache TVM CI environment, including `llvm-config-15/17`, Vulkan, ROCm, Maven, Sphinx, Doxygen, Emscripten, and ARM/QEMU cross-toolchain components. Metal-specific tests were skipped locally because no Metal runtime is available.
429 lines
16 KiB
Python
429 lines
16 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
"""Unit tests for ExecContext (RFC v3 §6). Cases mirror RFC §8.1 -- §8.10."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from tvm.tirx.exec_context import (
|
|
CLUSTER,
|
|
CTA,
|
|
LANE_CTA_THREAD,
|
|
LANE_FLAT,
|
|
LANE_W_INNER,
|
|
LANE_WG_OUTER,
|
|
LANE_WG_THREAD,
|
|
THREAD,
|
|
WARP,
|
|
WARPGROUP,
|
|
AxisRange,
|
|
ExecContext,
|
|
ExecContextError,
|
|
LaneBinding,
|
|
filter_modulo,
|
|
filter_narrow,
|
|
initial_A,
|
|
scope_switch,
|
|
)
|
|
|
|
# -- canonical bindings declared at kernel entry (see RFC §8 naming conv) --
|
|
WARP_FLAT = LaneBinding(axis="warpid", kind=LANE_FLAT, declared_extent=16)
|
|
WG_OUTER = LaneBinding(axis="warpid", kind=LANE_WG_OUTER, declared_extent=4)
|
|
W_INNER = LaneBinding(axis="warpid", kind=LANE_W_INNER, declared_extent=4)
|
|
LANE_BIND = LaneBinding(axis="laneid", kind=LANE_FLAT, declared_extent=32)
|
|
CTA_BIND = LaneBinding(axis="cta_id", kind=LANE_FLAT, declared_extent=1)
|
|
CTA_THREAD_BIND = LaneBinding(axis="thread", kind=LANE_CTA_THREAD, declared_extent=256)
|
|
WG_THREAD_BIND = LaneBinding(axis="thread", kind=LANE_WG_THREAD, declared_extent=128)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §3 scope_switch: split table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_initial_A_single_cta():
|
|
A = initial_A(warp_ext=16)
|
|
assert A.laneid == AxisRange(32, 0)
|
|
assert A.warpid == AxisRange(16, 0)
|
|
assert A.cta_id == AxisRange(1, 0)
|
|
assert A.size == 512
|
|
|
|
|
|
def test_initial_A_cluster():
|
|
A = initial_A(warp_ext=16, cta_ext=4)
|
|
assert A.cta_id == AxisRange(4, 0)
|
|
assert A.size == 2048
|
|
|
|
|
|
def test_axis_modulo_filter_uses_stride():
|
|
A = initial_A(warp_ext=16, cta_ext=4)
|
|
A = filter_modulo(A, "cta_id", 2, 0)
|
|
assert A.cta_id == AxisRange(2, 0, 2)
|
|
A = filter_narrow(A, CTA_BIND, 1, 4)
|
|
assert A.cta_id == AxisRange(1, 2, 2)
|
|
|
|
|
|
def test_axis_modulo_filter_two_cta_pair_residues():
|
|
A = initial_A(warp_ext=16, cta_ext=2)
|
|
assert filter_modulo(A, "cta_id", 2, 0).cta_id == AxisRange(1, 0, 2)
|
|
assert filter_modulo(A, "cta_id", 2, 1).cta_id == AxisRange(1, 1, 2)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"kappa,expected_inter_axes,expected_intra_axes",
|
|
[
|
|
(THREAD, {"laneid", "warpid", "cta_id"}, set()),
|
|
(WARP, {"warpid", "cta_id"}, {"laneid"}),
|
|
(CTA, {"cta_id"}, {"laneid", "warpid"}),
|
|
(CLUSTER, set(), {"laneid", "warpid", "cta_id"}),
|
|
],
|
|
)
|
|
def test_scope_switch_trivial(kappa, expected_inter_axes, expected_intra_axes):
|
|
A = initial_A(warp_ext=16, cta_ext=4)
|
|
split = scope_switch(A, kappa)
|
|
assert set(split.inter) == expected_inter_axes
|
|
assert set(split.intra) == expected_intra_axes
|
|
|
|
|
|
def test_scope_switch_warpgroup_aligned():
|
|
A = initial_A(warp_ext=16)
|
|
split = scope_switch(A, WARPGROUP)
|
|
assert split.inter["wgid"] == AxisRange(4, 0)
|
|
assert split.inter["cta_id"] == AxisRange(1, 0)
|
|
assert split.intra["laneid"] == AxisRange(32, 0)
|
|
assert split.intra["wid_in_wg"] == AxisRange(4, 0)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §4.2 warpgroup factoring: 3 cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_factor_case1_aligned():
|
|
A = initial_A(warp_ext=8) # ext=8, off=0 -- aligned
|
|
split = scope_switch(A, WARPGROUP)
|
|
assert split.inter["wgid"] == AxisRange(2, 0)
|
|
assert split.intra["wid_in_wg"] == AxisRange(4, 0)
|
|
|
|
|
|
def test_factor_case2_fits_in_one_wg():
|
|
# warpid ext=2, off=0 -- fits in one wg
|
|
A = initial_A(warp_ext=16)
|
|
A = filter_narrow(A, WARP_FLAT, 0, 2)
|
|
split = scope_switch(A, WARPGROUP)
|
|
assert split.inter["wgid"] == AxisRange(1, 0)
|
|
assert split.intra["wid_in_wg"] == AxisRange(2, 0)
|
|
|
|
|
|
def test_factor_case2_offset():
|
|
# warpid ext=2, off=6 -> wid_off=2, fits (2 <= 4-2)
|
|
A = initial_A(warp_ext=16)
|
|
A = filter_narrow(A, WARP_FLAT, 6, 8)
|
|
split = scope_switch(A, WARPGROUP)
|
|
assert split.inter["wgid"] == AxisRange(1, 1)
|
|
assert split.intra["wid_in_wg"] == AxisRange(2, 2)
|
|
|
|
|
|
def test_factor_case3_fails():
|
|
# RFC §8.6: warpid[2:6] crosses wg boundary unaligned
|
|
A = initial_A(warp_ext=16)
|
|
A = filter_narrow(A, WARP_FLAT, 2, 6)
|
|
assert A.warpid == AxisRange(4, 2)
|
|
with pytest.raises(ExecContextError, match="crosses warpgroup boundary"):
|
|
scope_switch(A, WARPGROUP)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.1 -- Pure narrowing CTA -> WG -> W
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_1_cta_wg_warp():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16)
|
|
# with T.cta()
|
|
ctx = ctx.with_scope_switch(CTA)
|
|
assert ctx.inter == {"cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "warpid": AxisRange(16, 0)}
|
|
# with T.warpgroup()
|
|
ctx = ctx.with_scope_switch(WARPGROUP)
|
|
assert ctx.inter == {"wgid": AxisRange(4, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(4, 0)}
|
|
# with T.warp()
|
|
ctx = ctx.with_scope_switch(WARP)
|
|
assert ctx.inter == {"warpid": AxisRange(16, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.2 -- Filter + scope_switch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_2_filter_then_warpgroup():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
ctx = ctx.with_filter(WARP_FLAT, 0, 8)
|
|
assert ctx.A.warpid == AxisRange(8, 0)
|
|
# recompute at cta: intra=(lane:32, warp:8)
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "warpid": AxisRange(8, 0)}
|
|
# enter warpgroup: factor(8, 0) -> case 1
|
|
ctx = ctx.with_scope_switch(WARPGROUP)
|
|
assert ctx.inter == {"wgid": AxisRange(2, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(4, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.3 -- Sugar form T.warp(warpid[2:4])
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_3_sugar_warp_range():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
# desugar: filter warpid[2:4], then warp
|
|
ctx = ctx.with_filter(WARP_FLAT, 2, 4).with_scope_switch(WARP)
|
|
assert ctx.A.warpid == AxisRange(2, 2)
|
|
assert ctx.inter == {"warpid": AxisRange(2, 2), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.4 -- Widen after filter (warp -> warpgroup)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_4_widen_warp_to_wg():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
ctx = ctx.with_filter(WARP_FLAT, 0, 4).with_scope_switch(WARP)
|
|
# widen to warpgroup
|
|
ctx = ctx.with_scope_switch(WARPGROUP)
|
|
assert ctx.inter == {"wgid": AxisRange(1, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(4, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.5 -- Partial warp selection -> warpgroup (partial intra)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_5_partial_wg():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
ctx = ctx.with_filter(WARP_FLAT, 0, 2).with_scope_switch(WARPGROUP)
|
|
# case 2: 2 <= 4-0
|
|
assert ctx.inter == {"wgid": AxisRange(1, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(2, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.6 -- Cross warpgroup boundary (factor fails)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_6_factor_fail():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
# with_filter recomputes (inter, intra) for current scope_kind=cta -- still OK
|
|
ctx2 = ctx.with_filter(WARP_FLAT, 2, 6)
|
|
assert ctx2.A.warpid == AxisRange(4, 2)
|
|
# scope_switch to warpgroup is the one that must fail
|
|
with pytest.raises(ExecContextError, match="crosses warpgroup boundary"):
|
|
ctx2.with_scope_switch(WARPGROUP)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.7 -- Deep mixed nesting
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_7_deep_nested():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
ctx = ctx.with_filter(WARP_FLAT, 0, 8).with_scope_switch(WARPGROUP)
|
|
assert ctx.inter == {"wgid": AxisRange(2, 0), "cta_id": AxisRange(1, 0)}
|
|
ctx = ctx.with_filter(WARP_FLAT, 0, 2)
|
|
# recompute at warpgroup: factor(2, 0) -> case 2
|
|
assert ctx.inter == {"wgid": AxisRange(1, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(2, 0)}
|
|
ctx = ctx.with_scope_switch(WARP)
|
|
assert ctx.inter == {"warpid": AxisRange(2, 0), "cta_id": AxisRange(1, 0)}
|
|
assert ctx.intra == {"laneid": AxisRange(32, 0)}
|
|
ctx = ctx.with_filter(LANE_BIND, 0, 8)
|
|
assert ctx.intra == {"laneid": AxisRange(8, 0)}
|
|
assert ctx.inter == {"warpid": AxisRange(2, 0), "cta_id": AxisRange(1, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.8 -- FA4 pattern: 3 sibling filter branches
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_8_fa4_pattern():
|
|
root = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
|
|
# Branch 1: warp 12 (single warp, tcgen05 MMA elected)
|
|
b1 = root.with_filter(WARP_FLAT, 12, 13)
|
|
assert b1.A.warpid == AxisRange(1, 12)
|
|
assert b1.intra == {"laneid": AxisRange(32, 0), "warpid": AxisRange(1, 12)}
|
|
|
|
# Branch 2: softmax warpgroups (warps 0-7)
|
|
b2 = root.with_filter(WARP_FLAT, 0, 8).with_scope_switch(WARPGROUP)
|
|
assert b2.inter == {"wgid": AxisRange(2, 0), "cta_id": AxisRange(1, 0)}
|
|
assert b2.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(4, 0)}
|
|
|
|
# Branch 3: correction warpgroup (warps 8-11 = wg2)
|
|
b3 = root.with_filter(WARP_FLAT, 8, 12)
|
|
assert b3.A.warpid == AxisRange(4, 8)
|
|
assert b3.intra == {"laneid": AxisRange(32, 0), "warpid": AxisRange(4, 8)}
|
|
# And should factor cleanly when entering warpgroup
|
|
b3wg = b3.with_scope_switch(WARPGROUP)
|
|
assert b3wg.inter == {"wgid": AxisRange(1, 2), "cta_id": AxisRange(1, 0)}
|
|
assert b3wg.intra == {"laneid": AxisRange(32, 0), "wid_in_wg": AxisRange(4, 0)}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.9 -- Cross-CTA with widening to cluster
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ex_8_9_cross_cta_cluster():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16, cta_ext=4).with_scope_switch(CTA)
|
|
assert ctx.inter == {"cta_id": AxisRange(4, 0)}
|
|
# filter to warp 0, then warp
|
|
w = ctx.with_filter(WARP_FLAT, 0, 1).with_scope_switch(WARP)
|
|
assert w.inter == {"warpid": AxisRange(1, 0), "cta_id": AxisRange(4, 0)}
|
|
assert w.intra == {"laneid": AxisRange(32, 0)}
|
|
|
|
# back at cta scope, enter warpgroup
|
|
wg = ctx.with_scope_switch(WARPGROUP)
|
|
assert wg.inter == {"wgid": AxisRange(4, 0), "cta_id": AxisRange(4, 0)}
|
|
# widen to cluster
|
|
cl = wg.with_scope_switch(CLUSTER)
|
|
assert cl.inter == {}
|
|
assert cl.intra == {
|
|
"laneid": AxisRange(32, 0),
|
|
"warpid": AxisRange(16, 0),
|
|
"cta_id": AxisRange(4, 0),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# §8.10 -- identical to 8.3 modulo prose; covered above
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rule 1 & 5: filter can only shrink A; saved/restored across scope exit
|
|
# (Restoration is the caller's (IR walker's) responsibility -- ExecContext
|
|
# is immutable, each with_filter returns a fresh ctx. Test that the parent
|
|
# is untouched.)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_filter_is_pure():
|
|
ctx = ExecContext.at_kernel_entry(warp_ext=16).with_scope_switch(CTA)
|
|
child = ctx.with_filter(WARP_FLAT, 0, 8)
|
|
assert ctx.A.warpid == AxisRange(16, 0) # parent not mutated
|
|
assert child.A.warpid == AxisRange(8, 0)
|
|
|
|
|
|
def test_filter_empty_range_rejected():
|
|
A = initial_A(warp_ext=16)
|
|
with pytest.raises(ExecContextError, match="empty or inverted"):
|
|
filter_narrow(A, WARP_FLAT, 5, 5)
|
|
|
|
|
|
def test_filter_out_of_range_rejected():
|
|
A = initial_A(warp_ext=16)
|
|
A = filter_narrow(A, WARP_FLAT, 0, 4)
|
|
with pytest.raises(ExecContextError, match="empty range"):
|
|
filter_narrow(A, WARP_FLAT, 8, 12) # disjoint from [0, 4)
|
|
|
|
|
|
def test_filter_flat_cta_thread_full_warp_range():
|
|
A = initial_A(warp_ext=8)
|
|
A = filter_narrow(A, CTA_THREAD_BIND, 0, 128)
|
|
assert A.warpid == AxisRange(4, 0)
|
|
assert A.laneid == AxisRange(32, 0)
|
|
|
|
|
|
def test_filter_flat_cta_thread_single_warp_lane_range():
|
|
A = initial_A(warp_ext=8)
|
|
A = filter_narrow(A, CTA_THREAD_BIND, 34, 40)
|
|
assert A.warpid == AxisRange(1, 1)
|
|
assert A.laneid == AxisRange(6, 2)
|
|
|
|
|
|
def test_filter_flat_cta_thread_nonrectangular_rejected():
|
|
A = initial_A(warp_ext=8)
|
|
with pytest.raises(ExecContextError, match="non-rectangular"):
|
|
filter_narrow(A, CTA_THREAD_BIND, 20, 50)
|
|
|
|
|
|
def test_filter_flat_warpgroup_thread_range_inside_one_warpgroup():
|
|
A = initial_A(warp_ext=8)
|
|
A = filter_narrow(A, WG_OUTER, 1, 2)
|
|
A = filter_narrow(A, WG_THREAD_BIND, 32, 64)
|
|
assert A.warpid == AxisRange(1, 5)
|
|
assert A.laneid == AxisRange(32, 0)
|
|
|
|
|
|
def test_filter_flat_warpgroup_thread_full_range_across_warpgroups_is_noop():
|
|
A = initial_A(warp_ext=8)
|
|
A2 = filter_narrow(A, WG_THREAD_BIND, 0, 128)
|
|
assert A2.warpid == AxisRange(8, 0)
|
|
assert A2.laneid == AxisRange(32, 0)
|
|
|
|
|
|
def test_filter_flat_warpgroup_thread_partial_range_across_warpgroups_rejected():
|
|
A = initial_A(warp_ext=8)
|
|
with pytest.raises(ExecContextError, match="multiple warpgroups"):
|
|
filter_narrow(A, WG_THREAD_BIND, 0, 64)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Factor-lane bindings: wg_outer and w_inner
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_filter_wg_outer():
|
|
A = initial_A(warp_ext=16)
|
|
A2 = filter_narrow(A, WG_OUTER, 1, 3) # wg 1..2 -> warps 4..11
|
|
assert A2.warpid == AxisRange(8, 4)
|
|
|
|
|
|
def test_filter_wg_outer_unaligned_rejected():
|
|
A = initial_A(warp_ext=16)
|
|
A = filter_narrow(A, WARP_FLAT, 2, 6) # warp offset 2 (not WG-aligned)
|
|
with pytest.raises(ExecContextError, match="aligned to WG_SIZE"):
|
|
filter_narrow(A, WG_OUTER, 0, 1)
|
|
|
|
|
|
def test_filter_w_inner():
|
|
A = initial_A(warp_ext=16)
|
|
# First narrow into a single warpgroup, then inner filter is valid
|
|
A = filter_narrow(A, WARP_FLAT, 4, 8) # wg1: warps 4..7
|
|
A2 = filter_narrow(A, W_INNER, 1, 3) # pick inner lanes 1..2
|
|
assert A2.warpid == AxisRange(2, 5)
|
|
|
|
|
|
def test_filter_w_inner_spanning_wg_rejected():
|
|
A = initial_A(warp_ext=16) # spans all 4 wgs
|
|
with pytest.raises(ExecContextError, match="spans multiple warpgroups"):
|
|
filter_narrow(A, W_INNER, 0, 2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main([__file__, "-v"]))
|