Files
apache--tvm/python/tvm/tirx/exec_context.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Waiting to run
CI / MacOS (push) Waiting to run
CI / Windows (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

409 lines
14 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.
"""ExecContext: per-program-point active-thread state.
The active thread set is represented as a ``TileLayout``: active axes live in
``layout.shard`` and per-axis lower bounds live in ``layout.offset``. Filters
narrow that layout; scope switches derive the current ``inter``/``intra`` view.
"""
from __future__ import annotations
from dataclasses import dataclass
from tvm.tirx.layout import Axis, Iter, TileLayout
WG_SIZE = 4
KERNEL = "kernel"
CLUSTER = "cluster"
CTA = "cta"
WARPGROUP = "warpgroup"
WARP = "warp"
THREAD = "thread"
SCOPE_KINDS = (KERNEL, CLUSTER, CTA, WARPGROUP, WARP, THREAD)
LANE_FLAT = "flat"
LANE_WG_OUTER = "wg_outer"
LANE_W_INNER = "w_inner"
LANE_CTA_THREAD = "cta_thread"
LANE_WG_THREAD = "wg_thread"
class ExecContextError(Exception):
"""Raised on structural violations of the ExecContext model."""
def _ceildiv(lhs: int, rhs: int) -> int:
return -((-lhs) // rhs)
def _gcd(lhs: int, rhs: int) -> int:
while rhs:
lhs, rhs = rhs, lhs % rhs
return abs(lhs)
def _extended_gcd(lhs: int, rhs: int) -> tuple[int, int, int]:
if rhs == 0:
return lhs, 1, 0
gcd, x1, y1 = _extended_gcd(rhs, lhs % rhs)
return gcd, y1, x1 - (lhs // rhs) * y1
def _mod_inverse(value: int, modulus: int) -> int:
if modulus == 1:
return 0
gcd, inv, _ = _extended_gcd(value % modulus, modulus)
if gcd != 1:
raise ExecContextError(f"{value} has no inverse modulo {modulus}")
return inv % modulus
@dataclass(frozen=True)
class AxisRange:
"""An active slice offset + stride * [0, extent) on one TileLayout axis."""
extent: int
offset: int = 0
stride: int = 1
def intersect(self, lo: int, hi: int) -> AxisRange:
i_lo = max(0, _ceildiv(lo - self.offset, self.stride))
i_hi = min(self.extent, (hi - 1 - self.offset) // self.stride + 1)
if i_hi <= i_lo:
raise ExecContextError(
f"filter produces empty range: current=[{self.offset},"
f" {self.offset + self.extent}) ∩ [{lo}, {hi})"
)
return AxisRange(
extent=i_hi - i_lo, offset=self.offset + self.stride * i_lo, stride=self.stride
)
def modulo(self, modulus: int, residue: int) -> AxisRange:
residue %= modulus
rhs = (residue - self.offset) % modulus
g = _gcd(self.stride, modulus)
if rhs % g != 0:
raise ExecContextError(
f"modulo filter produces empty range: {self.offset} + {self.stride} * i"
f" == {residue} mod {modulus}"
)
reduced_stride = self.stride // g
reduced_rhs = rhs // g
reduced_modulus = modulus // g
period = reduced_modulus
i0 = (reduced_rhs * _mod_inverse(reduced_stride, reduced_modulus)) % reduced_modulus
if i0 >= self.extent:
raise ExecContextError(
f"modulo filter produces empty range: {self.offset} + {self.stride} * i"
f" == {residue} mod {modulus}"
)
return AxisRange(
extent=(self.extent - 1 - i0) // period + 1,
offset=self.offset + self.stride * i0,
stride=self.stride * period,
)
@dataclass(frozen=True)
class ActiveSet:
"""Active thread set represented by a TileLayout."""
layout: TileLayout
@staticmethod
def from_axes(axes: list[tuple[str, AxisRange]]) -> ActiveSet:
shard = [Iter(axis_range.extent, axis_range.stride, name) for name, axis_range in axes]
offset = {
Axis.get(name): axis_range.offset for name, axis_range in axes if axis_range.offset != 0
}
return ActiveSet(TileLayout.from_iters(shard, [], offset))
@property
def size(self) -> int:
result = 1
for it in self.layout.shard:
result *= int(it.extent)
return result
@property
def axis_names(self) -> list[str]:
return [str(it.axis.name) for it in self.layout.shard]
def axis(self, name: str) -> AxisRange:
for it in self.layout.shard:
if str(it.axis.name) != name:
continue
offset = 0
for axis, value in self.layout.offset.items():
if str(axis.name) == name:
offset = int(value)
break
return AxisRange(int(it.extent), offset, int(it.stride))
raise ValueError(f"unknown active-set axis: {name!r}")
def replace_axis(self, axis: str, axis_range: AxisRange) -> ActiveSet:
axes: list[tuple[str, AxisRange]] = []
found = False
for name in self.axis_names:
if name == axis:
axes.append((name, axis_range))
found = True
else:
axes.append((name, self.axis(name)))
if not found:
raise ValueError(f"unknown active-set axis: {axis!r}")
return ActiveSet.from_axes(axes)
@property
def laneid(self) -> AxisRange:
return self.axis("laneid")
@property
def warpid(self) -> AxisRange:
return self.axis("warpid")
@property
def cta_id(self) -> AxisRange:
return self.axis("cta_id")
@dataclass(frozen=True)
class LaneBinding:
"""Resolution of a user-declared ScopeIdDef Var to one active-set axis."""
axis: str
kind: str
declared_extent: int
def initial_A(*, lane_ext: int = 32, warp_ext: int, cta_ext: int = 1) -> ActiveSet:
"""Build A at PrimFunc device entry: all threads active, offsets all zero."""
return ActiveSet.from_axes(
[
("laneid", AxisRange(lane_ext, 0)),
("warpid", AxisRange(warp_ext, 0)),
("cta_id", AxisRange(cta_ext, 0)),
]
)
def filter_narrow(A: ActiveSet, binding: LaneBinding, lo: int, hi: int) -> ActiveSet:
"""Intersect A's binding axis with [lo, hi)."""
if lo >= hi:
raise ExecContextError(f"filter range [{lo}, {hi}) is empty or inverted")
if binding.kind == LANE_CTA_THREAD:
new_warpid, new_laneid = _flat_product_range(A.warpid, A.laneid, lo, hi)
return A.replace_axis("laneid", new_laneid).replace_axis("warpid", new_warpid)
if binding.kind == LANE_WG_THREAD:
factored = _factor_warpid(A.warpid)
if factored is None:
raise ExecContextError(
"filter on flat warpgroup-thread range requires factorable warpid axis"
)
wid_in_wg, wgid = factored
new_wid_in_wg, new_laneid = _flat_product_range(wid_in_wg, A.laneid, lo, hi)
if wgid.extent != 1:
if new_wid_in_wg == wid_in_wg and new_laneid == A.laneid:
return A
raise ExecContextError(
"flat warpgroup-thread range across multiple warpgroups is not representable"
)
new_warpid = AxisRange(
extent=new_wid_in_wg.extent, offset=wgid.offset * WG_SIZE + new_wid_in_wg.offset
)
return A.replace_axis("laneid", new_laneid).replace_axis("warpid", new_warpid)
if binding.kind == LANE_FLAT:
new_axis = A.axis(binding.axis).intersect(lo, hi)
return A.replace_axis(binding.axis, new_axis)
if binding.axis != "warpid":
raise ExecContextError(
f"kind={binding.kind!r} only valid for axis='warpid'; got {binding.axis!r}"
)
wp = A.warpid
if wp.stride != 1:
raise ExecContextError(
f"kind={binding.kind!r} requires unit-stride warpid axis; got stride={wp.stride}"
)
if binding.kind == LANE_WG_OUTER:
if wp.offset % WG_SIZE != 0 or wp.extent % WG_SIZE != 0:
raise ExecContextError(
f"filter on wg_outer requires warpid axis aligned to WG_SIZE={WG_SIZE};"
f" got extent={wp.extent}, offset={wp.offset}"
)
cur_outer = AxisRange(extent=wp.extent // WG_SIZE, offset=wp.offset // WG_SIZE)
new_outer = cur_outer.intersect(lo, hi)
return A.replace_axis(
"warpid",
AxisRange(extent=new_outer.extent * WG_SIZE, offset=new_outer.offset * WG_SIZE),
)
if binding.kind == LANE_W_INNER:
cur_inner_off = wp.offset % WG_SIZE
if wp.extent > WG_SIZE - cur_inner_off:
raise ExecContextError(
"filter on w_inner would break A's TileLayout box: warpid spans multiple"
f" warpgroups (extent={wp.extent}, offset={wp.offset})"
)
cur_inner = AxisRange(extent=wp.extent, offset=cur_inner_off)
new_inner = cur_inner.intersect(lo, hi)
outer_base = (wp.offset // WG_SIZE) * WG_SIZE
return A.replace_axis(
"warpid", AxisRange(extent=new_inner.extent, offset=outer_base + new_inner.offset)
)
raise ValueError(f"unknown axis kind: {binding.kind!r}")
def filter_modulo(A: ActiveSet, axis: str, modulus: int, residue: int) -> ActiveSet:
"""Intersect an active-set axis with ``axis % modulus == residue``."""
if modulus <= 0:
raise ExecContextError(f"modulus must be positive, got {modulus}")
new_axis = A.axis(axis).modulo(modulus, residue)
return A.replace_axis(axis, new_axis)
@dataclass(frozen=True)
class Split:
"""A scope_switch split of A."""
inter: dict[str, AxisRange]
intra: dict[str, AxisRange]
def _factor_warpid(warp: AxisRange) -> tuple[AxisRange, AxisRange] | None:
if warp.stride != 1:
return None
off = warp.offset
ext = warp.extent
wid_off = off % WG_SIZE
wgid_off = off // WG_SIZE
if wid_off == 0 and ext % WG_SIZE == 0:
return (
AxisRange(extent=WG_SIZE, offset=0),
AxisRange(extent=ext // WG_SIZE, offset=wgid_off),
)
if ext <= WG_SIZE - wid_off:
return (AxisRange(extent=ext, offset=wid_off), AxisRange(extent=1, offset=wgid_off))
return None
def _flat_product_range(
major: AxisRange, lane: AxisRange, lo: int, hi: int
) -> tuple[AxisRange, AxisRange]:
active_min = major.offset * 32 + lane.offset
active_max = (
(major.offset + major.stride * (major.extent - 1)) * 32
+ lane.offset
+ lane.stride * (lane.extent - 1)
+ 1
)
if lo <= active_min and active_max <= hi:
return major, lane
if major.stride != 1 or lane.stride != 1:
raise ExecContextError("flat thread range narrowing requires unit-stride axes")
lane_hi = lane.offset + lane.extent
major_hi = major.offset + major.extent
hit_lo = max(major.offset, (lo - lane_hi) // 32 + 1)
hit_hi = min(major_hi, _ceildiv(hi - lane.offset, 32))
if hit_hi <= hit_lo:
raise ExecContextError("flat thread range produces empty active set")
if hit_hi == hit_lo + 1:
new_lane_lo = max(lane.offset, lo - hit_lo * 32)
new_lane_hi = min(lane_hi, hi - hit_lo * 32)
if new_lane_hi <= new_lane_lo:
raise ExecContextError("flat thread range produces empty lane range")
return AxisRange(1, hit_lo), AxisRange(new_lane_hi - new_lane_lo, new_lane_lo)
if lo <= hit_lo * 32 + lane.offset and (hit_hi - 1) * 32 + lane_hi <= hi:
return AxisRange(hit_hi - hit_lo, hit_lo), lane
raise ExecContextError("flat thread range would require a non-rectangular lane/warp active set")
def scope_switch(A: ActiveSet, scope_kind: str) -> Split:
"""Split A into (inter, intra) for the target scope kind."""
if scope_kind == THREAD:
return Split(inter={"laneid": A.laneid, "warpid": A.warpid, "cta_id": A.cta_id}, intra={})
if scope_kind == WARP:
return Split(inter={"warpid": A.warpid, "cta_id": A.cta_id}, intra={"laneid": A.laneid})
if scope_kind == CTA:
return Split(inter={"cta_id": A.cta_id}, intra={"laneid": A.laneid, "warpid": A.warpid})
if scope_kind == CLUSTER:
return Split(inter={}, intra={"laneid": A.laneid, "warpid": A.warpid, "cta_id": A.cta_id})
if scope_kind == WARPGROUP:
factored = _factor_warpid(A.warpid)
if factored is None:
raise ExecContextError(
"scope_switch(warpgroup) failed: warpid axis"
f" (extent={A.warpid.extent}, offset={A.warpid.offset})"
" crosses warpgroup boundary and is not aligned"
)
wid_in_wg, wgid = factored
return Split(
inter={"wgid": wgid, "cta_id": A.cta_id},
intra={"laneid": A.laneid, "wid_in_wg": wid_in_wg},
)
if scope_kind == KERNEL:
return Split(inter={"laneid": A.laneid, "warpid": A.warpid, "cta_id": A.cta_id}, intra={})
raise ValueError(f"unknown scope kind: {scope_kind!r}")
@dataclass(frozen=True)
class ExecContext:
"""Per-program-point compiler state: active set + scope kind + split."""
A: ActiveSet
scope_kind: str
inter: dict[str, AxisRange]
intra: dict[str, AxisRange]
@staticmethod
def at_kernel_entry(*, lane_ext: int = 32, warp_ext: int, cta_ext: int = 1) -> ExecContext:
A = initial_A(lane_ext=lane_ext, warp_ext=warp_ext, cta_ext=cta_ext)
split = scope_switch(A, KERNEL)
return ExecContext(A=A, scope_kind=KERNEL, inter=split.inter, intra=split.intra)
def with_filter(self, binding: LaneBinding, lo: int, hi: int) -> ExecContext:
new_A = filter_narrow(self.A, binding, lo, hi)
split = scope_switch(new_A, self.scope_kind)
return ExecContext(
A=new_A, scope_kind=self.scope_kind, inter=split.inter, intra=split.intra
)
def with_cta_axis_modulo(self, axis: str, modulus: int, residue: int) -> ExecContext:
new_A = filter_modulo(self.A, axis, modulus, residue)
split = scope_switch(new_A, self.scope_kind)
return ExecContext(
A=new_A, scope_kind=self.scope_kind, inter=split.inter, intra=split.intra
)
def with_scope_switch(self, scope_kind: str) -> ExecContext:
split = scope_switch(self.A, scope_kind)
return ExecContext(A=self.A, scope_kind=scope_kind, inter=split.inter, intra=split.intra)