Files
apache--tvm/python/tvm/s_tir/dlight/analysis/common_analysis.py
T
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

461 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.
# pylint: disable=missing-function-docstring, missing-class-docstring
# pylint: disable=unused-argument, unused-variable
"""Analysis on TIR blocks, loops and functions."""
import logging
from collections import namedtuple
from typing import Literal
from tvm_ffi import get_global_func
from tvm import ir, s_tir, tirx
from tvm.s_tir import Schedule
from tvm.s_tir.schedule import SBlockRV
from tvm.target.target import Target
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class IterInfo:
"""Information about a loop/iter var."""
kind: Literal["S", "R", "O"]
var: tirx.Var
_dom: tirx.Expr
loop_rv: s_tir.schedule.LoopRV
def __init__(
self,
kind: Literal["S", "R", "O"],
var: tirx.Var,
dom: tirx.Expr,
loop_rv: s_tir.schedule.LoopRV,
):
"""Construct an IterInfo object."""
self.kind = kind
self.var = var
self._dom = dom
self.loop_rv = loop_rv
@property
def dom(self) -> int | tirx.Expr:
"""The iteration domain of the loop."""
return int(self._dom) if isinstance(self._dom, tirx.IntImm) else self._dom
def __str__(self) -> str:
return f'Iter("{self.kind}", {self.dom})'
def __repr__(self) -> str:
return str(self)
get_sblockrealize = get_global_func("s_tir.schedule.GetSBlockRealize")
# BufferIndex Types
Index = namedtuple("Index", ["sub"]) # c
RemIndex = namedtuple("RemIndex", ["sub", "div"]) # c%len
DivIndex = namedtuple("DivIndex", ["sub", "div"]) # c//len
MergeIndex = namedtuple("MulIndex", ["dom", "mul", "sub"]) # co*len + cb
BufIndex = list[Index | RemIndex | DivIndex | MergeIndex | None]
class BufferInfo:
"Information about Buffer. Provides useful analysis"
buf_region: tirx.BufferRegion
shape: tuple[int]
assoc_lps: list[s_tir.schedule.LoopRV | None]
assoc_lps_info: list[tirx.For | None]
def __init__(
self,
sch: s_tir.Schedule,
block_rv: s_tir.schedule.SBlockRV,
buf_region: tirx.BufferRegion,
lps: list[s_tir.schedule.LoopRV] | None,
):
block = sch.get(block_rv)
if lps is None:
lps = sch.get_loops(block_rv)
loops = [sch.get(lp) for lp in lps]
iter_vars = [Var.var for Var in block.iter_vars]
iter_values = get_sblockrealize(sch, block_rv).iter_values
lpvar_lp = dict([loop.loop_var, lp] for loop, lp in zip(loops, lps))
var_lp = dict(zip(iter_vars, [lpvar_lp.get(val, None) for val in iter_values]))
def extract_index_types(buf: tirx.BufferRegion) -> BufIndex:
buf_index = []
for expr in buf.region:
expr = expr.min
dim = None
if isinstance(expr, tirx.expr.Add) and isinstance(expr.b, tirx.expr.Var):
var_add = expr.b
if (
isinstance(expr, tirx.expr.Mul)
and isinstance(expr.a, tirx.expr.Var)
and isinstance(expr.b, tirx.expr.IntImm)
):
mul = expr.b
var_mul = expr.a
dim = MergeIndex(var_mul, mul, var_add)
elif (
isinstance(expr, tirx.expr.FloorMod)
and isinstance(expr.a, tirx.expr.Var)
and isinstance(expr.b, tirx.expr.IntImm)
):
dim = RemIndex(expr.a, expr.b)
elif (
isinstance(expr, tirx.expr.FloorDiv)
and isinstance(expr.a, tirx.expr.Var)
and isinstance(expr.b, tirx.expr.IntImm)
):
dim = DivIndex(expr.a, expr.b)
elif isinstance(expr, tirx.expr.Var):
dim = Index(expr)
buf_index.append(dim)
return buf_index
indexes = extract_index_types(buf_region)
assoc_lps = [
(
var_lp.get(getattr(idx, "sub"), None)
if not isinstance(idx, DivIndex) and idx is not None
else None
)
for idx in indexes
]
self.buf_region = buf_region
self.assoc_lps = assoc_lps
self.assoc_lps_info = [(sch.get(lp) if lp is not None else None) for lp in assoc_lps]
self.shape = buf_region.buffer.shape
def get_scope(self) -> str:
return self.buf_region.buffer.scope()
def get_vecsize(self, buf_index: int = 0, vbits: int = 128):
if self.assoc_lps_info[-1] is None:
return None
vlp_extent = int(self.assoc_lps_info[-1].extent) & ~(
int(self.assoc_lps_info[-1].extent) - 1
)
vbuf_extent = int(self.shape[-1]) & ~(int(self.shape[-1]) - 1)
return min(vlp_extent, vbuf_extent, vbits // self.buf_region.buffer.dtype.dtype.bits)
def __str__(self) -> str:
return f"BufferInfo({self.buf_region})"
def __repr__(self) -> str:
return str(self)
class SBlockInfo:
"""Information about a TIR block."""
name: str
iters: list[IterInfo]
block_rv: s_tir.schedule.SBlockRV
_reduction_block: bool
def __init__(
self,
name: str,
iters: list[IterInfo],
block_rv: s_tir.schedule.SBlockRV,
reduction_block: bool = False,
):
"""Construct a SBlockInfo object."""
self.name = name
self.block_rv = block_rv
self.iters = iters
self._reduction_block = reduction_block
def dom(self) -> list[int | tirx.Expr]:
"""The iteration domain of the block."""
return [i.dom for i in self.iters]
def read_bufs(self, sch: s_tir.Schedule) -> list[BufferInfo]:
block_stmt = sch.get(self.block_rv)
lps = sch.get_loops(self.block_rv)
return [BufferInfo(sch, self.block_rv, buf, lps) for buf in block_stmt.reads]
def write_bufs(self, sch: s_tir.Schedule) -> list[BufferInfo]:
block_stmt = sch.get(self.block_rv)
lps = sch.get_loops(self.block_rv)
return [BufferInfo(sch, self.block_rv, buf, lps) for buf in block_stmt.writes]
def dom_kind(self) -> str:
"""The iteration domain kind of the block, for example, SSSS, SSSR."""
return "".join(i.kind for i in self.iters)
def is_injective(self) -> bool:
"""Whether the SBlock is injective, i.e. all its iteration domains are injective."""
return all(k == "S" for k in self.dom_kind())
def is_elementwise(self, sch: s_tir.Schedule) -> bool:
"""Whether the SBlock is elementwise, i.e. trivial mapping between read/write region"""
def _check_unit_var_range(dom: ir.Range, var: tirx.Var) -> bool:
return dom.min.same_as(var) and dom.extent == 1
if not self.is_injective():
return False
block = sch.get(self.block_rv)
if len(block.reads) != 1 or len(block.writes) != 1:
return False
r_region = block.reads[0].region
w_region = block.writes[0].region
if len(r_region) != len(w_region):
return False
for var, r_dom, w_dom in zip(block.iter_vars, r_region, w_region):
if not _check_unit_var_range(r_dom, var) or not _check_unit_var_range(w_dom, var):
return False
return True
def get_loops(self) -> list[s_tir.schedule.LoopRV]:
return [iter_info.loop_rv for iter_info in self.iters]
def is_reduction(self) -> bool:
"""Whether the SBlock is a reduction workload."""
# TODO(@junrushao): distinguish GEMV and reduction
return self._reduction_block
def is_layout_transform(self, sch: s_tir.Schedule) -> bool:
"""Whether the SBlock can be considered having a Layout Transform Pattern"""
return (
all(k == "S" for k in self.dom_kind())
and len(self.write_bufs(sch)) == 1
and len(self.read_bufs(sch)) == 1
and not self.is_elementwise(sch)
and not get_global_func("s_tir.schedule.HasIfThenElse")(sch.get(self.block_rv))
)
def is_data_pad(self, sch: s_tir.Schedule) -> bool:
"""Whether the SBlock can be considered having a data pad pattern"""
return (
all(k == "S" for k in self.dom_kind())
and len(self.write_bufs(sch)) == 1
and len(self.read_bufs(sch)) == 1
and not self.is_elementwise(sch)
and len(self.write_bufs(sch)[0].buf_region.region)
== len(self.read_bufs(sch)[0].buf_region.region)
and get_global_func("s_tir.schedule.HasIfThenElse")(sch.get(self.block_rv))
)
def is_convolution(self) -> bool:
"""Whether a SBlock can be considered having Convolution Pattern"""
raise NotImplementedError
def is_pool(self) -> bool:
"""Whether a SBlock can be considered having Pooling Pattern"""
raise NotImplementedError
def is_gemv(self) -> bool:
"""Whether the SBlock is a GEMV workload."""
raise NotImplementedError
def is_gemm(self) -> bool:
"""Whether the SBlock is a GEMM workload."""
raise NotImplementedError
def __str__(self) -> str:
return f'SBlockInfo("{self.name}", "{self.dom_kind()}", {self.dom()})'
def __repr__(self) -> str:
return str(self)
_normalize_prim_func = get_global_func("s_tir.schedule.NormalizePrimFunc")
def normalize_prim_func(sch: s_tir.Schedule) -> list[SBlockInfo] | None:
"""Normalize the primfunc to normal form"""
try:
result = _normalize_prim_func(sch)
if result is None:
return None
except Exception: # pylint: disable=broad-except
return None
def _iter_kind(i: tirx.IterVar) -> str:
return {
tirx.IterVar.DataPar: "S",
tirx.IterVar.CommReduce: "R",
}.get(i.iter_type, "O")
blocks: list[SBlockInfo] = []
for block, loops, iters, is_reduction in zip(*result):
blocks.append(
SBlockInfo(
name=sch.get(block).name_hint,
iters=[
IterInfo(
kind=_iter_kind(iter), # type: ignore
var=iter.var,
dom=iter.dom.extent,
loop_rv=loop,
)
for loop, iter in zip(loops, iters)
],
block_rv=block,
reduction_block=is_reduction,
)
)
return blocks
def get_sblock_info(sch: s_tir.Schedule, block: s_tir.schedule.SBlockRV) -> SBlockInfo:
def _iter_kind(loop: tirx.IterVar) -> str:
return {tirx.IterVar.DataPar: "S", tirx.IterVar.CommReduce: "R"}.get(loop.iter_type, "O")
def _is_reduction_block(block: s_tir.schedule.SBlockRV):
for iter_var in sch.get(block).iter_vars:
if _iter_kind(iter_var) == "R":
return True
return False
return SBlockInfo(
name=sch.get(block).name_hint,
iters=[
IterInfo(
kind=_iter_kind(iter_var),
var=iter_var.var,
dom=iter_var.dom.extent,
loop_rv=loop_rv,
)
for loop_rv, iter_var in zip(sch.get_loops(block), sch.get(block).iter_vars)
],
block_rv=block,
reduction_block=_is_reduction_block(block),
)
def _assert_gpu_target(target: Target):
if "gpu" not in target.keys:
raise ValueError(f"Expect a GPU target, but got {target}")
def get_max_threads_per_block(target: Target) -> int:
_assert_gpu_target(target)
max_threads_per_block = None
for name in ["max_threads_per_block", "max_num_threads"]:
if max_threads_per_block is None:
max_threads_per_block = target.attrs.get(name, None)
if max_threads_per_block is None:
max_threads_per_block = 64
return int(max_threads_per_block)
TARGET_KIND_TO_DEFAULT_MAX_SMEM = {
"cuda": 49152,
"rocm": 65536,
"metal": 32768,
"opencl": 16384,
"vulkan": 16384,
}
def get_max_shared_memory_per_block(target: Target) -> int:
_assert_gpu_target(target)
max_shared_memory_per_block = target.attrs.get("max_shared_memory_per_block", None)
if max_shared_memory_per_block is not None:
return int(max_shared_memory_per_block)
# Layered fallback strategy for targets that do not carry this attribute
# 1) Use explicit target attrs provided (handled above).
# 2) Fall back to backend defaults matching target-kind defaults/tag defaults.
# 3) Use a conservative GPU default as last resort.
default_smem = TARGET_KIND_TO_DEFAULT_MAX_SMEM.get(target.kind.name, 16384)
logger.warning(
"Target %s missing 'max_shared_memory_per_block'; using %d bytes.",
target.kind.name,
default_smem,
)
return int(default_smem)
def get_root_block(sch: Schedule, func_name: str = "main") -> SBlockRV:
try:
block = sch.mod[func_name].body.block
except Exception:
raise ValueError(
f"The function body is expected to be the root block, but got:\n"
f"{sch.mod[func_name].body}"
)
return sch.get_sblock(block.name_hint)
def collect_block_iter_vars_used_in_access_region(
block: tirx.SBlock, region: list[ir.Range]
) -> set[tirx.Var]:
"""Collect the block iter variables used in the access region of a buffer region."""
tir_vars = set()
for expr in region:
assert expr.extent == 1
tir_vars |= collect_vars_used_in_prim_expr(expr.min)
tir_vars &= set(iter_var.var for iter_var in block.iter_vars)
return tir_vars
def collect_vars_used_in_prim_expr(expr: tirx.Expr) -> set[tirx.Var]:
"""Collect the variables used in the Expr."""
tir_vars = set()
def _collect_tir_var(expr):
if isinstance(expr, tirx.Var):
tir_vars.add(expr)
tirx.stmt_functor.post_order_visit(expr, _collect_tir_var)
return tir_vars
def detect_dominant_read(block: tirx.SBlock) -> tirx.Expr:
"""Detect the dominant read indices in the block."""
dominant_read = None
num_read_iters = -1
for buffer_region in block.reads:
tir_vars = collect_block_iter_vars_used_in_access_region(block, buffer_region.region)
if num_read_iters < len(tir_vars):
num_read_iters = len(tir_vars)
dominant_read = buffer_region
assert dominant_read is not None
(result,) = dominant_read.buffer.offset_of([e.min for e in dominant_read.region])
return result
def is_broadcast_epilogue(
sch: s_tir.Schedule,
block: s_tir.schedule.SBlockRV,
epilogue: s_tir.schedule.SBlockRV,
) -> bool:
"""Check if the epilogue block is a broadcast pattern"""
write_buffers = {r.buffer for r in sch.get(block).writes}
epilogue_iters = {i.var: i for i in sch.get(epilogue).iter_vars if i.dom != 1}
for buffer_region in sch.get(epilogue).reads:
if buffer_region.buffer not in write_buffers:
continue
tir_vars = collect_block_iter_vars_used_in_access_region(
sch.get(epilogue), buffer_region.region
)
if len(tir_vars) < len(epilogue_iters):
return True
return False