Files
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

1244 lines
45 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-docstring, invalid-name
# ruff: noqa: E722
"""A GEMM schedule rule for GPU operators."""
from dataclasses import dataclass
from enum import Enum
from tvm import s_tir, tirx
from tvm.ir import Range
from tvm.s_tir.schedule.schedule import SBlockRV
from tvm.script import tirx as T
from tvm.target import Target
from tvm.tirx import Expr, IterVar, Var
from tvm.tirx.analysis import undefined_vars
from ..analysis import IterInfo, SBlockInfo, get_root_block
from .base import GPUScheduleRule
def _collect_producers(sch: s_tir.Schedule, block: s_tir.schedule.SBlockRV):
result = []
for producer in sch.get_producers(block):
result.append(producer)
result.extend(_collect_producers(sch, producer))
return result
def _collect_consumers(sch: s_tir.Schedule, block: s_tir.schedule.SBlockRV):
result = []
for consumer in sch.get_consumers(block):
result.append(consumer)
result.extend(_collect_consumers(sch, consumer))
return result
def auto_inline_producers(
sch: s_tir.Schedule,
block: s_tir.schedule.SBlockRV,
):
while True:
inlined_cnt = 0
producers = _collect_producers(sch, block)
for producer in producers:
try:
sch.compute_inline(producer)
inlined_cnt += 1
except: # pylint: disable=bare-except
continue
if inlined_cnt == 0:
return
def auto_inline_consumers(
sch: s_tir.Schedule,
block: s_tir.schedule.SBlockRV,
):
while True:
inlined_cnt = 0
consumers = _collect_consumers(sch, block)
for consumer in consumers:
try:
sch.compute_inline(consumer)
inlined_cnt += 1
except: # pylint: disable=bare-except
continue
for consumer in consumers:
try:
sch.reverse_compute_inline(consumer)
inlined_cnt += 1
except: # pylint: disable=bare-except
continue
if inlined_cnt == 0:
return
def auto_inline_consumer_chain(
sch: s_tir.Schedule,
block: s_tir.schedule.SBlockRV,
):
auto_inline_consumers(sch, block)
remaining_consumers = sch.get_consumers(block)
if len(remaining_consumers) != 0:
# Some blocks have failed to be inlined to the producer cache-write stage.
# This could be due to another producer block that has not been scheduled.
for c in remaining_consumers:
for p in sch.get_producers(c):
if sch.get(p) != sch.get(block):
auto_inline_producers(sch, p)
sch.compute_inline(p)
# Try inlining into the cache-write stage again, this time it should succeed.
auto_inline_consumers(sch, block)
msg = "There are some consumers of the cache-write stage that are not properly inlined."
assert len(sch.get_consumers(block)) == 0, msg
class IterKind(Enum):
"""Iter kinds for GEMM-liked programs.
We can simplify the computation to C[S, I, J] += A[S, I, K] * B[S, J, K],
where `I, J, K` are fundamental axes for gemm and `S` represents all
other spatial axes (e.g. batches)
kIter_S: spatial axes
kIter_I: I axes
kIter_J: J axes
kIter_K: K axes
kIter_T: trivial axes (i.e. with extent 1)
"""
kIter_S = 0
kIter_I = 1
kIter_J = 2
kIter_K = 3
kIter_T = 4
@dataclass
class IterTrait:
kind: IterKind
extent: Expr
def _is_one(x: Expr) -> bool:
return isinstance(x, tirx.IntImm) and x.value == 1
def make_iter_fusion_index_map(
traits: list[IterTrait],
kind_order: list[IterKind],
) -> tirx.IndexMap:
fused_iters: dict[IterKind, Expr] = {}
input_iters: list[tirx.Var] = []
for i, trait in enumerate(traits):
v_i = tirx.Var(f"i{i}", trait.extent.ty)
input_iters.append(v_i)
if trait.kind == IterKind.kIter_T:
continue
if trait.kind not in kind_order:
raise ValueError(f"Unknown iter kind {trait.kind}")
if trait.kind in fused_iters:
fused_iters[trait.kind] = fused_iters[trait.kind] * trait.extent + v_i
else:
fused_iters[trait.kind] = v_i
final_indices: list[tirx.Expr] = [
fused_iters.get(kind, tirx.IntImm(traits[0].extent.ty, 0)) for kind in kind_order
]
return tirx.IndexMap(input_iters, final_indices, None)
def detect_iter_traits(block: tirx.SBlock) -> tuple[list[IterTrait]] | None:
"""Detect iter traits based on the pattern C[S, I, J] += A[S, I, K] * B[S, J, K]
Parameters
----------
block : tirx.SBlock
The block to be analyzed
Returns
-------
traits : Optional[Tuple[List[IterTrait]]]
The detected iter traits for axes in A, B and C. None if the block
does not match the pattern.
"""
if len(block.reads) != 2 or len(block.writes) != 1:
return None
def get_access_axes(region: list[Range]) -> set[Var]:
axes: set[Var] = set()
for r in region:
if not _is_one(r.extent):
raise ValueError("Expect elemwise block access")
axes = axes.union(set(undefined_vars(r.min)))
return axes
try:
A_axes = get_access_axes(block.reads[0].region)
B_axes = get_access_axes(block.reads[1].region)
C_axes = get_access_axes(block.writes[0].region)
except ValueError:
return None
traits: dict[Var, IterTrait] = {}
for iter_var in block.iter_vars:
var = iter_var.var
kind: IterKind
if _is_one(iter_var.dom.extent):
kind = IterKind.kIter_T
elif iter_var.iter_type == iter_var.DataPar:
if var in A_axes and var in B_axes and var in C_axes:
kind = IterKind.kIter_S
elif var in A_axes and var in C_axes:
kind = IterKind.kIter_I
elif var in B_axes and var in C_axes:
kind = IterKind.kIter_J
else:
return None
elif iter_var.iter_type == tirx.IterVar.CommReduce:
if var in A_axes and var in B_axes and var not in C_axes:
kind = IterKind.kIter_K
else:
return None
else:
return None
traits[var] = IterTrait(kind, iter_var.dom.extent)
# A Gemm-kernel requires have I, J and K axes
gemm_traits = {IterKind.kIter_I, IterKind.kIter_J, IterKind.kIter_K}
if {x.kind for x in traits.values()}.intersection(gemm_traits) != gemm_traits:
return None
A_traits = [traits[iter_var.var] for iter_var in block.iter_vars if iter_var.var in A_axes]
B_traits = [traits[iter_var.var] for iter_var in block.iter_vars if iter_var.var in B_axes]
C_traits = [traits[iter_var.var] for iter_var in block.iter_vars if iter_var.var in C_axes]
block_traits = [traits[i.var] for i in block.iter_vars]
return A_traits, B_traits, C_traits, block_traits
def get_index_map(block: tirx.SBlock) -> tuple[tirx.IndexMap, ...] | None:
"""Get index maps for the block
Parameters
----------
block : tirx.SBlock
The block to be analyzed
Returns
-------
index_maps : Optional[Tuple[tirx.IndexMap]]
The index maps for the block, or None if the block is not a gemm-liked kernel
"""
traits = detect_iter_traits(block)
if traits is None:
return None
A_traits, B_traits, C_traits, block_traits = traits
A_index_map = make_iter_fusion_index_map(
A_traits, [IterKind.kIter_S, IterKind.kIter_I, IterKind.kIter_K]
)
B_index_map = make_iter_fusion_index_map(
B_traits, [IterKind.kIter_S, IterKind.kIter_J, IterKind.kIter_K]
)
C_index_map = make_iter_fusion_index_map(
C_traits, [IterKind.kIter_S, IterKind.kIter_I, IterKind.kIter_J]
)
matmul_index_map = make_iter_fusion_index_map(
block_traits, [IterKind.kIter_S, IterKind.kIter_I, IterKind.kIter_J, IterKind.kIter_K]
)
return (
matmul_index_map,
A_index_map,
B_index_map,
C_index_map,
)
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 get_reduction_blocks(sch, blocks) -> bool:
# Get the main computation block
def is_reduction(block: SBlockRV) -> bool:
block_stmt = sch.get(block)
iter_types = {iter_var.iter_type for iter_var in block_stmt.iter_vars}
return iter_types == {IterVar.CommReduce, IterVar.DataPar}
def is_spatial(block: SBlockRV) -> bool:
block_stmt = sch.get(block)
iter_types = {iter_var.iter_type for iter_var in block_stmt.iter_vars}
return iter_types == {IterVar.DataPar}
# NOTE: We assume there is only one reduction block in the function
# all blocks are required to be spatial or reduction
if not all([is_reduction(block) or is_spatial(block) for block in blocks]):
return None
# There is only one reduction block
reduction_blocks = [block for block in blocks if is_reduction(block)]
if len(reduction_blocks) != 1:
return None
return reduction_blocks
def get_in_out_dtypes(block: tirx.SBlock) -> tuple[str]:
"""
Detect In/Out data types for the given block based on the analysis if read/write buffers.
"""
assert len(block.reads) > 0 and len(block.writes) > 0
in_dtype = block.reads[0].buffer.dtype
out_dtype = block.writes[0].buffer.dtype
return (in_dtype, out_dtype)
def check_sm_version(arch: str) -> int:
sm_version = arch.replace("sm_", "")
return int(sm_version) if sm_version.isdigit() else -1
class MetalMatmul(GPUScheduleRule):
"""
The schedule rule for Metal matmul computation.
"""
def apply( # pylint: disable=too-many-locals,missing-docstring
self,
func: tirx.PrimFunc,
target: Target,
_: bool,
) -> s_tir.Schedule | None:
from tvm.s_tir.tensor_intrin.metal import ( # pylint: disable=import-outside-toplevel
get_simdgroup_intrin_group,
)
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
return None
sch = s_tir.Schedule(func)
root_block = get_root_block(sch)
blocks = sch.get_child_blocks(root_block)
reduction_blocks = get_reduction_blocks(sch, blocks)
if reduction_blocks is None:
return None
# Step 0. Configs
block_size_x: int = 16
block_size_y: int = 16
block_size_k: int = 32
micro_size: int = 8
warp_size: int = 32
ty_len: int = 1
tz_len: int = 4
vector_size: int = 4
# Step 1. Normalize generic matmul to C[S, I, J] += A[S, I, K] * B[S, J, K]
# Reindex first and than analyze the index map
main_block = reduction_blocks[0]
reindex_a = sch.reindex(main_block, ("read", 0))
reindex_b = sch.reindex(main_block, ("read", 1))
reindex_c = sch.reindex(main_block, ("write", 0))
index_maps = get_index_map(sch.get(main_block))
assert index_maps is not None
matmul_index_map, a_index_map, b_index_map, c_index_map = index_maps
sch.transform_layout(reindex_a, ("write", 0), a_index_map)
sch.transform_layout(reindex_b, ("write", 0), b_index_map)
sch.transform_layout(reindex_c, ("read", 0), c_index_map)
sch.transform_block_layout(main_block, matmul_index_map)
# Step 2. Padding for dynamic shape kernels
sch.pad_einsum(
main_block,
[
1,
ty_len * block_size_x,
tz_len * block_size_y,
block_size_k,
],
)
# Step 3. Schedule matmul to use simdgroup intrinsics
batch, i, j, k = sch.get_loops(main_block)
bx, ty, i0, i1 = sch.split(i, [None, ty_len, block_size_x // micro_size, micro_size])
by, tz, j0, j1 = sch.split(j, [None, tz_len, block_size_y // micro_size, micro_size])
k0, k1, k2 = sch.split(k, [None, block_size_k // micro_size, micro_size])
sch.reorder(bx, by, ty, tz, k0, k1, i0, j0, i1, j1, k2)
sch.bind(bx, "blockIdx.x")
sch.bind(by, "blockIdx.y")
sch.bind(batch, "blockIdx.z")
sch.bind(ty, "threadIdx.y")
sch.bind(tz, "threadIdx.z")
def fetch_to_shared(block, idx):
block_read = sch.cache_read(block, idx, "shared")
sch.compute_at(block_read, k0, preserve_unit_loops=True)
fused = sch.fuse(*sch.get_loops(block_read)[-2:])
_, _tz, _ty, _tx, vec = sch.split(fused, [None, tz_len, ty_len, warp_size, vector_size])
sch.bind(_tz, "threadIdx.z")
sch.bind(_ty, "threadIdx.y")
sch.bind(_tx, "threadIdx.x")
sch.vectorize(vec)
return block_read
a_g2s = fetch_to_shared(main_block, 0)
b_g2s = fetch_to_shared(main_block, 1)
auto_inline_producers(sch, a_g2s)
auto_inline_producers(sch, b_g2s)
# create read cache to load matrix from shared memory to wmma fragments
A_simdgroup = sch.cache_read(main_block, 0, "metal.simdgroup")
B_simdgroup = sch.cache_read(main_block, 1, "metal.simdgroup")
sch.compute_at(A_simdgroup, k1)
sch.compute_at(B_simdgroup, k1)
C_simd2s = sch.cache_write(main_block, 0, "metal.simdgroup")
C_s2g = sch.cache_write(C_simd2s, 0, "shared")
sch.reverse_compute_at(C_simd2s, tz, preserve_unit_loops=True)
sch.reverse_compute_at(C_s2g, by, preserve_unit_loops=True)
intrin_group = get_simdgroup_intrin_group(
load_scope="shared",
store_scope="shared",
dtype="float16",
trans_a=False,
trans_b=True,
)
sch.transform_layout(B_simdgroup, ("write", 0), lambda s, i, j: (s, j, i))
def tensorize_block(block: s_tir.schedule.SBlockRV, intrin: str):
*_, i, j = sch.get_loops(block)
io, ii = sch.split(i, [None, micro_size])
jo, ji = sch.split(j, [None, micro_size])
sch.reorder(io, jo, ii, ji)
sch.tensorize(ii, intrin)
C_init = sch.decompose_reduction(main_block, k0)
tensorize_block(A_simdgroup, intrin_group["load_a"])
tensorize_block(B_simdgroup, intrin_group["load_b"])
tensorize_block(C_simd2s, intrin_group["store"])
tensorize_block(C_init, intrin_group["init"])
*_, i, j, k = sch.get_loops(main_block)
sch.tensorize(i, intrin_group["compute"])
auto_inline_consumer_chain(sch, C_s2g)
fused = sch.fuse(*sch.get_loops(C_s2g)[-2:])
_, _tz, _ty, _tx, vec = sch.split(fused, [None, tz_len, ty_len, warp_size, vector_size])
sch.bind(_tz, "threadIdx.z")
sch.bind(_ty, "threadIdx.y")
sch.bind(_tx, "threadIdx.x")
sch.vectorize(vec)
return sch
class MatmulTensorization(GPUScheduleRule):
"""
The schedule rule for float16 tensor core matmul computation.
func with attr 'dlight.do_not_tensorize' will not be tensorized.
"""
def apply( # pylint: disable=too-many-locals,missing-docstring
self,
func: tirx.PrimFunc,
target: Target,
_: bool,
) -> s_tir.Schedule | None:
from tvm.s_tir.tensor_intrin.cuda import ( # pylint: disable=import-outside-toplevel
get_wmma_intrin_group,
)
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
return None
sch = s_tir.Schedule(func)
root_block = get_root_block(sch)
blocks = sch.get_child_blocks(root_block)
if "dlight.do_not_tensorize" in func.attrs.keys():
return None
reduction_blocks = get_reduction_blocks(sch, blocks)
if reduction_blocks is None:
return None
# Start Schedule
# Step 0. Get schedule config.
# NOTE: we can analyze the config by the hardware spec in the future
# tensor core intrinsic size
micro_size_x = 16
micro_size_y = 16
micro_size_k = 16
warp_size = 32
vector_size = 4
i_factors, j_factors, k_factors = (
[None, 1, 4, 2],
[1, None, 4, 2],
[None, 4],
)
num_ty = i_factors[2] * j_factors[2]
x_pad_factor = i_factors[2] * i_factors[3]
y_pad_factor = j_factors[2] * j_factors[3]
k_pad_factor = k_factors[1]
# Step 1. Normalize generic matmul to C[S, I, J] += A[S, I, K] * B[S, J, K]
# Reindex first and than analyze the index map
main_block = reduction_blocks[0]
reindex_a = sch.reindex(main_block, ("read", 0))
reindex_b = sch.reindex(main_block, ("read", 1))
reindex_c = sch.reindex(main_block, ("write", 0))
index_maps = get_index_map(sch.get(main_block))
assert index_maps is not None
matmul_index_map, a_index_map, b_index_map, c_index_map = index_maps
sch.transform_layout(reindex_a, ("write", 0), a_index_map)
sch.transform_layout(reindex_b, ("write", 0), b_index_map)
sch.transform_layout(reindex_c, ("read", 0), c_index_map)
sch.transform_block_layout(main_block, matmul_index_map)
# Step 2. Padding for dynamic shape kernels
sch.pad_einsum(
main_block,
[
1,
micro_size_x * x_pad_factor,
micro_size_y * y_pad_factor,
micro_size_k * k_pad_factor,
],
)
# Step 3. Schedule matmul to use tensor core
block = main_block
batch, i, j, k = sch.get_loops(block)
# inner loops for tensor core computation
i, i_inner = sch.split(i, factors=[None, micro_size_x])
j, j_inner = sch.split(j, factors=[None, micro_size_y])
k, k_inner = sch.split(k, factors=[None, micro_size_k])
sch.reorder(i, j, k, i_inner, j_inner, k_inner)
block_inner = block
block_outer = sch.blockize(i_inner)
i0, i1, i2, i3 = sch.split(i, factors=i_factors)
j0, j1, j2, j3 = sch.split(j, factors=j_factors)
k0, k1 = sch.split(k, k_factors)
sch.annotate(k0, "software_pipeline_order", [0, 3, 1, 4, 5, 2, 6])
sch.annotate(k0, "software_pipeline_stage", [0, 0, 0, 0, 0, 1, 1])
sch.annotate(k1, "software_pipeline_order", [0, 1, 2])
sch.annotate(k1, "software_pipeline_stage", [0, 0, 1])
sch.reorder(i0, j0, i1, j1, j2, i2, k0, k1, i3, j3)
block_idx = sch.fuse(i0, j0)
block_idy = sch.fuse(i1, j1)
thread_idy = sch.fuse(j2, i2)
sch.bind(batch, "blockIdx.z")
sch.bind(block_idx, "blockIdx.x")
sch.bind(block_idy, "blockIdx.y")
sch.bind(thread_idy, "threadIdx.y")
def fetch_to_shared(block, idx, ndim):
block_read = sch.cache_read(block, idx, "shared.dyn")
sch.compute_at(block_read, k0)
fused = sch.fuse(*sch.get_loops(block_read)[-ndim:])
_, f_1, f_2, f_3 = sch.split(fused, factors=[None, num_ty, warp_size, vector_size])
sch.bind(f_2, "threadIdx.x")
sch.bind(f_1, "threadIdx.y")
sch.vectorize(f_3)
sch.storage_align(block_read, 0, axis=-2, factor=16, offset=8)
sch.annotate(block_read, "tirx.manifest_shared_memory_local_stage", 1)
sch.annotate(block_read, "double_buffer_scope", 0)
return block_read
a_g2s = fetch_to_shared(block_outer, 0, 2)
b_g2s = fetch_to_shared(block_outer, 1, 2)
auto_inline_producers(sch, a_g2s)
auto_inline_producers(sch, b_g2s)
# create read cache to load matrix from shared memory to wmma fragments
A_mat = sch.cache_read(block_outer, 0, "wmma.matrix_a")
B_mat = sch.cache_read(block_outer, 1, "wmma.matrix_b")
sch.compute_at(A_mat, k1)
sch.compute_at(B_mat, k1)
# create write cache to store matrix from wmma fragments to shared memory and global memory
accumulator_shared_to_global = sch.cache_write(block_outer, 0, "shared.dyn")
sch.storage_align(accumulator_shared_to_global, 0, -2, 16, 4)
store = sch.cache_write(block_outer, 0, "wmma.accumulator")
sch.reverse_compute_at(store, thread_idy)
sch.reverse_compute_at(accumulator_shared_to_global, thread_idy)
# split the store loop to match hardware intrinsic pattern
i, j = sch.get_loops(store)[-2:]
i0, i1 = sch.split(i, factors=[None, 16])
j0, j1 = sch.split(j, factors=[None, 16])
sch.reorder(i0, j0, i1, j1)
block_init_c = sch.decompose_reduction(block_outer, k0)
block_init_c_inner = sch.get_child_blocks(block_init_c)[0]
# Tensorization by hardware intrinsics
intrin_group = get_wmma_intrin_group(
load_scope="shared.dyn",
store_scope="shared.dyn",
in_dtype="float16",
out_dtype="float32",
trans_b=True,
)
try:
i, j = sch.get_loops(A_mat)[-2:]
i0, i1 = sch.split(i, factors=[None, 16])
j0, j1 = sch.split(j, factors=[None, 16])
sch.reorder(i0, j0, i1, j1)
sch.unroll(i0)
sch.unroll(j0)
sch.tensorize(i1, intrin_group["load_a"])
i, j = sch.get_loops(B_mat)[-2:]
i0, i1 = sch.split(i, factors=[None, 16])
j0, j1 = sch.split(j, factors=[None, 16])
sch.reorder(i0, j0, i1, j1)
sch.unroll(i0)
sch.unroll(j0)
sch.tensorize(i1, intrin_group["load_b"])
except: # pylint: disable=bare-except
return None
# Try to tensorize the init, store and compute block with f16 or f32 intrinsics
tensorize_success: bool = False
def tensorize_init_store_compute():
sch.tensorize(sch.get_loops(block_init_c_inner)[-2], intrin_group["init"])
sch.tensorize(sch.get_loops(store)[-2], intrin_group["store"])
sch.tensorize(sch.get_loops(block_inner)[-3], intrin_group["compute"])
try:
tensorize_init_store_compute()
tensorize_success = True
except: # pylint: disable=bare-except
intrin_group = get_wmma_intrin_group(
load_scope="shared.dyn",
store_scope="shared.dyn",
in_dtype="float16",
out_dtype="float16",
trans_b=True,
)
if not tensorize_success:
try:
tensorize_init_store_compute()
tensorize_success = True
except: # pylint: disable=bare-except
return None
auto_inline_consumer_chain(sch, accumulator_shared_to_global)
fused = sch.fuse(*sch.get_loops(accumulator_shared_to_global)[-2:])
_, f1, f2 = sch.split(fused, factors=[None, warp_size, vector_size])
sch.bind(f1, "threadIdx.x")
sch.vectorize(f2)
return sch if tensorize_success else None
class MatmulInt8Tensorization(GPUScheduleRule):
"""
The schedule rule for int8 tensor core matmul computation.
func with attr 'dlight.do_not_tensorize' will not be tensorized.
"""
def apply( # pylint: disable=too-many-locals,missing-docstring
self,
func: tirx.PrimFunc,
target: Target,
_: bool,
) -> s_tir.Schedule | None:
from tvm.s_tir.tensor_intrin.cuda import ( # pylint: disable=import-outside-toplevel
get_wmma_intrin_group,
)
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
return None
sch = s_tir.Schedule(func)
root_block = get_root_block(sch)
blocks = sch.get_child_blocks(root_block)
if "dlight.do_not_tensorize" in func.attrs.keys():
return None
reduction_blocks = get_reduction_blocks(sch, blocks)
if reduction_blocks is None:
return None
# Start Schedule
# Step 0. Get schedule config.
# NOTE: we can analyze the config by the hardware spec in the future
# tensor core intrinsic size
micro_size_x = 16
micro_size_y = 16
micro_size_k = 16
warp_size = 32
vector_size = 4
i_factors, j_factors, k_factors = (
[None, 1, 4, 2],
[1, None, 4, 2],
[None, 1],
)
num_ty = i_factors[2] * j_factors[2]
x_pad_factor = i_factors[2] * i_factors[3]
y_pad_factor = j_factors[2] * j_factors[3]
k_pad_factor = k_factors[1]
# Step 1. Normalize generic matmul to C[S, I, J] += A[S, I, K] * B[S, J, K]
# Reindex first and than analyze the index map
main_block = reduction_blocks[0]
reindex_a = sch.reindex(main_block, ("read", 0))
reindex_b = sch.reindex(main_block, ("read", 1))
reindex_c = sch.reindex(main_block, ("write", 0))
index_maps = get_index_map(sch.get(main_block))
assert index_maps is not None
matmul_index_map, a_index_map, b_index_map, c_index_map = index_maps
sch.transform_layout(reindex_a, ("write", 0), a_index_map)
sch.transform_layout(reindex_b, ("write", 0), b_index_map)
sch.transform_layout(reindex_c, ("read", 0), c_index_map)
sch.transform_block_layout(main_block, matmul_index_map)
# Step 2. Padding for dynamic shape kernels
sch.pad_einsum(
main_block,
[
1,
micro_size_x * x_pad_factor,
micro_size_y * y_pad_factor,
micro_size_k * k_pad_factor,
],
)
# Step 3. Schedule matmul to use tensor core
block = main_block
batch, i, j, k = sch.get_loops(block)
# inner loops for tensor core computation
i, i_inner = sch.split(i, factors=[None, micro_size_x])
j, j_inner = sch.split(j, factors=[None, micro_size_y])
k, k_inner = sch.split(k, factors=[None, micro_size_k])
sch.reorder(i, j, k, i_inner, j_inner, k_inner)
block_inner = block
block_outer = sch.blockize(i_inner)
i0, i1, i2, i3 = sch.split(i, factors=i_factors)
j0, j1, j2, j3 = sch.split(j, factors=j_factors)
k0, k1 = sch.split(k, k_factors)
sch.annotate(k0, "software_pipeline_order", [0, 3, 1, 4, 5, 2, 6])
sch.annotate(k0, "software_pipeline_stage", [0, 0, 0, 0, 0, 1, 1])
sch.annotate(k1, "software_pipeline_order", [0, 1, 2])
sch.annotate(k1, "software_pipeline_stage", [0, 0, 1])
sch.reorder(i0, j0, i1, j1, j2, i2, k0, k1, i3, j3)
block_idx = sch.fuse(i0, j0)
block_idy = sch.fuse(i1, j1)
thread_idy = sch.fuse(j2, i2)
sch.bind(batch, "blockIdx.z")
sch.bind(block_idx, "blockIdx.x")
sch.bind(block_idy, "blockIdx.y")
sch.bind(thread_idy, "threadIdx.y")
def fetch_to_shared(block, idx, ndim):
block_read = sch.cache_read(block, idx, "shared.dyn")
sch.compute_at(block_read, k0)
fused = sch.fuse(*sch.get_loops(block_read)[-ndim:])
_, f_1, f_2, f_3 = sch.split(fused, factors=[None, num_ty, warp_size, vector_size])
sch.bind(f_2, "threadIdx.x")
sch.bind(f_1, "threadIdx.y")
sch.vectorize(f_3)
sch.storage_align(block_read, 0, axis=-2, factor=32, offset=16)
sch.annotate(block_read, "tirx.manifest_shared_memory_local_stage", 1)
sch.annotate(block_read, "double_buffer_scope", 0)
return block_read
a_g2s = fetch_to_shared(block_outer, 0, 2)
b_g2s = fetch_to_shared(block_outer, 1, 2)
auto_inline_producers(sch, a_g2s)
auto_inline_producers(sch, b_g2s)
# create read cache to load matrix from shared memory to wmma fragments
A_mat = sch.cache_read(block_outer, 0, "wmma.matrix_a")
B_mat = sch.cache_read(block_outer, 1, "wmma.matrix_b")
sch.compute_at(A_mat, k1)
sch.compute_at(B_mat, k1)
# create write cache to store matrix from wmma fragments to shared memory and global memory
accumulator_shared_to_global = sch.cache_write(block_outer, 0, "shared.dyn")
sch.storage_align(accumulator_shared_to_global, 0, -2, 16, 4)
store = sch.cache_write(block_outer, 0, "wmma.accumulator")
sch.reverse_compute_at(store, thread_idy)
sch.reverse_compute_at(accumulator_shared_to_global, thread_idy)
# split the store loop to match hardware intrinsic pattern
i, j = sch.get_loops(store)[-2:]
i0, i1 = sch.split(i, factors=[None, 16])
j0, j1 = sch.split(j, factors=[None, 16])
sch.reorder(i0, j0, i1, j1)
block_init_c = sch.decompose_reduction(block_outer, k0)
block_init_c_inner = sch.get_child_blocks(block_init_c)[0]
# Tensorization by hardware intrinsics
intrin_group = get_wmma_intrin_group(
load_scope="shared.dyn",
store_scope="shared.dyn",
in_dtype="int8",
out_dtype="int32",
trans_b=True,
)
try:
i, j = sch.get_loops(A_mat)[-2:]
i0, i1 = sch.split(i, factors=[None, 16])
j0, j1 = sch.split(j, factors=[None, 16])
sch.reorder(i0, j0, i1, j1)
sch.unroll(i0)
sch.unroll(j0)
sch.tensorize(i1, intrin_group["load_a"])
i, j = sch.get_loops(B_mat)[-2:]
i0, i1 = sch.split(i, factors=[None, 16])
j0, j1 = sch.split(j, factors=[None, 16])
sch.reorder(i0, j0, i1, j1)
sch.unroll(i0)
sch.unroll(j0)
sch.tensorize(i1, intrin_group["load_b"])
except: # pylint: disable=bare-except
return None
def tensorize_init_store_compute():
sch.tensorize(sch.get_loops(block_init_c_inner)[-2], intrin_group["init"])
sch.tensorize(sch.get_loops(store)[-2], intrin_group["store"])
sch.tensorize(sch.get_loops(block_inner)[-3], intrin_group["compute"])
try:
tensorize_init_store_compute()
except: # pylint: disable=bare-except
return None
auto_inline_consumer_chain(sch, accumulator_shared_to_global)
fused = sch.fuse(*sch.get_loops(accumulator_shared_to_global)[-2:])
_, f1, f2 = sch.split(fused, factors=[None, warp_size, vector_size])
sch.bind(f1, "threadIdx.x")
sch.vectorize(f2)
return sch
class Matmul(GPUScheduleRule):
"""The schedule rule for matmul-like computation"""
@dataclass
class Config:
block_size_x: int = 8
block_size_y: int = 8
vthread_x: int = 1
vthread_y: int = 1
micro_size_x: int = 4
micro_size_y: int = 4
micro_size_k: int = 8
vector_size: int = 1
unroll: int = 256 # 0 means no unroll
use_shared: bool = True
storage_align: bool = False
inner_x: bool = False
def get_configs(self, target: Target) -> Config:
"""Get the schedule config for the target"""
if target.kind.name == "cuda" or target.kind.name == "rocm":
return Matmul.Config(
block_size_x=8,
block_size_y=16,
vthread_x=1,
vthread_y=1,
micro_size_x=4,
micro_size_y=4,
micro_size_k=16,
vector_size=2,
unroll=256,
use_shared=True,
storage_align=True,
inner_x=False,
)
elif target.kind.name == "opencl" and (
("android" in str(target.host)) or ("adreno" in str(target.attrs))
):
return Matmul.Config(
block_size_x=32,
block_size_y=4,
vthread_x=1,
vthread_y=1,
micro_size_x=8,
micro_size_y=2,
micro_size_k=16,
vector_size=8,
unroll=16,
use_shared=False,
storage_align=False,
inner_x=True,
)
else:
return Matmul.Config()
def apply( # pylint: disable=too-many-locals,missing-docstring
self,
func: tirx.PrimFunc,
target: Target,
_: bool,
) -> s_tir.Schedule | None:
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
return None
sch = s_tir.Schedule(func)
config = self.get_configs(target)
root_block = get_root_block(sch)
blocks = sch.get_child_blocks(root_block)
reduction_blocks = get_reduction_blocks(sch, blocks)
if reduction_blocks is None:
return None
main_block = reduction_blocks[0]
block_stmt = sch.get(main_block)
main_block_info = get_sblock_info(sch, main_block)
iter_infos = main_block_info.iters
if not get_index_map(block_stmt):
return None
# Checks if it's a inner reduction by getting the last matrix's inner Index
def is_inner_reduction(block_stmt, iter_infos):
end_it = block_stmt.reads[-1].region[-1].min
return {it.var: it.kind for it in iter_infos}.get(end_it, "O") == "R"
if (
target.kind.name == "opencl"
and (("android" in str(target.host)) or ("adreno" in str(target.attrs)))
) and not is_inner_reduction(block_stmt, iter_infos):
ret = self.sch_outer_reduction(sch, config, main_block, blocks)
if ret is not None:
return ret
# Step 0. Normalize generic matmul to C[S, I, J] += A[S, I, K] * B[S, J, K]
# Reindex first and than analyze the index map
reindex_a = sch.reindex(main_block, ("read", 0))
reindex_b = sch.reindex(main_block, ("read", 1))
reindex_c = sch.reindex(main_block, ("write", 0))
index_maps = get_index_map(sch.get(main_block))
assert index_maps is not None
matmul_index_map, a_index_map, b_index_map, c_index_map = index_maps
sch.transform_layout(reindex_a, ("write", 0), a_index_map)
sch.transform_layout(reindex_b, ("write", 0), b_index_map)
sch.transform_layout(reindex_c, ("read", 0), c_index_map)
sch.transform_block_layout(main_block, matmul_index_map)
# Step 1. Check Tensor Core support
# Tensorization config:
# If any value of I, J, K is fixed and less than this threshold,
# tensorization rule will not be applied.
minimal_tensorize_threshold = 64
block_stmt = sch.get(main_block)
if target.kind.name == "cuda" and check_sm_version(str(target.attrs.get("arch", ""))) >= 70:
apply_tensorization: bool = True
# the batch dimension is not taken into consideration.
for item_var in block_stmt.iter_vars[1:]:
extent = item_var.dom.extent
if isinstance(extent, tirx.expr.IntImm):
if extent.value <= minimal_tensorize_threshold:
apply_tensorization = False
if apply_tensorization:
# Analyze read/write buffers and choose correct tensorizer: int8 or fp16.
in_dtype, out_dtype = get_in_out_dtypes(block_stmt)
tensorize_sch = None
if in_dtype == "int8" and out_dtype == "int32":
tensorize_sch = MatmulInt8Tensorization().apply(func, target, _)
elif in_dtype == "float16" and out_dtype in ["float16", "float32"]:
tensorize_sch = MatmulTensorization().apply(func, target, _)
if tensorize_sch is not None:
return tensorize_sch
elif target.kind.name == "metal":
try:
return MetalMatmul().apply(func, target, _)
except: # pylint: disable=bare-except
pass
# Step 2. Schedule matmul
y_kernel_size = config.vthread_y * config.block_size_y * config.micro_size_y
x_kernel_size = config.vthread_x * config.block_size_x * config.micro_size_x
if config.inner_x:
sch.pad_einsum(
main_block,
[1, y_kernel_size, x_kernel_size, config.micro_size_k],
)
batch, y, x, k = sch.get_loops(main_block)
else:
sch.pad_einsum(
main_block,
[1, x_kernel_size, y_kernel_size, config.micro_size_k],
)
batch, x, y, k = sch.get_loops(main_block)
by, vy, ty, yi = sch.split(
y, [None, config.vthread_y, config.block_size_y, config.micro_size_y]
)
bx, vx, tx, xi = sch.split(
x, [None, config.vthread_x, config.block_size_x, config.micro_size_x]
)
ko, ki = sch.split(k, factors=[None, config.micro_size_k])
reordered_loops = [by, bx, vy, vx, ty, tx, ko, ki] + (
[yi, xi] if config.inner_x else [xi, yi]
)
sch.reorder(*reordered_loops)
by = sch.fuse(batch, by)
sch.bind(bx, "blockIdx.x")
sch.bind(by, "blockIdx.y")
sch.bind(vy, "vthread.y")
sch.bind(vx, "vthread.x")
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
inner_loop = config.micro_size_x if config.inner_x else config.micro_size_y
if inner_loop % config.vector_size == 0:
_, v = sch.split(reordered_loops[-1], [None, config.vector_size])
sch.vectorize(v)
if config.unroll > 0:
sch.annotate(tx, ann_key="pragma_auto_unroll_max_step", ann_val=config.unroll)
sch.annotate(tx, ann_key="pragma_unroll_explicit", ann_val=1)
l2g = sch.cache_write(main_block, 0, "local")
sch.reverse_compute_at(l2g, tx, preserve_unit_loops=True)
if config.micro_size_x % config.vector_size == 0:
_, v = sch.split(sch.get_loops(l2g)[-1], [None, config.vector_size])
sch.vectorize(v)
if config.use_shared:
def _cooperative_fetch(index, vec_len):
block = sch.cache_read(main_block, index, "shared")
num_loops = len(sch.get_loops(block))
sch.compute_at(block, ko, preserve_unit_loops=True)
loops = sch.get_loops(block)[-num_loops:]
ty, tx, _, vec = sch.split(
sch.fuse(*loops),
factors=[config.block_size_y, config.block_size_x, None, vec_len],
)
sch.vectorize(vec)
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
if config.storage_align:
sch.storage_align(block, 0, axis=1, factor=8, offset=vec_len)
return block
a_g2s = _cooperative_fetch(0, vec_len=config.vector_size)
b_g2s = _cooperative_fetch(1, vec_len=config.vector_size)
auto_inline_producers(sch, a_g2s)
auto_inline_producers(sch, b_g2s)
else:
auto_inline_producers(sch, main_block)
auto_inline_consumer_chain(sch, l2g)
sch.decompose_reduction(main_block, ko)
return sch
def sch_outer_reduction(
self,
sch: s_tir.Schedule,
config: Config,
reduction_block: s_tir.schedule.SBlockRV,
blocks: list[s_tir.schedule.SBlockRV],
) -> s_tir.Schedule | None:
"""Get vectorization factor"""
def get_max_factor(n, factors):
factors = sorted(factors, reverse=True)
for factor in factors:
if n % factor == 0:
return factor
return 1
reduction_loops = sch.get_loops(reduction_block)
if not len(reduction_loops) == 4:
return None
mb, ms, n, k = reduction_loops
if not (
isinstance(sch.get(n).extent, tirx.IntImm)
and isinstance(sch.get(mb).extent, tirx.IntImm)
and not isinstance(sch.get(ms).extent, tirx.IntImm)
):
return None
Threads_X, Threads_Y, VecSize, Unroll_M = (
config.block_size_x,
config.block_size_y,
config.vector_size,
config.unroll,
)
VecSize = min(get_max_factor(sch.get(n).extent // Threads_X, [1, 2, 4, 8]), VecSize)
dequant_block = None
matmul_block = reduction_block
epilogue_block = None
if blocks[-1] is not matmul_block:
epilogue_block = blocks[-1]
for blk in blocks[:-1]:
if "dequantize" in sch.get(blk).name_hint:
dequant_block = blk
elif blk is not matmul_block:
sch.compute_inline(blk)
block = sch.reindex(reduction_block, ("read", 0))
sch.pad_einsum(reduction_block, [1, Unroll_M, 1, 1])
sch.compute_inline(block)
trans_block, matmul_reindex = (
sch.get_producers(matmul_block)[0],
sch.get_consumers(matmul_block)[0],
)
if epilogue_block is not None:
sch.compute_inline(matmul_reindex)
matmul_reindex = epilogue_block
sch.transform_layout(
trans_block,
("write", 0),
T.index_map(lambda i0, i1, i2: (i0, i1 // Unroll_M, i2, i1 % Unroll_M)),
)
# transpose block schedules
# sch.set_scope(trans_block, 0, "global.texture-1d")
tb, tn, tk = sch.get_loops(trans_block)
tbx, ttx = sch.split(tk, [None, Threads_X])
tby, tty, tc = sch.split(tn, [None, Threads_Y, Unroll_M])
sch.bind(tb, "blockIdx.z")
sch.bind(tby, "blockIdx.y")
sch.bind(tbx, "blockIdx.x")
sch.bind(tty, "threadIdx.y")
sch.bind(ttx, "threadIdx.x")
sch.reorder(tb, tby, tbx, tty, ttx, tc)
sch.vectorize(tc)
mb, ms, n, k = sch.get_loops(matmul_block)
m = sch.fuse(mb, ms)
bx, tx, vec = sch.split(n, [None, Threads_X, VecSize])
by, ty, unr = sch.split(m, [None, Threads_Y, Unroll_M])
k1, k2, k3 = sch.split(k, [None, 4, 8])
sch.reorder(bx, by, tx, ty, k1, k2, k3, unr, vec)
sch.set_scope(matmul_block, 0, "local")
if dequant_block is not None:
sch.compute_at(dequant_block, k3)
sch.set_scope(dequant_block, 0, "local")
sch.bind(by, "blockIdx.y")
sch.bind(bx, "blockIdx.x")
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
sch.vectorize(vec)
inp = sch.cache_read(matmul_block, read_buffer_index=0, storage_scope="local")
sch.compute_at(inp, k3, preserve_unit_loops=True)
sch.vectorize(sch.get_loops(inp)[-1])
sch.unroll(unr)
sch.unroll(k3)
if dequant_block is not None:
Aq_local = sch.cache_read(dequant_block, read_buffer_index=0, storage_scope="local")
sch.compute_at(Aq_local, k2, preserve_unit_loops=True)
sch.vectorize(sch.get_loops(Aq_local)[-1])
As_local = sch.cache_read(dequant_block, read_buffer_index=1, storage_scope="local")
sch.compute_at(As_local, k1, preserve_unit_loops=True)
sch.vectorize(sch.get_loops(As_local)[-1])
sch.vectorize(sch.get_loops(dequant_block)[-1])
sch.reverse_compute_at(matmul_reindex, ty)
o_ur, o_vec = sch.get_loops(matmul_reindex)[-2:]
sch.vectorize(o_vec)
sch.unroll(o_ur)
sch.decompose_reduction(matmul_block, k1)
return sch