409 lines
14 KiB
Python
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)
|