Files
apache--tvm/python/tvm/s_tir/dlight/gpu/general_reduction.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

184 lines
8.3 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=invalid-name
"""Reduction rule for operators including softmax, layer norm, RMS norm, etc"""
from tvm import arith, s_tir, tirx
from tvm.target import Target
from ..analysis import normalize_prim_func
from ..base import try_inline_contiguous_spatial
from .base import GPUScheduleRule
class GeneralReduction(GPUScheduleRule):
"""General Reduction rule for operators including softmax, layer norm, RMS norm, etc"""
def apply( # pylint: disable=too-many-locals
self,
func: tirx.PrimFunc,
target: Target,
_: bool,
) -> None | s_tir.Schedule | list[s_tir.Schedule]:
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
return None
if target.kind.name == "cuda":
len_tx = 256
unroll_depth = 256
elif target.kind.name == "opencl":
len_tx = 256
unroll_depth = 64
else:
len_tx = 64
unroll_depth = 64
sch = s_tir.Schedule(func)
block_infos = normalize_prim_func(sch)
block_infos = try_inline_contiguous_spatial(sch, block_infos)
if block_infos is None or len(block_infos) == 0:
return None
dom_kind = block_infos[0].dom_kind()
num_leading_s = len(dom_kind) - len(dom_kind.lstrip("S"))
num_trailing_r = len(dom_kind) - len(dom_kind.rstrip("R"))
# Align the number of block iters of the last block.
num_last_block_iter = len(block_infos[-1].dom_kind())
if num_last_block_iter < len(dom_kind):
# If the last block is a scalar value, there is nothing left to
# tile/parallelise, and `iters` is an empty tuple.
# Add a unit thread loop so the final write happens inside a valid
# GPU thread environment.
if num_last_block_iter == 0:
# Put every block (both the running reductions and the final
# scalar write) inside a trivial GPU thread. The very first block
# gets a `blockIdx.x` wrapper so that kernels still have a unique
# block scope.
for i, info in enumerate(block_infos):
loop_rv = sch.add_unit_loop(info.block_rv)
if i == 0:
sch.bind(loop_rv, "blockIdx.x")
else:
sch.bind(loop_rv, "threadIdx.x")
return sch
def f_layout_mapping(*iters):
analyzer = arith.Analyzer()
# Try to match the iters of last block to the iters of the first block.
# For matched positions, use the iter from the input `iters`.
# For unmatched positions, use a new iter which is constant 0.
num_matched = 0
target_layout_iters = []
for block_iter in block_infos[0].iters:
if num_matched < len(iters) and analyzer.can_prove_equal(
block_iter.dom, block_infos[-1].iters[num_matched].dom
):
target_layout_iters.append(iters[num_matched])
num_matched += 1
else:
target_layout_iters.append(tirx.const(0, iters[0].ty))
# If all the iters of the last block can match, return the new layout.
if num_matched == len(iters):
return target_layout_iters
# Otherwise, fallback to appending zeros in the beginning.
return [tirx.const(0, iters[0].ty)] * (len(dom_kind) - num_last_block_iter) + list(
iters
)
index_map = tirx.IndexMap.from_func(f_layout_mapping, ndim=num_last_block_iter)
sch.transform_block_layout(block_infos[-1].block_rv, index_map)
try:
# TODO: fix num_leading_s = 0 case
assert num_trailing_r > 0
for block in block_infos[1:-1]:
assert block.dom_kind() == dom_kind
assert block_infos[-1].is_injective()
assert len(block_infos[-1].dom_kind()) <= len(dom_kind)
except AssertionError:
return None
if "R" not in block_infos[-1].dom_kind():
# The final block is a spatial block.
# It is possible that the loop order of the last block is not the same as
# previous blocks.
# Thus we reorder spatial loops to align with reduction loops for followup schedule.
# We first collect all the buffers written by reduction blocks,
# then in the final block, any index of those buffers are spatial.
reduced_buffers = []
for block_info in block_infos[:-1]:
for buffer_write in sch.get(block_info.block_rv).writes:
reduced_buffers.append(buffer_write.buffer)
spatial_block = sch.get(block_infos[-1].block_rv)
spatial_loops = set()
block_var_to_loop_var = {}
loops = sch.get_loops(block_infos[-1].block_rv)
for block_iter, loop_rv in zip(spatial_block.iter_vars, loops):
block_var_to_loop_var[block_iter.var] = sch.get(loop_rv).loop_var
def _visit_expr(e: tirx.Expr):
if isinstance(e, tirx.Var) and e in block_var_to_loop_var:
spatial_loops.add(block_var_to_loop_var[e])
for buffer_read in spatial_block.reads:
buffer = buffer_read.buffer
if buffer in reduced_buffers:
for read_range in buffer_read.region:
tirx.stmt_functor.post_order_visit(read_range.min, _visit_expr)
tirx.stmt_functor.post_order_visit(read_range.extent, _visit_expr)
s_loops = []
other_loops = []
for loop_rv in loops:
loop = sch.get(loop_rv)
if loop.loop_var in spatial_loops or loop.extent == 1:
s_loops.append(loop_rv)
else:
other_loops.append(loop_rv)
sch.reorder(*s_loops, *other_loops)
loops = sch.get_loops(block_infos[-1].block_rv)
bx = sch.fuse(*loops[:num_leading_s])
r_loop, tx = sch.split(loops[-1], [None, len_tx])
sch.reorder(tx, r_loop)
sch.bind(bx, "blockIdx.x")
sch.bind(tx, "threadIdx.x")
sch.annotate(r_loop, ann_key="pragma_auto_unroll_max_step", ann_val=unroll_depth)
sch.annotate(r_loop, ann_key="pragma_unroll_explicit", ann_val=1)
for block in reversed(block_infos[:-1]):
block = block.block_rv
for i, _ in enumerate(sch.get(block).writes):
sch.set_scope(block, buffer_index=i, storage_scope="shared")
sch.compute_at(block, bx, preserve_unit_loops=True)
r_loop = sch.fuse(*sch.get_loops(block)[-num_trailing_r:])
r_loop, tx = sch.split(r_loop, [None, len_tx])
sch.reorder(tx, r_loop)
sch.bind(tx, "threadIdx.x")
sch.annotate(r_loop, ann_key="pragma_auto_unroll_max_step", ann_val=unroll_depth)
sch.annotate(r_loop, ann_key="pragma_unroll_explicit", ann_val=1)
# TODO: It's just a workaround to avoid unroll spatial loops, because of the bug of
# the pass lower-thread-allreduce. We should fix it in the future.
# sch.annotate(bx, ann_key="pragma_auto_unroll_max_step", ann_val=unroll_depth)
# sch.annotate(bx, ann_key="pragma_unroll_explicit", ann_val=1)
return sch