chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""DLight package provides efficient schedules out-of-box for deep learning workloads."""
|
||||
|
||||
from . import gpu
|
||||
from . import adreno
|
||||
from . import cpu
|
||||
from .analysis import (
|
||||
SBlockInfo,
|
||||
IterInfo,
|
||||
normalize_prim_func,
|
||||
)
|
||||
from .base import (
|
||||
ApplyDefaultSchedule,
|
||||
ScheduleRule,
|
||||
try_inline,
|
||||
try_inline_contiguous_spatial,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""
|
||||
Adreno schedule rules.
|
||||
"""
|
||||
|
||||
from .convolution import Conv2d
|
||||
from .layout_transform import LayoutTransform
|
||||
from .fallback import Fallback
|
||||
from .pool import Pool2D
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.
|
||||
"""Base schedule rule for Adreno operators."""
|
||||
|
||||
from tvm.target import Target
|
||||
|
||||
from ..base import ScheduleRule
|
||||
|
||||
|
||||
class AdrenoScheduleRule(ScheduleRule): # pylint: disable=too-few-public-methods
|
||||
"""The Schedule Rule specific to Adreno targets,
|
||||
will return None if the target is not Adreno."""
|
||||
|
||||
def is_target_available(self, target: Target) -> bool:
|
||||
"""Check whether the target is available for Adreno rule.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target : Target
|
||||
The compilation target to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
available : bool
|
||||
Whether the target is available for this rule.
|
||||
"""
|
||||
return super().is_target_available(target) and "adreno" in target.keys
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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
|
||||
"""A Conv2d schedule rule for Adreno GPU operators."""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from .. import analysis
|
||||
from .base import AdrenoScheduleRule
|
||||
from .utils import schedule_default, schedule_inline_blocks
|
||||
|
||||
|
||||
class Conv2d(AdrenoScheduleRule):
|
||||
"""The schedule rule for convolution computation"""
|
||||
|
||||
@staticmethod
|
||||
def schedule_conv2d(sch: s_tir.Schedule, blk: s_tir.schedule.SBlockRV):
|
||||
n, oc, oh, ow, ob, ic, kh, kw = sch.get_loops(blk)
|
||||
|
||||
bz, vz, tz = sch.split(oc, [None, 8, 1], preserve_unit_iters=True)
|
||||
by, vy, ty = sch.split(oh, [None, 1, 16], preserve_unit_iters=True)
|
||||
bx, vx, tx = sch.split(ow, [None, 1, 16], preserve_unit_iters=True)
|
||||
|
||||
bz = sch.fuse(n, bz, preserve_unit_iters=True)
|
||||
sch.reorder(bz, by, bx, vz, vy, vx, tz, ty, tx, ob)
|
||||
sch.bind(bz, "blockIdx.z")
|
||||
sch.bind(by, "blockIdx.y")
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(vz, "vthread.z")
|
||||
sch.bind(vy, "vthread.y")
|
||||
sch.bind(vx, "vthread.x")
|
||||
sch.bind(tz, "threadIdx.z")
|
||||
sch.bind(ty, "threadIdx.y")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
|
||||
rblk = sch.cache_read(blk, 0, "local")
|
||||
ico, icb = sch.split(ic, [None, 4], preserve_unit_iters=True)
|
||||
sch.reorder(ico, kh, kw, icb, ob)
|
||||
|
||||
sch.compute_at(rblk, kw, preserve_unit_loops=True)
|
||||
sch.vectorize(sch.get_loops(rblk)[-1])
|
||||
wblk = sch.cache_write(blk, 0, "local")
|
||||
sch.reverse_compute_at(wblk, tx, preserve_unit_loops=True)
|
||||
sch.vectorize(sch.get_loops(wblk)[-1])
|
||||
init_blk = sch.decompose_reduction(blk, tx)
|
||||
sch.vectorize(sch.get_loops(init_blk)[-1])
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,missing-docstring
|
||||
self,
|
||||
func: tirx.PrimFunc | s_tir.Schedule,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> s_tir.Schedule | None:
|
||||
if not (isinstance(func, tirx.PrimFunc | s_tir.Schedule)) or not self.is_target_available(
|
||||
target
|
||||
):
|
||||
return None
|
||||
|
||||
if isinstance(func, tirx.PrimFunc):
|
||||
sch = s_tir.Schedule(func)
|
||||
sch.work_on("main")
|
||||
elif isinstance(func, s_tir.Schedule):
|
||||
sch = func
|
||||
|
||||
root_block = analysis.get_root_block(sch, sch.func_working_on)
|
||||
blocks = sch.get_child_blocks(root_block)
|
||||
reduction_blocks = list(
|
||||
filter(lambda block: analysis.get_sblock_info(sch, block).is_reduction(), blocks)
|
||||
)
|
||||
remaining_blocks = [blk for blk in blocks if blk not in reduction_blocks]
|
||||
|
||||
def is_convolution(blk):
|
||||
block_info = analysis.get_sblock_info(sch, blk)
|
||||
return "conv2d_NCHWc" in block_info.name
|
||||
|
||||
if len(reduction_blocks) != 1 or not is_convolution(reduction_blocks[0]):
|
||||
return None
|
||||
|
||||
conv_blk = reduction_blocks[0]
|
||||
Conv2d.schedule_conv2d(sch, conv_blk)
|
||||
remaining_blocks = schedule_inline_blocks(sch, remaining_blocks)
|
||||
schedule_default(sch, remaining_blocks)
|
||||
|
||||
return sch
|
||||
@@ -0,0 +1,192 @@
|
||||
# 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.
|
||||
|
||||
# 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.
|
||||
"""Dlight Adreno Fallback Schedules"""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from .. import analysis
|
||||
from .base import AdrenoScheduleRule
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name,missing-function-docstring,unused-variable,unused-import
|
||||
class Fallback(AdrenoScheduleRule):
|
||||
"""Texture Based Fallback Schedule(s) for Adreno"""
|
||||
|
||||
@staticmethod
|
||||
def schedule_inline_blocks(
|
||||
sch: s_tir.Schedule, blocks: list[s_tir.schedule.SBlockRV]
|
||||
) -> list[s_tir.schedule.SBlockRV]:
|
||||
"""
|
||||
Auto Inlines Injective and Element-wise Operations while trying to omit data pad blocks...
|
||||
"""
|
||||
|
||||
if blocks is None:
|
||||
root_blk = analysis.get_root_block(sch)
|
||||
blocks = sch.get_child_blocks(root_blk)
|
||||
|
||||
remaining_blocks = []
|
||||
for blk in blocks:
|
||||
block_info = analysis.get_sblock_info(sch, blk)
|
||||
if block_info.is_injective() and not block_info.is_data_pad(sch):
|
||||
if len(sch.get_consumers(blk)) == 1:
|
||||
try:
|
||||
sch.compute_inline(blk)
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
remaining_blocks.append(blk)
|
||||
elif len(sch.get_producers(blk)) == 1:
|
||||
inlined_once = False
|
||||
try:
|
||||
# Would cause an issue inlining to producer with multiple consumers
|
||||
while (
|
||||
len(sch.get_producers(blk)) == 1
|
||||
and len(sch.get_consumers(sch.get_producers(blk)[0])) == 1
|
||||
):
|
||||
sch.reverse_compute_inline(blk)
|
||||
inlined_once = True
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
break
|
||||
if not inlined_once:
|
||||
remaining_blocks.append(blk)
|
||||
else:
|
||||
remaining_blocks.append(blk)
|
||||
else:
|
||||
remaining_blocks.append(blk)
|
||||
return remaining_blocks
|
||||
|
||||
@staticmethod
|
||||
def schedule_default(sch: s_tir.Schedule, blk: s_tir.schedule.SBlockRV):
|
||||
block_info = analysis.get_sblock_info(sch, blk)
|
||||
|
||||
s_loops, r_loops, o_loops = [], [], []
|
||||
v_loop = block_info.write_bufs(sch)[0].assoc_lps[-1]
|
||||
|
||||
for iter_info in block_info.iters:
|
||||
if sch.get(iter_info.loop_rv) == sch.get(v_loop):
|
||||
continue
|
||||
{"S": s_loops, "R": r_loops, "O": o_loops}.get(iter_info.kind).append(iter_info.loop_rv)
|
||||
|
||||
iter_vars = analysis.collect_block_iter_vars_used_in_access_region(
|
||||
sch.get(blk), block_info.write_bufs(sch)[0].buf_region.region
|
||||
)
|
||||
o_outer = [lp for lp in o_loops if sch.get(lp).var in iter_vars]
|
||||
o_inner = [lp for lp in o_loops if sch.get(lp).var not in iter_vars]
|
||||
|
||||
# Can't change loop order for opaque loops
|
||||
if o_loops != o_outer + o_inner:
|
||||
return
|
||||
|
||||
o_outer.append(v_loop)
|
||||
sch.reorder(*s_loops, *o_outer, *r_loops, *o_inner)
|
||||
|
||||
assert s_loops
|
||||
tgt = Target.current(allow_none=True)
|
||||
|
||||
b = sch.fuse(*s_loops)
|
||||
tx_extent = get_max_threads_per_block(tgt) if tgt is not None else 256
|
||||
bx, tx = sch.split(b, [None, tx_extent])
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
|
||||
if len(r_loops) > 1:
|
||||
lp = [*s_loops, *o_outer][-1]
|
||||
init_block = sch.decompose_reduction(blk, lp)
|
||||
wblk = sch.cache_write(blk, 0, "local")
|
||||
sch.compute_at(wblk, lp)
|
||||
if v_loop:
|
||||
sch.vectorize(sch.get_loops(init_block)[-1])
|
||||
sch.vectorize(sch.get_loops(wblk)[-1])
|
||||
elif v_loop is not None:
|
||||
sch.vectorize(v_loop)
|
||||
|
||||
@staticmethod
|
||||
def schedule_fallback(sch):
|
||||
root_block = analysis.get_root_block(sch)
|
||||
blocks = sch.get_child_blocks(root_block)
|
||||
|
||||
schedule_blocks = [
|
||||
blk
|
||||
for blk in blocks
|
||||
if analysis.get_sblock_info(sch, blk).is_reduction()
|
||||
or analysis.get_sblock_info(sch, blk).is_data_pad(sch)
|
||||
]
|
||||
remaining_blocks = [blk for blk in blocks if blk not in schedule_blocks]
|
||||
|
||||
for blk in schedule_blocks:
|
||||
Fallback.schedule_default(sch, blk)
|
||||
remaining_blocks = Fallback.schedule_inline_blocks(sch, remaining_blocks)
|
||||
# TODO: Analyze unscheduled blocks to schedule instead of relying on remaining
|
||||
for blk in remaining_blocks:
|
||||
Fallback.schedule_default(sch, blk)
|
||||
|
||||
def apply( # pylint: disable=too-many-locals
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> None | s_tir.Schedule | list[s_tir.Schedule]:
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
|
||||
return None
|
||||
|
||||
sch = s_tir.Schedule(func)
|
||||
root_block = analysis.get_root_block(sch)
|
||||
blocks = sch.get_child_blocks(root_block)
|
||||
|
||||
if any(len(sch.get_child_blocks(block)) != 0 for block in blocks):
|
||||
return None
|
||||
|
||||
block_infos = [analysis.get_sblock_info(sch, block) for block in blocks]
|
||||
if not any("texture" in block.write_bufs(sch)[0].get_scope() for block in block_infos):
|
||||
return None
|
||||
|
||||
Fallback.schedule_fallback(sch)
|
||||
return sch
|
||||
@@ -0,0 +1,146 @@
|
||||
# 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.
|
||||
# ruff: noqa: F841
|
||||
|
||||
# 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, unused-variable
|
||||
|
||||
"Schedules for Texture Based Layout Transforms"
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from .. import analysis
|
||||
from .base import AdrenoScheduleRule
|
||||
|
||||
|
||||
class LayoutTransform(AdrenoScheduleRule):
|
||||
"""Texture based Layout Transform Dlight Schedule for Adreno"""
|
||||
|
||||
def __init__(self, use_op_name=True):
|
||||
self.use_op_name = use_op_name
|
||||
|
||||
# TODO: Try using Coalesced Writes...
|
||||
def apply( # pylint: disable=too-many-locals
|
||||
self,
|
||||
func: tirx.PrimFunc | s_tir.Schedule,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> None | s_tir.Schedule | list[s_tir.Schedule]:
|
||||
# pylint: disable=invalid-name
|
||||
if not (isinstance(func, tirx.PrimFunc | s_tir.Schedule)) or not self.is_target_available(
|
||||
target
|
||||
):
|
||||
return None
|
||||
|
||||
if isinstance(func, tirx.PrimFunc):
|
||||
sch = s_tir.Schedule(func)
|
||||
sch.work_on("main")
|
||||
elif isinstance(func, s_tir.Schedule):
|
||||
sch = func
|
||||
|
||||
root_block = analysis.get_root_block(sch, sch.func_working_on)
|
||||
|
||||
if len(sch.get_child_blocks(root_block)) != 1:
|
||||
return None
|
||||
|
||||
blk = sch.get_child_blocks(root_block)[0]
|
||||
block_info = analysis.get_sblock_info(sch, blk)
|
||||
if not (
|
||||
(self.use_op_name and block_info.name == "te_layout_transform")
|
||||
or (not self.use_op_name and block_info.is_layout_transform(sch))
|
||||
):
|
||||
return None
|
||||
|
||||
read_buf, write_buf = (block_info.read_bufs(sch)[0], block_info.write_bufs(sch)[0])
|
||||
lps = block_info.get_loops()
|
||||
lpv_read, lpv_write = (
|
||||
read_buf.assoc_lps[-1],
|
||||
write_buf.assoc_lps[-1],
|
||||
)
|
||||
|
||||
if lpv_read is None or lpv_write is None:
|
||||
return None
|
||||
|
||||
vlen_read, vlen_write = read_buf.get_vecsize(), write_buf.get_vecsize()
|
||||
local_cache = sch.get(lpv_read) != sch.get(lpv_write) or vlen_read != vlen_write
|
||||
block_loops = [
|
||||
lp
|
||||
for lp in lps
|
||||
if sch.get(lp) != sch.get(lpv_read) and sch.get(lp) != sch.get(lpv_write)
|
||||
]
|
||||
vec_loops = (
|
||||
[lpv_read, lpv_write] if sch.get(lpv_read) != sch.get(lpv_write) else (lpv_read,)
|
||||
)
|
||||
sch.reorder(*block_loops, *vec_loops)
|
||||
if local_cache:
|
||||
if sch.get(lpv_read) != sch.get(lpv_write):
|
||||
blp_read, vlp_read = sch.split(
|
||||
lpv_read, [None, vlen_read], preserve_unit_iters=True
|
||||
)
|
||||
blp_write, vlp_write = sch.split(
|
||||
lpv_write, [None, vlen_write], preserve_unit_iters=True
|
||||
)
|
||||
sch.reorder(blp_read, blp_write, vlp_read, vlp_write)
|
||||
block_loops += [blp_read, blp_write]
|
||||
rblk = sch.cache_read(blk, 0, "local")
|
||||
sch.compute_at(rblk, block_loops[-1], preserve_unit_loops=True)
|
||||
sch.vectorize(sch.get_loops(rblk)[-1])
|
||||
sch.vectorize(vlp_write)
|
||||
else:
|
||||
if vlen_read > vlen_write:
|
||||
read_lp, vec_lp = sch.split(blk, [None, vlen_write], preserve_unit_iters=True)
|
||||
rblk = sch.cache_read(blk, 0, "local")
|
||||
sch.compute_at(rblk, read_lp, preserve_unit_loops=True)
|
||||
sch.vectorize(sch.get_loops(rblk)[-1])
|
||||
sch.vectorize(vec_lp)
|
||||
else:
|
||||
rblk = sch.cache_read(blk, 0, "local")
|
||||
sch.compute_at(rblk, block_loops[-1], preserve_unit_loops=True)
|
||||
_, vread_lp = sch.split(
|
||||
sch.get_loops(rblk)[-1], vlen_read, preserve_unit_iters=True
|
||||
)
|
||||
sch.vectorize(vread_lp)
|
||||
sch.vectorize(vlp_write)
|
||||
else:
|
||||
blp, vlp = sch.split(lpv_read, [None, vlen_read], preserve_unit_iters=True)
|
||||
block_loops += [blp]
|
||||
sch.vectorize(vlp)
|
||||
|
||||
b = sch.fuse(*block_loops)
|
||||
tx_extent = min(sch.get(b).extent, 256)
|
||||
candidates = [1, 2, 4, 8, 16, 32]
|
||||
bx, tx = sch.split(b, [None, 256], preserve_unit_iters=True)
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
return sch
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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
|
||||
# ruff: noqa: F841
|
||||
"""Pool schedule rule for Adreno operators."""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from .. import analysis
|
||||
from .base import AdrenoScheduleRule
|
||||
|
||||
|
||||
# pylint: disable=invalid-name, unused-variable
|
||||
class Pool2D(AdrenoScheduleRule):
|
||||
def apply( # pylint: disable=too-many-locals,missing-docstring
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> s_tir.Schedule:
|
||||
sch = s_tir.Schedule(func)
|
||||
root = sch.get_sblock(name="root", func_name="main")
|
||||
|
||||
blocks = sch.get_child_blocks(root)
|
||||
blocks_names = [sch.get(blk).name_hint for blk in blocks]
|
||||
|
||||
if "adaptive_pool_sum" not in blocks_names and "pool_max" not in blocks_names:
|
||||
return None
|
||||
|
||||
def schedule_pad(blk: s_tir.schedule.SBlockRV):
|
||||
lps, veclp = sch.get_loops(blk)[:-1], sch.get_loops(blk)[-1]
|
||||
sch.vectorize(veclp)
|
||||
b = sch.fuse(*lps)
|
||||
tx_extent = min(int(sch.get(b).extent) & ~int(sch.get(b).extent - 1), 256)
|
||||
bx, tx = sch.split(b, [None, tx_extent])
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
|
||||
def schedule_max_pool(blk: s_tir.schedule.SBlockRV):
|
||||
block_info = analysis.get_sblock_info(sch, blk)
|
||||
iters_kind = "".join([_iter.kind for _iter in block_info.iters])
|
||||
if iters_kind != "SSSSSRR":
|
||||
return None
|
||||
|
||||
lps = sch.get_loops(blk)
|
||||
block_lps, vec_lp, red_lps = lps[:4], lps[4], lps[5:]
|
||||
write_blk = sch.cache_write(blk, 0, "local")
|
||||
sch.reverse_compute_at(write_blk, vec_lp)
|
||||
b = sch.fuse(*block_lps)
|
||||
tx_extent = min(int(sch.get(b).extent) & ~int(sch.get(b).extent - 1), 256)
|
||||
bx, tx = sch.split(b, [None, tx_extent])
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.vectorize(vec_lp)
|
||||
|
||||
return True
|
||||
|
||||
passed_reduction = False
|
||||
for blk in blocks:
|
||||
if sch.get(blk).name_hint == "pad_temp":
|
||||
schedule_pad(blk)
|
||||
elif (
|
||||
sch.get(blk).name_hint == "adaptive_pool_sum"
|
||||
or sch.get(blk).name_hint == "pool_max"
|
||||
):
|
||||
ok = schedule_max_pool(blk)
|
||||
if not ok:
|
||||
return None
|
||||
passed_reduction = True
|
||||
else:
|
||||
try:
|
||||
if passed_reduction:
|
||||
sch.reverse_compute_inline(blk)
|
||||
else:
|
||||
sch.compute_inline(blk)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
return sch
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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.
|
||||
"""Utilis for Adreno operators."""
|
||||
|
||||
# pylint: disable=import-outside-toplevel, unused-argument, invalid-name, missing-function-docstring
|
||||
|
||||
from tvm import s_tir
|
||||
from tvm.target import Target
|
||||
|
||||
from ..analysis import SBlockInfo
|
||||
|
||||
|
||||
def get_texture_storage(block_info: SBlockInfo):
|
||||
"""
|
||||
Returns the texture layout acceptable for the shape
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape: array
|
||||
Shape of the tensor to be packed to texture
|
||||
"""
|
||||
# certain limitation of the Qualcomm devices. Subject to be determined for certain device
|
||||
# individually, but until we have access to remote device during compilation, we have to
|
||||
# define it uniformly for all target devices
|
||||
# spatial_limit = 16384, depth_limit = 2048
|
||||
# TODO: Check Write Bufs.
|
||||
shape = block_info.write_bufs[0].buf_region.buffer.shape
|
||||
|
||||
spatial_limit = Target.current().attrs["texture_spatial_limit"]
|
||||
depth_limit = Target.current().attrs["texture_depth_limit"]
|
||||
|
||||
if len(shape) > 4:
|
||||
if shape[0] < spatial_limit and shape[1] * shape[2] * shape[3] < spatial_limit:
|
||||
return "global.texture-weight"
|
||||
elif shape[0] < depth_limit and shape[2] * shape[3] < spatial_limit:
|
||||
return "global.texture-nhwc"
|
||||
elif (
|
||||
shape[0] * shape[1] < depth_limit
|
||||
and shape[2] < spatial_limit
|
||||
and shape[3] < spatial_limit
|
||||
):
|
||||
return "global.texture"
|
||||
elif len(shape) > 3:
|
||||
if shape[0] < spatial_limit and shape[1] * shape[2] < spatial_limit:
|
||||
return "global.texture-weight"
|
||||
elif shape[0] < depth_limit and shape[1] < spatial_limit and shape[2] < spatial_limit:
|
||||
return "global.texture"
|
||||
elif len(shape) == 3:
|
||||
if shape[0] < spatial_limit and shape[1] < spatial_limit:
|
||||
return "global.texture-weight"
|
||||
|
||||
return "global"
|
||||
|
||||
|
||||
def schedule_inline_blocks(
|
||||
sch: s_tir.Schedule, blocks: list[s_tir.schedule.SBlockRV] | None = None
|
||||
):
|
||||
from .fallback import Fallback
|
||||
|
||||
return Fallback.schedule_inline_blocks(sch, blocks)
|
||||
|
||||
|
||||
def schedule_default(sch, blocks: list[s_tir.schedule.SBlockRV] | None = None):
|
||||
from .fallback import Fallback
|
||||
|
||||
ret = []
|
||||
for blk in blocks:
|
||||
ret.append(Fallback.schedule_default(sch, blk))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def schedule_fallback(sch, blk):
|
||||
from .fallback import Fallback
|
||||
|
||||
return Fallback.schedule_fallback(sch)
|
||||
@@ -0,0 +1,35 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""Base infra"""
|
||||
|
||||
from .common_analysis import (
|
||||
SBlockInfo,
|
||||
IterInfo,
|
||||
collect_block_iter_vars_used_in_access_region,
|
||||
collect_vars_used_in_prim_expr,
|
||||
detect_dominant_read,
|
||||
is_broadcast_epilogue,
|
||||
normalize_prim_func,
|
||||
get_root_block,
|
||||
get_sblock_info,
|
||||
get_max_shared_memory_per_block,
|
||||
)
|
||||
from .gemv import (
|
||||
is_gemv,
|
||||
normalize,
|
||||
)
|
||||
@@ -0,0 +1,460 @@
|
||||
# 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
|
||||
@@ -0,0 +1,163 @@
|
||||
# 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.
|
||||
"""Analysis for GEMV."""
|
||||
|
||||
import tvm_ffi
|
||||
|
||||
from tvm import arith, s_tir, tirx
|
||||
|
||||
from .common_analysis import (
|
||||
SBlockInfo,
|
||||
collect_block_iter_vars_used_in_access_region,
|
||||
collect_vars_used_in_prim_expr,
|
||||
detect_dominant_read,
|
||||
)
|
||||
|
||||
|
||||
def get_reduction_expr(block: tirx.SBlock) -> tirx.Expr | None:
|
||||
"""Extracts the reduction expression from a TIR block.
|
||||
|
||||
This function checks whether the given TIR block follows a reduction pattern
|
||||
of the form `X[...] = X[...] + Y` and returns `Y` as the reduction expression.
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
block : tirx.SBlock
|
||||
The TIR block to analyze.
|
||||
|
||||
Returns:
|
||||
-------
|
||||
Optional[tirx.Expr]
|
||||
The reduction expression (`Y`) if detected, otherwise None.
|
||||
"""
|
||||
|
||||
buffer_store = block.body
|
||||
if not isinstance(buffer_store, tirx.BufferStore):
|
||||
return None
|
||||
if not isinstance(buffer_store.value, tirx.Add):
|
||||
return None
|
||||
if not tvm_ffi.structural_equal(
|
||||
buffer_store.value.a,
|
||||
tirx.BufferLoad(buffer_store.buffer, block.body.indices),
|
||||
map_free_vars=True,
|
||||
):
|
||||
return None
|
||||
return buffer_store.value.b
|
||||
|
||||
|
||||
def is_gemv(sch: s_tir.Schedule, block_info: SBlockInfo) -> list[tirx.Buffer] | None:
|
||||
"""Check if the block is a GEMV.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
sch : s_tir.Schedule
|
||||
The schedule
|
||||
|
||||
block_info : SBlockInfo
|
||||
The block info to be checked
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : Optional[List[tirx.Buffer]]
|
||||
The vector buffers used in the GEMV if it is a GEMV, otherwise None.
|
||||
"""
|
||||
block = block_info.block_rv
|
||||
block_stmt = sch.get(block)
|
||||
conditions = []
|
||||
conditions.append(block_info.is_reduction())
|
||||
conditions.append(len(block_stmt.reads) >= 2)
|
||||
conditions.append(len(block_stmt.writes) == 1)
|
||||
conditions.append(get_reduction_expr(block_stmt) is not None)
|
||||
conditions.append(
|
||||
len(collect_block_iter_vars_used_in_access_region(block_stmt, block_stmt.writes[0].region))
|
||||
> 0
|
||||
)
|
||||
if not all(conditions):
|
||||
return None
|
||||
|
||||
iter_num = len(block_stmt.iter_vars)
|
||||
ret = [
|
||||
read.buffer
|
||||
for read in block_stmt.reads
|
||||
if len(collect_block_iter_vars_used_in_access_region(block_stmt, read.region)) < iter_num
|
||||
and len(collect_block_iter_vars_used_in_access_region(block_stmt, read.region)) > 0
|
||||
]
|
||||
return ret if 0 < len(ret) < len(block_stmt.reads) else None
|
||||
|
||||
|
||||
def normalize(
|
||||
sch: s_tir.Schedule,
|
||||
block_info: SBlockInfo,
|
||||
) -> bool | None:
|
||||
"""Normalize the main block."""
|
||||
block_stmt: tirx.SBlock = sch.get(block_info.block_rv)
|
||||
access = arith.normalize_to_iter_sum(
|
||||
detect_dominant_read(block_stmt),
|
||||
input_iters={i.var: i.dom for i in block_stmt.iter_vars},
|
||||
)
|
||||
buffers_use_vars = [
|
||||
collect_block_iter_vars_used_in_access_region(block_stmt, buf.region)
|
||||
for buf in block_stmt.writes
|
||||
]
|
||||
buffers_use_vars.extend(
|
||||
[
|
||||
collect_block_iter_vars_used_in_access_region(block_stmt, buf.region)
|
||||
for buf in block_stmt.reads
|
||||
]
|
||||
)
|
||||
if collect_vars_used_in_prim_expr(access.base) & set(
|
||||
iter_var.var for iter_var in block_stmt.iter_vars
|
||||
):
|
||||
return None
|
||||
iter_to_info = {i.var: i for i in block_info.iters}
|
||||
batch_loops, s_loops, r_loops, c_loops = [], [], [], []
|
||||
inner_axis = access.args[-1].source.source
|
||||
is_inner_reduction = iter_to_info[inner_axis].kind == "R"
|
||||
|
||||
for split_expr in access.args:
|
||||
var = split_expr.source.source
|
||||
info = iter_to_info.get(var)
|
||||
loop = info.loop_rv
|
||||
is_reduction = info.kind == "R"
|
||||
if split_expr.lower_factor > 1:
|
||||
if c_loops:
|
||||
return None
|
||||
loop, c_loop = sch.split(loop, factors=[None, split_expr.lower_factor])
|
||||
# we only support the reduction dim being grouped atm
|
||||
if not is_reduction:
|
||||
return None
|
||||
c_loops.append(c_loop)
|
||||
if is_reduction:
|
||||
r_loops.append(loop)
|
||||
elif all([var in buf_vars for buf_vars in buffers_use_vars]):
|
||||
batch_loops.append(loop)
|
||||
else:
|
||||
s_loops.append(loop)
|
||||
|
||||
assert s_loops
|
||||
assert r_loops
|
||||
if not c_loops:
|
||||
c_loops = [sch.add_unit_loop(block_info.block_rv)]
|
||||
if not batch_loops:
|
||||
batch_loops = [sch.add_unit_loop(block_info.block_rv)]
|
||||
sch.reorder(*batch_loops, *s_loops, *r_loops, *c_loops)
|
||||
sch.fuse(*batch_loops)
|
||||
sch.fuse(*s_loops)
|
||||
sch.fuse(*r_loops)
|
||||
return is_inner_reduction
|
||||
@@ -0,0 +1,29 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""Base infra"""
|
||||
|
||||
from .common_schedules import try_inline, try_inline_contiguous_spatial
|
||||
from .schedule_rule import ScheduleRule
|
||||
from .transform import ApplyDefaultSchedule
|
||||
from .utils import (
|
||||
auto_vectorize,
|
||||
get_bytes,
|
||||
get_extent,
|
||||
max_threads_per_block,
|
||||
suggest_threads_per_block,
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
# ruff: noqa: E722
|
||||
"""Common schedule strategies for TIR."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from tvm import s_tir
|
||||
|
||||
from ..analysis import SBlockInfo
|
||||
|
||||
|
||||
def try_inline(
|
||||
sch: s_tir.Schedule,
|
||||
blocks: list[SBlockInfo],
|
||||
) -> list[SBlockInfo]:
|
||||
"""Try to inline as many blocks as possible, and return the remaining blocks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sch : s_tir.Schedule
|
||||
The TIR schedule used to inline blocks.
|
||||
blocks : List[SBlockInfo]
|
||||
The blocks to be inlined.
|
||||
|
||||
Returns
|
||||
-------
|
||||
remaining : List[SBlockInfo]
|
||||
The remaining blocks that cannot be inlined.
|
||||
"""
|
||||
|
||||
def _trial(func: Callable):
|
||||
for i, block in enumerate(blocks):
|
||||
try:
|
||||
func(block.block_rv)
|
||||
except: # pylint: disable=bare-except
|
||||
continue
|
||||
return i
|
||||
return None
|
||||
|
||||
while True:
|
||||
i = _trial(sch.compute_inline)
|
||||
if i is None:
|
||||
i = _trial(sch.reverse_compute_inline)
|
||||
if i is None:
|
||||
break
|
||||
blocks.pop(i)
|
||||
return blocks
|
||||
|
||||
|
||||
def try_inline_contiguous_spatial(
|
||||
sch: s_tir.Schedule,
|
||||
block_infos: list[SBlockInfo],
|
||||
) -> list[SBlockInfo]:
|
||||
"""Try to inline contiguous spatial blocks in a schedule
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sch : s_tir.Schedule
|
||||
The TIR schedule used to inline blocks.
|
||||
block_infos : List[SBlockInfo]
|
||||
The blocks to be try.
|
||||
|
||||
Returns
|
||||
-------
|
||||
remaining : List[SBlockInfo]
|
||||
The remaining blocks that cannot be inlined.
|
||||
"""
|
||||
|
||||
if block_infos is None:
|
||||
return None
|
||||
results = []
|
||||
spatial_blocks = []
|
||||
block: SBlockInfo
|
||||
for block in block_infos:
|
||||
if block.is_injective():
|
||||
spatial_blocks.append(block)
|
||||
elif spatial_blocks:
|
||||
results.extend(try_inline(sch, spatial_blocks))
|
||||
results.append(block)
|
||||
spatial_blocks = []
|
||||
else:
|
||||
results.append(block)
|
||||
if spatial_blocks:
|
||||
results.extend(try_inline(sch, spatial_blocks))
|
||||
return results
|
||||
@@ -0,0 +1,121 @@
|
||||
# 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.
|
||||
"""A lightweight wrapper on an arbitrary function that can be used to schedule a TIR PrimFunc."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
|
||||
class ScheduleRule: # pylint: disable=too-few-public-methods
|
||||
"""A thin wrapper on an arbitrary function that can be used to schedule a TIR PrimFunc.
|
||||
|
||||
Given a PrimFunc, a target, and a tunable flag, the apply method of a ScheduleRule
|
||||
returns either a Schedule, a list of Schedules, or None, where None means that the rule
|
||||
is not applicable to the given PrimFunc. If the tunable flag is True, the ScheduleRule is
|
||||
allowed to return either a Schedule or a list of Schedules, and the Schedules are allowed to
|
||||
contain tunable instructions. If the tunable flag is False, the ScheduleRule is only allowed to
|
||||
return a Schedule, and the Schedule is not allowed to contain tunable instructions.
|
||||
"""
|
||||
|
||||
def apply(
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
tunable: bool,
|
||||
) -> None | s_tir.Schedule | list[s_tir.Schedule]:
|
||||
"""Apply the ScheduleRule to the given PrimFunc.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : tirx.PrimFunc
|
||||
The PrimFunc to apply the ScheduleRule to.
|
||||
target : Target
|
||||
The compilation target the schedule is supposed to be built for.
|
||||
tunable : bool
|
||||
Whether the schedule is allowed to contain tunable instructions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
results : Union[None, s_tir.Schedule, List[s_tir.Schedule]]
|
||||
Either a Schedule, a list of Schedules, or None, where None means that the rule
|
||||
is not applicable to the given PrimFunc.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def from_callable(
|
||||
name,
|
||||
) -> Callable[
|
||||
[
|
||||
Callable[
|
||||
[tirx.PrimFunc, Target, bool],
|
||||
None | s_tir.Schedule | list[s_tir.Schedule],
|
||||
],
|
||||
],
|
||||
"ScheduleRule",
|
||||
]:
|
||||
"""Create a ScheduleRule from a callable.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
|
||||
Returns
|
||||
-------
|
||||
decorator : Callable
|
||||
A decorator that takes a callable and returns a ScheduleRule.
|
||||
|
||||
Examples
|
||||
--------
|
||||
.. code-block:: python
|
||||
|
||||
@ScheduleRule.from_callable("MyRule")
|
||||
def my_rule(func: tirx.PrimFunc, target: Target, tunable: bool) -> Union[None, Schedule]
|
||||
# Do something with func and target
|
||||
"""
|
||||
|
||||
def decorator(f) -> "ScheduleRule": # pylint: disable=invalid-name
|
||||
class _Rule(ScheduleRule):
|
||||
def apply(
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
tunable: bool,
|
||||
) -> None | s_tir.Schedule | list[s_tir.Schedule]:
|
||||
return f(func, target, tunable)
|
||||
|
||||
_Rule.__name__ = name
|
||||
return _Rule()
|
||||
|
||||
return decorator
|
||||
|
||||
def is_target_available(self, target: Target) -> bool: # pylint: disable=unused-argument
|
||||
"""Check whether the rule is available for the given target.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target : Target
|
||||
The compilation target the schedule is supposed to be built for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
available : bool
|
||||
Whether the rule is available for the given target.
|
||||
"""
|
||||
return True
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.
|
||||
"""
|
||||
Apply ScheduleRules onto an IRModule to generate default schedules without tuning,
|
||||
or a space for MetaSchedule tuning
|
||||
"""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.ir import IRModule
|
||||
from tvm.ir.transform import PassContext, module_pass
|
||||
from tvm.target import Target
|
||||
|
||||
from .schedule_rule import ScheduleRule
|
||||
|
||||
|
||||
def _is_scheduled(func: tirx.PrimFunc) -> bool:
|
||||
if not isinstance(func, tirx.PrimFunc):
|
||||
return False
|
||||
if "tirx.is_scheduled" not in func.attrs:
|
||||
return False
|
||||
return func.attrs["tirx.is_scheduled"] == 1
|
||||
|
||||
|
||||
def _get_target(func: tirx.PrimFunc) -> Target:
|
||||
target = func.attrs.get("target")
|
||||
if target is None:
|
||||
return Target.current(allow_none=False)
|
||||
else:
|
||||
return target
|
||||
|
||||
|
||||
@module_pass(opt_level=0, name="ApplyDefaultSchedule")
|
||||
class ApplyDefaultSchedule: # pylint: disable=too-few-public-methods
|
||||
"""A IRModule pass that applies a list of ScheduleRules to all PrimFuncs in the module."""
|
||||
|
||||
def __init__(self, *rules: ScheduleRule):
|
||||
"""Construct a new ApplyDefaultSchedule pass.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*rules : ScheduleRule
|
||||
The ScheduleRules to apply to all PrimFuncs in the module.
|
||||
"""
|
||||
self.rules = list(rules)
|
||||
|
||||
def transform_module( # pylint: disable=missing-function-docstring
|
||||
self,
|
||||
mod: IRModule,
|
||||
_: PassContext,
|
||||
) -> IRModule:
|
||||
updated_functions = {}
|
||||
for g_var, func in mod.functions_items():
|
||||
if isinstance(func, tirx.PrimFunc) and not _is_scheduled(func):
|
||||
target = _get_target(func)
|
||||
|
||||
sch = _apply_rules(func, target, self.rules, tunable=False)
|
||||
if sch is not None:
|
||||
assert len(sch) == 1
|
||||
updated_functions[g_var] = (
|
||||
sch[0].mod["main"].with_attr("tirx.is_scheduled", True)
|
||||
)
|
||||
for g_var, func in updated_functions.items():
|
||||
mod[g_var] = func
|
||||
return mod
|
||||
|
||||
|
||||
def _apply_rules(
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
rules: list[ScheduleRule],
|
||||
tunable: bool,
|
||||
) -> list[s_tir.Schedule] | None:
|
||||
for rule in rules:
|
||||
space = rule.apply(func, target, tunable)
|
||||
if space is None:
|
||||
continue
|
||||
if isinstance(space, s_tir.Schedule):
|
||||
space = [space]
|
||||
return space
|
||||
return None
|
||||
@@ -0,0 +1,115 @@
|
||||
# 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
|
||||
"""Utility methods for generic GPU."""
|
||||
|
||||
from tvm import DataType, s_tir, tirx
|
||||
from tvm.ir import PrimType
|
||||
from tvm.target import Target
|
||||
|
||||
|
||||
def get_bytes(dtype: DataType | PrimType | str) -> int:
|
||||
if isinstance(dtype, PrimType):
|
||||
dtype = dtype.dtype
|
||||
if isinstance(dtype, str):
|
||||
dtype = DataType(dtype)
|
||||
return dtype.itemsize
|
||||
|
||||
|
||||
def get_extent(sch: s_tir.Schedule, loop_rv: s_tir.schedule.LoopRV):
|
||||
loop: tirx.For = sch.get(loop_rv)
|
||||
return loop.extent.value if isinstance(loop.extent, tirx.IntImm) else loop.extent
|
||||
|
||||
|
||||
def auto_vectorize(sch: s_tir.Schedule, loop: s_tir.schedule.LoopRV, max_vec: int):
|
||||
"""Auto vectorize the loop."""
|
||||
extent = get_extent(sch, loop)
|
||||
if not isinstance(extent, int):
|
||||
return
|
||||
v = loop if extent <= max_vec else sch.split(loop, factors=[None, max_vec])[-1]
|
||||
sch.vectorize(v)
|
||||
|
||||
|
||||
def max_threads_per_block(target: Target) -> int:
|
||||
"""Get the maximum number of threads per block for a given target.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target : Target
|
||||
The target to get the maximum number of threads per block for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
max_threads_per_block : int
|
||||
The maximum number of threads per block for the given target.
|
||||
"""
|
||||
for name in ["max_threads_per_block", "max_num_threads"]:
|
||||
result = target.attrs.get(name, None)
|
||||
if result is not None:
|
||||
return result
|
||||
if target.kind.name == "cuda":
|
||||
return 1024
|
||||
return 256
|
||||
|
||||
|
||||
def suggest_threads_per_block(
|
||||
target: Target,
|
||||
loops: list[tirx.For],
|
||||
max_threads_for_dynamic_loop: int = 32,
|
||||
) -> list[int]:
|
||||
if target.kind.name == "cuda":
|
||||
threads = 1024
|
||||
elif target.kind.name == "rocm":
|
||||
threads = 256
|
||||
elif target.kind.name == "metal":
|
||||
threads = 256
|
||||
elif target.kind.name == "opencl":
|
||||
threads = 256
|
||||
else:
|
||||
threads = 64
|
||||
results: list[int | None] = []
|
||||
dynamic: list[int] = []
|
||||
for i, loop in enumerate(loops):
|
||||
loop_extent = loop.extent
|
||||
if isinstance(loop_extent, tirx.IntImm):
|
||||
loop_extent = loop_extent.value
|
||||
extent = 1
|
||||
while extent <= loop_extent and extent <= threads:
|
||||
extent *= 2
|
||||
extent //= 2
|
||||
assert extent >= 1
|
||||
assert threads % extent == 0
|
||||
threads //= extent
|
||||
results.append(extent)
|
||||
else:
|
||||
results.append(None)
|
||||
dynamic.append(i)
|
||||
|
||||
for i in dynamic:
|
||||
extent = 1
|
||||
while extent <= max_threads_for_dynamic_loop and extent <= threads:
|
||||
extent *= 2
|
||||
extent //= 2
|
||||
assert extent >= 1
|
||||
assert threads % extent == 0
|
||||
threads //= extent
|
||||
results[i] = extent
|
||||
|
||||
if dynamic:
|
||||
results[dynamic[0]] *= threads
|
||||
|
||||
return results
|
||||
@@ -0,0 +1,26 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""Benchmarking dynamic shape workloads"""
|
||||
|
||||
from .bench import benchmark, benchmark_prim_func, benchmark_relax_func
|
||||
from .extract import (
|
||||
extract_prim_func,
|
||||
extract_from_relax,
|
||||
extract_func_info_from_prim_func,
|
||||
extract_all_func_info_from_relax,
|
||||
)
|
||||
@@ -0,0 +1,313 @@
|
||||
# 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.
|
||||
"""Extract self-contained benchmarking scripts for dynamic shape workloads"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import tvm
|
||||
from tvm import relax
|
||||
from tvm.ir import IRModule
|
||||
from tvm.s_tir.meta_schedule.runner import EvaluatorConfig
|
||||
from tvm.s_tir.meta_schedule.testing.tune_utils import generate_input_data
|
||||
from tvm.tirx import PrimFunc
|
||||
|
||||
from .extract import extract_all_func_info_from_relax, extract_func_info_from_prim_func
|
||||
from .utils import (
|
||||
default_dym_var_sample_func,
|
||||
dym_var_sample_str,
|
||||
get_func_name_from_gv,
|
||||
populuate_input_shape,
|
||||
print_results,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm.s_tir.meta_schedule.runner import RPCConfig
|
||||
|
||||
|
||||
def benchmark(
|
||||
mod_or_func: PrimFunc | IRModule,
|
||||
*,
|
||||
dym_var_sample: dict[str, int],
|
||||
args: list[relax.TensorType | tuple[tuple[int | str, ...], str]] | None,
|
||||
target: str | tvm.target.Target | None = None,
|
||||
func_name: str | None = None,
|
||||
evaluator_config: Optional["EvaluatorConfig"] = None,
|
||||
rpc_config: Optional["RPCConfig"] = None,
|
||||
) -> tuple[list[tuple[tuple[int, ...], str]], float, float]:
|
||||
"""Benchmark a PrimFunc or IRModule with dynamic input shapes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mod_or_func : Union[PrimFunc, IRModule]
|
||||
The PrimFunc or IRModule to be benchmarked.
|
||||
dym_var_sample : Optional[Dict[str, int]]
|
||||
The dynamic shape variable sample, e.g., {"n": 64, "m": 128}.
|
||||
args : Optional[List[Union[relax.TensorType, Tuple[Tuple[Union[int, str], ...], str]]]]
|
||||
The input tensor information, including shape and dtype. If none, will use
|
||||
the input information from the PrimFunc or IRModule.
|
||||
target : Optional[Union[str, tvm.target.Target]]
|
||||
The target to be benchmarked on, if none, will get the target from context.
|
||||
func_name : Optional[str]
|
||||
The name of the function to be benchmarked, will use "main" by default.
|
||||
evaluator_config : Optional["EvaluatorConfig"]
|
||||
The evaluator configuration to use.
|
||||
If none, will use default evaluator configuration.
|
||||
rpc_config : Optional["RPCConfig"]
|
||||
The RPC configuration to connect to the remote device.
|
||||
If none, will use local mode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
input_infos : List[Tuple[Tuple[int, ...], str]]
|
||||
The input tensor information, including shape and dtype.
|
||||
median : float
|
||||
The median of the benchmarking results.
|
||||
std : float
|
||||
The standard deviation of the benchmarking results.
|
||||
"""
|
||||
# produce IRModule and function name
|
||||
if isinstance(mod_or_func, PrimFunc):
|
||||
func_name = "main" if func_name is None else func_name
|
||||
mod = IRModule.from_expr(mod_or_func.with_attr("global_symbol", func_name))
|
||||
else:
|
||||
mod = mod_or_func
|
||||
# assume only one global function
|
||||
(func_name,) = mod.get_global_vars()
|
||||
func_name = func_name.name_hint
|
||||
# produce input shapes
|
||||
if args is None:
|
||||
args, _ = extract_func_info_from_prim_func(mod[func_name])
|
||||
# produce target & device
|
||||
target = tvm.target.Target.current() if target is None else tvm.target.Target(target)
|
||||
if target is None:
|
||||
raise ValueError("Target is not specified")
|
||||
if target.kind.name == "llvm":
|
||||
dev = tvm.cpu()
|
||||
elif target.kind.name == "cuda":
|
||||
dev = tvm.cuda()
|
||||
else:
|
||||
raise ValueError(f"Unsupported device type from {target.kind.name}")
|
||||
# populate input shapes
|
||||
input_infos = populuate_input_shape(args, dym_var_sample)
|
||||
# generate input tensors, including scalars
|
||||
# scalars are appended to the end of the list due to parsing order
|
||||
input_tensors: list[tvm.runtime.Tensor | int] = []
|
||||
scalar_input_tensors: list[int] = []
|
||||
for input_shape, input_dtype in input_infos:
|
||||
if input_dtype == "scalar":
|
||||
# special case like [n], generate int value
|
||||
assert len(input_shape) == 1
|
||||
scalar_input_tensors.append(input_shape[0])
|
||||
else:
|
||||
# normal case like [1, n, 128], generate random tensor
|
||||
input_tensors.append(
|
||||
tvm.runtime.tensor(generate_input_data(list(input_shape), input_dtype), device=dev)
|
||||
)
|
||||
# append scalar input tensors for rotary embedding
|
||||
input_tensors.extend(scalar_input_tensors)
|
||||
# build locally
|
||||
rt_mod = tvm.tirx.build(mod, target=target)
|
||||
# set up evaluator config
|
||||
evaluator_config = EvaluatorConfig._normalized( # pylint: disable=protected-access
|
||||
evaluator_config
|
||||
)
|
||||
# run benchmark
|
||||
if rpc_config is None:
|
||||
profile_result = rt_mod.time_evaluator(
|
||||
func_name,
|
||||
dev=dev,
|
||||
number=evaluator_config.number,
|
||||
repeat=evaluator_config.repeat,
|
||||
min_repeat_ms=evaluator_config.min_repeat_ms,
|
||||
f_preproc=(
|
||||
"cache_flush_cpu_non_first_arg" if evaluator_config.enable_cpu_cache_flush else ""
|
||||
),
|
||||
)(*input_tensors)
|
||||
else:
|
||||
from tvm.testing import rpc_run # pylint: disable=import-outside-toplevel
|
||||
|
||||
_, profile_result = rpc_run(
|
||||
rt_mod,
|
||||
device_type=dev._DEVICE_TYPE_TO_NAME[dev.dlpack_device_type()],
|
||||
args=[w.numpy() if isinstance(w, tvm.runtime.Tensor) else w for w in input_tensors],
|
||||
rpc_config=rpc_config,
|
||||
evaluator_config=evaluator_config,
|
||||
)
|
||||
# return input infos, median, std
|
||||
return input_infos, profile_result.median, profile_result.std
|
||||
|
||||
|
||||
def benchmark_prim_func(
|
||||
mod_or_func: PrimFunc | IRModule,
|
||||
*,
|
||||
dym_var_sample_func: Callable[[dict[str, str]], dict[str, int]] = default_dym_var_sample_func,
|
||||
args: list[relax.TensorType | tuple[tuple[int | str, ...], str]] | None = None,
|
||||
dym_var_dict: dict[str, str] | None = None,
|
||||
sample_number: int = 5,
|
||||
target: str | tvm.target.Target | None = None,
|
||||
weight: int | None = 1,
|
||||
relax_func_name: str | None = None,
|
||||
prim_func_name: str | None = None,
|
||||
evaluator_config: Optional["EvaluatorConfig"] = None,
|
||||
rpc_config: Optional["RPCConfig"] = None,
|
||||
sort_by: str | None = None,
|
||||
desc: bool | None = True,
|
||||
):
|
||||
"""Benchmark a PrimFunc or IRModule with dynamic input shapes and show results.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mod_or_func : Union[PrimFunc, IRModule]
|
||||
The PrimFunc or IRModule to be benchmarked.
|
||||
dym_var_sample_func : Callable[[Dict[str, str]], Dict[str, int]]
|
||||
The function to sample dynamic shape variables.
|
||||
dym_var_dict : Optional[Dict[str, str]]
|
||||
Dynamic shape variable dictionary, e.g., {"n": "int32", "m": "int32"}. If none, will use
|
||||
the input information from the PrimFunc or IRModule.
|
||||
args : Optional[List[Union[relax.TensorType, Tuple[Tuple[Union[int, str], ...], str]]]]
|
||||
The input tensor information, including shape and dtype. If none, will use
|
||||
the input information from the PrimFunc or IRModule.
|
||||
sample_number : int
|
||||
The number of times to sample dynamic shape variables.
|
||||
target: Optional[Union[str, tvm.target.Target]]
|
||||
The target to be benchmarked on, if none, will get the target from context.
|
||||
weight : Optional[int]
|
||||
The weight of this PrimFunc.
|
||||
relax_func_name : Optional[str]
|
||||
The name of the relax function.
|
||||
prim_func_name : Optional[str]
|
||||
The name of the PrimFunc.
|
||||
evaluator_config : Optional["EvaluatorConfig"]
|
||||
The evaluator configuration to use.
|
||||
If none, will use default evaluator configuration.
|
||||
rpc_config : Optional["RPCConfig"]
|
||||
The RPC configuration to connect to the remote device.
|
||||
If none, will use local mode.
|
||||
sort_by : Optional[str]
|
||||
Sort results by this key, if None, no sorting.
|
||||
desc : Optional[bool]
|
||||
Whether to sort results in descending order.
|
||||
"""
|
||||
results = []
|
||||
if dym_var_dict is None or args is None:
|
||||
args, dym_var_dict = extract_func_info_from_prim_func(mod_or_func)
|
||||
for _ in range(sample_number):
|
||||
dym_var_sample = dym_var_sample_func(dym_var_dict)
|
||||
_, median, std = benchmark(
|
||||
mod_or_func,
|
||||
args=args,
|
||||
dym_var_sample=dym_var_sample,
|
||||
target=target,
|
||||
evaluator_config=evaluator_config,
|
||||
rpc_config=rpc_config,
|
||||
)
|
||||
row = {
|
||||
"InputInfo": ", ".join([f"{k} = {v}" for k, v in dym_var_sample.items()]),
|
||||
"Time(us)": median * 1e6,
|
||||
"Std(us)": std * 1e6,
|
||||
}
|
||||
if relax_func_name is not None:
|
||||
row["RelaxFunc"] = relax_func_name
|
||||
if prim_func_name is not None:
|
||||
row["PrimFunc"] = prim_func_name
|
||||
weight = 1 if weight is None else weight
|
||||
row["Weight"] = weight
|
||||
row["WxTime(ms)"] = weight * median * 1e3
|
||||
results.append(row)
|
||||
print_results(results, sort_by=sort_by, desc=desc)
|
||||
|
||||
|
||||
def benchmark_relax_func(
|
||||
mod: tvm.ir.IRModule,
|
||||
relax_func: tvm.ir.GlobalVar | str,
|
||||
sample_number: int = 2,
|
||||
dym_var_sample_func: Callable[
|
||||
[dict[str, str]],
|
||||
dict[str, int],
|
||||
] = default_dym_var_sample_func,
|
||||
target: str | dict | tvm.target.Target = None,
|
||||
evaluator_config: Optional["EvaluatorConfig"] = None,
|
||||
rpc_config: Optional["RPCConfig"] = None,
|
||||
) -> None:
|
||||
"""Benchmark a relax function with dynamic input shapes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mod : tvm.ir.IRModule
|
||||
The IRModule to be benchmarked.
|
||||
relax_func : Union[tvm.ir.GlobalVar, str]
|
||||
The relax function to be benchmarked.
|
||||
sample_number : int
|
||||
The number of times to sample dynamic shape variables.
|
||||
dym_var_sample_func : Callable[[Dict[str, str]], Dict[str, int]]
|
||||
The function to sample dynamic shape variables.
|
||||
target : Union[str, tvm.target.Target]
|
||||
The target to be benchmarked on.
|
||||
dev : tvm.runtime.Device
|
||||
The device to be benchmarked on.
|
||||
evaluator_config : Optional["EvaluatorConfig"]
|
||||
The evaluator configuration to use.
|
||||
If none, will use default evaluator configuration.
|
||||
rpc_config : Optional["RPCConfig"]
|
||||
The RPC configuration to connect to the remote device.
|
||||
"""
|
||||
if target is None:
|
||||
target = {"kind": "llvm", "num-cores": 4}
|
||||
# extract function information
|
||||
relax_funcs, dynamic_var_dict = extract_all_func_info_from_relax(mod)
|
||||
# find the relax function global var
|
||||
if isinstance(relax_func, str):
|
||||
for gv in relax_funcs: # pylint: disable=invalid-name
|
||||
if get_func_name_from_gv(gv) == relax_func:
|
||||
relax_func = gv
|
||||
break
|
||||
if not isinstance(relax_func, tvm.ir.GlobalVar):
|
||||
raise ValueError(
|
||||
f"Cannot find relax function with name {relax_func}, "
|
||||
+ f"candidates are: {[get_func_name_from_gv(gv) for gv in relax_funcs]}"
|
||||
)
|
||||
# benchmark
|
||||
for _ in range(sample_number):
|
||||
dym_var_sample = dym_var_sample_func(dynamic_var_dict[relax_func])
|
||||
bench_results = []
|
||||
# enumerate all functors
|
||||
for functor in relax_funcs[relax_func]:
|
||||
for args, weight in relax_funcs[relax_func][functor]:
|
||||
_, median, _ = benchmark(
|
||||
mod[functor],
|
||||
args=args,
|
||||
dym_var_sample=dym_var_sample,
|
||||
target=target,
|
||||
evaluator_config=evaluator_config,
|
||||
rpc_config=rpc_config,
|
||||
)
|
||||
bench_results.append(
|
||||
{
|
||||
f"PrimFuncs in {get_func_name_from_gv(relax_func)}": get_func_name_from_gv(
|
||||
functor
|
||||
),
|
||||
f"InputInfo({dym_var_sample_str(dym_var_sample)})": ", ".join(
|
||||
[str(w) for w in args]
|
||||
),
|
||||
"Time(us)": median * 1e6,
|
||||
# "Std(us)": std * 1e6,
|
||||
"Weight": weight,
|
||||
"WxTime(ms)": median * weight * 1e3,
|
||||
}
|
||||
)
|
||||
print_results(bench_results)
|
||||
@@ -0,0 +1,355 @@
|
||||
# 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.
|
||||
"""Performance debug tool for dynamic shape workloads"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import cloudpickle
|
||||
import tvm_ffi
|
||||
|
||||
import tvm
|
||||
from tvm import relax
|
||||
|
||||
from .utils import default_dym_var_sample_func, get_func_name_from_gv
|
||||
|
||||
SKETCH = """import pickle
|
||||
|
||||
import tvm
|
||||
from tvm import relax
|
||||
from tvm.script import tirx as T
|
||||
|
||||
from tvm.s_tir.dlight.benchmark import benchmark_prim_func
|
||||
|
||||
MODEL_NAME = "{model_name}"
|
||||
RELAX_FUNC_NAME = "{relax_func_name}"
|
||||
PRIM_FUNC_NAME = "{prim_func_name}"
|
||||
FUNC_HASH = {func_hash}
|
||||
WEIGHT = {weight}
|
||||
SAMPLE_NUMBER = {sample_number}
|
||||
|
||||
DYM_VAR_SAMPLE_FUNC = {dym_var_sample_func}
|
||||
|
||||
# None means extract from PrimFunc
|
||||
INPUT_ARGS = {input_args}
|
||||
DYM_VAR_DICT = {dym_var_dict}
|
||||
|
||||
{func_script}
|
||||
|
||||
if __name__ == "__main__":
|
||||
target = tvm.target.Target({target})
|
||||
benchmark_prim_func(
|
||||
main,
|
||||
args = INPUT_ARGS,
|
||||
dym_var_dict = DYM_VAR_DICT,
|
||||
dym_var_sample_func = DYM_VAR_SAMPLE_FUNC,
|
||||
sample_number = SAMPLE_NUMBER,
|
||||
target = target,
|
||||
weight = WEIGHT,
|
||||
relax_func_name = RELAX_FUNC_NAME,
|
||||
prim_func_name = PRIM_FUNC_NAME,
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def extract_shape(
|
||||
arg: tuple | list | relax.Tuple | relax.ShapeType,
|
||||
) -> list[relax.ShapeType]:
|
||||
"""Extract shape information from a relax argument.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg : Union[Tuple, List, relax.Tuple, relax.ShapeType]
|
||||
The relax argument to be extracted.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : List[relax.ShapeType]
|
||||
The extracted shape information.
|
||||
"""
|
||||
if isinstance(arg, tuple | list | tvm.relax.Tuple):
|
||||
results = []
|
||||
for sub_arg in arg:
|
||||
results.extend(extract_shape(sub_arg))
|
||||
return results
|
||||
return [arg.ty]
|
||||
|
||||
|
||||
def extract_dynamic_var(
|
||||
func_dict: dict[
|
||||
tvm.ir.GlobalVar,
|
||||
dict[
|
||||
tvm.ir.GlobalVar,
|
||||
list[tuple[list, int]],
|
||||
],
|
||||
],
|
||||
) -> dict[tvm.ir.GlobalVar, dict[str, str]]:
|
||||
"""Extract dynamic shape variables from a relax function dictionary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func_dict : Dict[
|
||||
tvm.ir.GlobalVar,
|
||||
Dict[
|
||||
tvm.ir.GlobalVar,
|
||||
List[Tuple[List, int]],
|
||||
],
|
||||
The relax function dictionary, containing the input arguments' shape information of each
|
||||
PrimFunc in a Relax function.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : Dict[tvm.ir.GlobalVar, Dict[str, str]]
|
||||
The dictionary of dynamic shape variables. Given in format {"n": "int32", "m": "int32"}.
|
||||
"""
|
||||
dym_var_dict: dict[tvm.ir.GlobalVar, dict[str, str]] = {}
|
||||
for gv in func_dict: # pylint: disable=invalid-name,too-many-nested-blocks
|
||||
dym_var_dict[gv] = {}
|
||||
for functor in func_dict[gv]:
|
||||
for arg_list, _ in func_dict[gv][functor]:
|
||||
flattened_arg_list = []
|
||||
for arg in arg_list:
|
||||
if isinstance(arg, relax.TupleType):
|
||||
flattened_arg_list.extend(arg.fields)
|
||||
else:
|
||||
flattened_arg_list.append(arg)
|
||||
for arg in flattened_arg_list:
|
||||
if isinstance(arg, relax.TensorType):
|
||||
for val in arg.shape.values:
|
||||
if isinstance(val, tvm.tirx.Var):
|
||||
dym_var_dict[gv][str(val)] = str(val.ty)
|
||||
elif isinstance(arg, relax.ShapeType):
|
||||
for val in arg.values:
|
||||
if isinstance(val, tvm.tirx.Var):
|
||||
dym_var_dict[gv][str(val)] = str(val.ty)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return dym_var_dict
|
||||
|
||||
|
||||
def update_records(
|
||||
records: dict[list[relax.ShapeType], int], new_args: list[relax.ShapeType]
|
||||
) -> None:
|
||||
"""Update the count of a function input argument config.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
records : Dict[List[relax.ShapeType], int]
|
||||
The dictionary to count how many times a function input argument config appears.
|
||||
new_args : List[relax.ShapeType]
|
||||
The new input argument config.
|
||||
"""
|
||||
for i, (args, count) in enumerate(records):
|
||||
if new_args == args:
|
||||
records[i] = (args, count + 1)
|
||||
return
|
||||
records.append((new_args, 1))
|
||||
|
||||
|
||||
def extract_func_info_from_prim_func(
|
||||
func: tvm.tirx.PrimFunc,
|
||||
) -> tuple[list[tuple[tuple[tvm.tirx.Var | int, ...], str]], dict[str, str]]:
|
||||
"""Extract function input information from a PrimFunc.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : tvm.tirx.PrimFunc
|
||||
The PrimFunc to be analyzed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : Tuple[
|
||||
List[Tuple[Tuple[Union[tvm.tirx.Var, int], ...], str]],
|
||||
Dict[str, str],
|
||||
]
|
||||
The function input information and dynamic shape variable dictionary.
|
||||
"""
|
||||
func_args = []
|
||||
dym_var = {}
|
||||
for param in func.params:
|
||||
buffer = func.buffer_map[param]
|
||||
shape = []
|
||||
for dim in buffer.shape:
|
||||
if isinstance(dim, tvm.tirx.IntImm):
|
||||
shape.append(dim.value)
|
||||
elif isinstance(dim, tvm.tirx.Var):
|
||||
dym_var[str(dim)] = str(dim.ty)
|
||||
shape.append(dim)
|
||||
else:
|
||||
raise ValueError(f"Unknown shape: {buffer.shape}")
|
||||
func_args.append((tuple(shape), str(buffer.dtype)))
|
||||
return func_args, dym_var
|
||||
|
||||
|
||||
def extract_all_func_info_from_relax(
|
||||
mod: tvm.ir.IRModule,
|
||||
) -> tuple[
|
||||
dict[tvm.ir.GlobalVar, dict[tvm.ir.GlobalVar, list[tuple[list, int]]]],
|
||||
dict[tvm.ir.GlobalVar, dict[str, str]],
|
||||
]:
|
||||
"""Extract function input information from a relax module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mod : tvm.ir.IRModule
|
||||
The Relax module to be analyzed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : Tuple[
|
||||
Dict[tvm.ir.GlobalVar, Dict[tvm.ir.GlobalVar, List[Tuple[List, int]]]],
|
||||
Dict[tvm.ir.GlobalVar, Dict[str, str]],
|
||||
]
|
||||
The function input information and dynamic shape variable dictionary.
|
||||
"""
|
||||
relax_func_dict: dict[tvm.ir.GlobalVar, dict[tvm.ir.GlobalVar, list[tuple[list, int]]]] = {}
|
||||
for gv, func in mod.functions_items(): # pylint: disable=invalid-name,too-many-nested-blocks
|
||||
if isinstance(func, tvm.relax.Function):
|
||||
for block in func.body.blocks:
|
||||
for binding in block.bindings:
|
||||
if isinstance(binding.value, tvm.ir.Call):
|
||||
raw_args = binding.value.args
|
||||
functor = raw_args[0]
|
||||
if isinstance(functor, tvm.ir.GlobalVar) and isinstance(
|
||||
mod.functions[functor], tvm.tirx.PrimFunc
|
||||
):
|
||||
args = extract_shape(raw_args[1:]) + extract_shape(binding.value)
|
||||
if isinstance(functor, tvm.ir.GlobalVar):
|
||||
if gv not in relax_func_dict:
|
||||
relax_func_dict[gv] = {}
|
||||
if functor not in relax_func_dict[gv]:
|
||||
relax_func_dict[gv][functor] = []
|
||||
update_records(relax_func_dict[gv][functor], args)
|
||||
|
||||
return relax_func_dict, extract_dynamic_var(relax_func_dict)
|
||||
|
||||
|
||||
def extract_prim_func( # pylint: disable=too-many-arguments
|
||||
model_name: str,
|
||||
relax_func_name: str,
|
||||
prim_func_name: str,
|
||||
func: tvm.tirx.PrimFunc,
|
||||
*,
|
||||
func_args: list[tuple[tuple[tvm.ir.Call | int, ...], str]] | None = None,
|
||||
dym_var_dict: dict[str, str] | None = None,
|
||||
weight: int = 1,
|
||||
sample_number: int = 5,
|
||||
target: str | dict | tvm.target.Target | None = None,
|
||||
) -> str:
|
||||
"""Extract a self-contained PrimFunc test file from a Relax module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model_name: str
|
||||
The name of the model.
|
||||
relax_func_name: str
|
||||
The name of the Relax function.
|
||||
prim_func_name: str
|
||||
The name of the prim function.
|
||||
func: tvm.tirx.PrimFunc
|
||||
The PrimFunc to be extracted.
|
||||
func_args: Optional[List[Tuple[Tuple[Union[tvm.ir.Call, int], ...], str]]]
|
||||
The arguments of the prim function, including both static and dynamic shape arguments.
|
||||
Given in format [ ..., ((1, n, 128), "float32"), ... ].
|
||||
If not given, the arguments will be extracted from the PrimFunc.
|
||||
dym_var_dict: Optional[Dict[str, str]]
|
||||
The dictionary of dynamic shape variables. Given in format {"n": "int32", "m": "int32"}.
|
||||
If not given, the dictionary will be extracted from the PrimFunc.
|
||||
weight: int
|
||||
The weight of the prim function, by default 1.
|
||||
sample_number: int
|
||||
The number of times to sample dynamic shape variables, by default 5.
|
||||
target: Optional[Union[str, dict, tvm.target.Target]]
|
||||
The target device to run the PrimFunc. If None, will use target from the context.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : str
|
||||
The extracted PrimFunc test file content.
|
||||
"""
|
||||
if target is None:
|
||||
target = tvm.target.Target.current()
|
||||
if target is None:
|
||||
raise ValueError("Target is not specified.")
|
||||
elif isinstance(target, str | dict):
|
||||
target = tvm.target.Target(target)
|
||||
elif not isinstance(target, tvm.target.Target):
|
||||
raise TypeError("Unsupported target type: " + str(type(target)))
|
||||
target_json = str(target)
|
||||
|
||||
return SKETCH.format(
|
||||
**{
|
||||
"model_name": model_name,
|
||||
"relax_func_name": relax_func_name,
|
||||
"prim_func_name": prim_func_name,
|
||||
"func_hash": tvm_ffi.structural_hash(func),
|
||||
"weight": weight,
|
||||
"sample_number": sample_number,
|
||||
"dym_var_dict": f"pickle.loads({cloudpickle.dumps(dym_var_dict)})"
|
||||
if dym_var_dict is not None
|
||||
else "None",
|
||||
"input_args": f"pickle.loads({cloudpickle.dumps(func_args)})" if func_args else "None",
|
||||
"dym_var_sample_func": "pickle.loads("
|
||||
+ f"{cloudpickle.dumps(default_dym_var_sample_func)}"
|
||||
+ ")",
|
||||
"func_script": func.script(),
|
||||
"target": target_json,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def extract_from_relax(
|
||||
mod: tvm.ir.IRModule,
|
||||
model_name: str,
|
||||
file_path: str,
|
||||
target: str | dict | tvm.target.Target | None = None,
|
||||
) -> None:
|
||||
"""Extract self-contained PrimFunc test files from a Relax module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mod: tvm.ir.IRModule
|
||||
The Relax module to be extracted.
|
||||
model_name: str
|
||||
The name of the model.
|
||||
file_path: str
|
||||
The path to store the extracted files.
|
||||
target: Optional[Union[str, tvm.target.Target]]
|
||||
The target device to run the PrimFunc. If None, will use target from the context.
|
||||
"""
|
||||
relax_funcs, dym_var_dict = extract_all_func_info_from_relax(mod)
|
||||
Path(file_path).mkdir(parents=True, exist_ok=True)
|
||||
for relax_func_gv in relax_funcs: # pylint: disable=consider-using-dict-items
|
||||
relax_func_name = get_func_name_from_gv(relax_func_gv)
|
||||
for prim_func_gv in relax_funcs[relax_func_gv]:
|
||||
prim_func_name = get_func_name_from_gv(prim_func_gv)
|
||||
for func_args, weight in relax_funcs[relax_func_gv][prim_func_gv]:
|
||||
with open(
|
||||
f"{file_path}/{relax_func_name}_{prim_func_name}.py", "w", encoding="utf-8"
|
||||
) as file:
|
||||
print(
|
||||
extract_prim_func(
|
||||
model_name=model_name,
|
||||
relax_func_name=relax_func_name,
|
||||
prim_func_name=prim_func_name,
|
||||
func=mod[prim_func_gv],
|
||||
dym_var_dict=dym_var_dict[relax_func_gv],
|
||||
func_args=func_args,
|
||||
weight=weight,
|
||||
target=target,
|
||||
),
|
||||
file=file,
|
||||
)
|
||||
@@ -0,0 +1,178 @@
|
||||
# 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.
|
||||
"""Util functions for benchmarking dynamic shape workloads"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import tvm
|
||||
from tvm import relax
|
||||
|
||||
INPUT_SHAPE_TYPE = list[tuple[tuple[int, ...], str]] # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def _dtype_str(dtype) -> str:
|
||||
if isinstance(dtype, tvm.ir.PrimType):
|
||||
dtype = dtype.dtype
|
||||
return str(dtype)
|
||||
|
||||
|
||||
def get_func_name_from_gv(gv: tvm.ir.GlobalVar) -> str: # pylint: disable=invalid-name
|
||||
"""Get function name from a global variable.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
gv : tvm.ir.GlobalVar
|
||||
The given global variable.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : str
|
||||
The global variable name without the prefix "...@".
|
||||
"""
|
||||
return gv.name_hint
|
||||
|
||||
|
||||
def dym_var_sample_str(sample: dict[str | tvm.ir.Call, int]) -> str:
|
||||
"""Convert a variable value sample to a string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sample : Dict[Union[str, tvm.ir.Call], int]
|
||||
Variable value sample, e.g., {n: 64, m: 128} or {"n": 64, "m": 128}
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : str
|
||||
Variable value sample string, e.g., "n=64, m=128"
|
||||
"""
|
||||
return ", ".join([f"{k}={v}" for k, v in sample.items()])
|
||||
|
||||
|
||||
def populuate_input_shape(
|
||||
input_infos: list[relax.TensorType | tuple[tuple[int | str, ...], str]],
|
||||
dym_var_sample: dict[str, int],
|
||||
) -> INPUT_SHAPE_TYPE:
|
||||
"""
|
||||
Populate input shapes with dynamic shape variable samples.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_infos : List[Union[relax.TensorType, Tuple[Tuple[Union[int, str], ...], str]]]
|
||||
Input tensor information, including shape and dtype,
|
||||
e.g., [..., Shape(1, n, 128) with dtype="int32", ...]
|
||||
dym_var_sample : Dict[str, int]
|
||||
Dynamic shape variable sample, e.g., {"n": 64}
|
||||
|
||||
Returns
|
||||
-------
|
||||
results : INPUT_SHAPE_TYPE
|
||||
Input shapes with dynamic shape variable samples, e.g.,
|
||||
[..., ((1, 64, 128), "int32"), ...] if n=64 or
|
||||
[..., (128, "scalar"), ...] if n=128 for scalar input
|
||||
"""
|
||||
results: INPUT_SHAPE_TYPE = []
|
||||
for input_info in input_infos:
|
||||
shape = []
|
||||
if isinstance(input_info, relax.ShapeType):
|
||||
# scalar input
|
||||
results.append(((dym_var_sample[str(input_info.values[0])],), "scalar"))
|
||||
else:
|
||||
if isinstance(input_info, relax.TensorType):
|
||||
tensor_shape = input_info.shape
|
||||
tensor_dtype = input_info.dtype
|
||||
else:
|
||||
tensor_shape, tensor_dtype = input_info # type: ignore
|
||||
for dim in tensor_shape:
|
||||
if isinstance(dim, int):
|
||||
shape.append(dim)
|
||||
elif isinstance(dim, tvm.tirx.IntImm):
|
||||
shape.append(dim.value)
|
||||
else:
|
||||
shape.append(dym_var_sample[str(dim)])
|
||||
results.append(((*shape,), _dtype_str(tensor_dtype)))
|
||||
return results
|
||||
|
||||
|
||||
def default_dym_var_sample_func(dym_var_dict: dict[str, str]) -> dict[str, int]:
|
||||
"""
|
||||
Default dynamic shape variable sample function.
|
||||
Sample a random value for each dynamic shape variable.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dym_var_dict : Dict[str, str]
|
||||
Dynamic shape variable dictionary, e.g., {"n": "int32", "m": "int32"}
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : Dict[str, int]
|
||||
Dynamic shape variable sample, e.g., {"n": 64, "m": 128}
|
||||
"""
|
||||
results = {}
|
||||
for var in dym_var_dict:
|
||||
if dym_var_dict[var] in ["int32", "int64"]:
|
||||
import random # pylint: disable=import-outside-toplevel
|
||||
|
||||
results[var] = random.randint(2, 128)
|
||||
else:
|
||||
raise TypeError("Unsupported dynamic shape variable type: " + dym_var_dict[var])
|
||||
return results
|
||||
|
||||
|
||||
def print_results(
|
||||
bench_results: list[dict[str, Any]], sort_by: str = "WxTime(ms)", desc: bool = True
|
||||
):
|
||||
"""Print benchmark results.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bench_results : List[Dict[str, Any]]
|
||||
Benchmark results as dictionary list.
|
||||
sort_by : str
|
||||
Sort results by this key, if None, no sorting.
|
||||
desc : bool
|
||||
Whether to sort results in descending order.
|
||||
"""
|
||||
# pylint: disable=invalid-name, import-outside-toplevel
|
||||
try:
|
||||
import pandas as pd
|
||||
|
||||
df = pd.DataFrame()
|
||||
for record in bench_results:
|
||||
df = pd.concat(
|
||||
[df, pd.DataFrame(record, index=[0])],
|
||||
ignore_index=True,
|
||||
)
|
||||
if sort_by is not None:
|
||||
if sort_by not in df.columns:
|
||||
raise ValueError(f"sort_by key {sort_by} not in benchmark results")
|
||||
df = df.sort_values(sort_by, ascending=not desc).reset_index().drop("index", axis=1)
|
||||
print(df)
|
||||
except ModuleNotFoundError:
|
||||
print("Pandas not found, printing results in raw format.")
|
||||
keys = []
|
||||
if len(bench_results) > 0:
|
||||
for key in bench_results[0]:
|
||||
keys.append(str(key))
|
||||
print("\t".join(keys))
|
||||
for record in bench_results:
|
||||
values = []
|
||||
for key in keys:
|
||||
values.append(str(record[key]))
|
||||
print("\t".join(values))
|
||||
print("\n")
|
||||
# pylint: enable=invalid-name, import-outside-toplevel
|
||||
@@ -0,0 +1,23 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""
|
||||
CPU-generic schedule rules.
|
||||
"""
|
||||
|
||||
from .gemv import GEMV
|
||||
from .reduction import Reduction
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
"""Base schedule rule for CPU operators."""
|
||||
|
||||
from tvm.target import Target
|
||||
|
||||
from ..base import ScheduleRule
|
||||
|
||||
|
||||
class CPUScheduleRule(ScheduleRule): # pylint: disable=too-few-public-methods
|
||||
"""The Schedule Rule specific to CPU targets, will return None if the target is not CPU."""
|
||||
|
||||
def is_target_available(self, target: Target) -> bool:
|
||||
"""Check whether the target is available for gpu rule.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target : Target
|
||||
The compilation target to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
available : bool
|
||||
Whether the target is available for this rule.
|
||||
"""
|
||||
return super().is_target_available(target) and "llvm" == target.kind.name
|
||||
@@ -0,0 +1,131 @@
|
||||
# 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.
|
||||
"""A rule for GEMV and DecodeGEMV."""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from ..analysis import SBlockInfo, normalize_prim_func
|
||||
from ..analysis.gemv import is_gemv, normalize
|
||||
from ..base import get_extent, try_inline_contiguous_spatial
|
||||
from .base import CPUScheduleRule
|
||||
|
||||
|
||||
class GEMV(CPUScheduleRule):
|
||||
"""A rule for GEMV and DecodeGEMV."""
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,too-many-branches,too-many-return-statements, no-else-return
|
||||
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
|
||||
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:
|
||||
return None
|
||||
if len(block_infos) == 1:
|
||||
epilogue = None
|
||||
elif len(block_infos) == 2:
|
||||
epilogue = block_infos[1]
|
||||
if not epilogue.is_injective():
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
block_info = block_infos[0]
|
||||
if len(block_info.iters) not in [2, 3]:
|
||||
# either [B, S, R] = [B, S, R] * [B, R]
|
||||
# or [S, R] = [S, R] * [R]
|
||||
return None
|
||||
block = block_info.block_rv
|
||||
vector_input_buffers = is_gemv(sch, block_info)
|
||||
if vector_input_buffers is None:
|
||||
return None
|
||||
|
||||
# Step 1. Normalize the block, merge spatial and reduction iters
|
||||
is_inner_reduction = normalize(sch, block_info)
|
||||
|
||||
# Step 2. Do the scheduling
|
||||
if is_inner_reduction is None:
|
||||
return None
|
||||
elif is_inner_reduction:
|
||||
return self.sch_inner_reduction(sch, target, block, vector_input_buffers, epilogue)
|
||||
else:
|
||||
# sch_outer reduction
|
||||
return None
|
||||
|
||||
def sch_inner_reduction( # pylint: disable=too-many-arguments, too-many-positional-arguments, invalid-name, unused-argument
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
vector_input_buffers: list[tirx.Buffer],
|
||||
epilogue_info: SBlockInfo | None,
|
||||
):
|
||||
"""Schedule the inner reduction block."""
|
||||
|
||||
def apply( # pylint: disable=unused-variable, too-many-locals
|
||||
sch: s_tir.Schedule,
|
||||
gemv,
|
||||
vector_width: int = 8,
|
||||
parallel_threads: int = 8,
|
||||
unroll_factor: int = 256,
|
||||
):
|
||||
batch, s, r, c = sch.get_loops(block)
|
||||
len_batch, len_s, len_r, len_c = (
|
||||
get_extent(sch, batch),
|
||||
get_extent(sch, s),
|
||||
get_extent(sch, r),
|
||||
get_extent(sch, c),
|
||||
)
|
||||
len_S = len_batch * len_s
|
||||
len_R = len_r * len_c
|
||||
|
||||
if isinstance(len_S, int) and isinstance(len_R, int):
|
||||
if len_S > len_R:
|
||||
tile_s, tile_r = 128, 64 # Larger tiling for s-axis when len_S is larger
|
||||
else:
|
||||
tile_s, tile_r = 64, 128 # Larger tiling for r-axis when len_R is larger
|
||||
else:
|
||||
tile_s, tile_r = 64, 64 # Default tile sizes for unknown extents
|
||||
|
||||
tile_c = min(vector_width, len_c) # Ensure c-axis tiling aligns with SIMD vector width
|
||||
|
||||
# Apply loop tiling (improves cache locality)
|
||||
s_outer, s_inner = sch.split(s, factors=[None, tile_s])
|
||||
r_outer, r_inner = sch.split(r, factors=[None, tile_r])
|
||||
c_outer, c_inner = sch.split(c, factors=[None, tile_c])
|
||||
|
||||
# Apply vectorization (SIMD optimization)
|
||||
sch.vectorize(s_inner) # Vectorize computation along c-axis for AVX/NEON
|
||||
|
||||
# Enable parallel execution
|
||||
sch.parallel(s_outer) # Parallelize along the s-axis (major computation loop)
|
||||
|
||||
# Apply loop unrolling for better CPU performance
|
||||
sch.annotate(r_outer, "pragma_auto_unroll_max_step", unroll_factor)
|
||||
sch.annotate(r_outer, "pragma_unroll_explicit", 1)
|
||||
return sch
|
||||
|
||||
return apply(
|
||||
sch,
|
||||
gemv=block,
|
||||
)
|
||||
@@ -0,0 +1,153 @@
|
||||
# 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.
|
||||
"""CPU reduction rule for operators including softmax, layer norm, RMS norm, etc."""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
from tvm.target.codegen import llvm_get_vector_width
|
||||
|
||||
from ..analysis import normalize_prim_func
|
||||
from ..base import get_extent
|
||||
from .base import CPUScheduleRule
|
||||
|
||||
|
||||
def _get_num_leading_s(dom_kind: str) -> int:
|
||||
"""Count leading spatial ('S') axes in a dom_kind string."""
|
||||
return len(dom_kind) - len(dom_kind.lstrip("S"))
|
||||
|
||||
|
||||
class Reduction(CPUScheduleRule):
|
||||
"""CPU reduction rule for softmax, layer norm, RMS norm, and similar operators.
|
||||
|
||||
Targets patterns with a mix of reduction (SR) and injective (SS) blocks,
|
||||
where all blocks share the same leading spatial axes.
|
||||
Example: softmax = maxelem(SR) -> exp(SS) -> expsum(SR) -> norm(SS).
|
||||
|
||||
Schedule strategy:
|
||||
1. Parallelize leading spatial axes (batch dimension).
|
||||
2. Move all blocks under the spatial loop via compute_at.
|
||||
3. Vectorize injective blocks (exp, delta, norm) on their inner axis.
|
||||
4. Split reduction inner axis to VLEN-sized chunks and annotate for
|
||||
LLVM unrolling, preventing harmful full-unroll by the backend.
|
||||
|
||||
Note: vectorized reduction via rfactor is not used here because TVM's
|
||||
rfactor primitive requires the reduction block to be the first child of
|
||||
its enclosing loop, which is incompatible with compute_at when multiple
|
||||
blocks share the same spatial loop. A follow-up using RVV reduction
|
||||
intrinsics (vfredmax/vfredusum) via tensorize can address this.
|
||||
"""
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,too-many-return-statements,too-many-branches
|
||||
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
|
||||
|
||||
sch = s_tir.Schedule(func)
|
||||
block_infos = normalize_prim_func(sch)
|
||||
if block_infos is None or len(block_infos) < 2:
|
||||
return None
|
||||
|
||||
# Must have at least one reduction block and last block must be injective.
|
||||
if not any(not bi.is_injective() for bi in block_infos):
|
||||
return None
|
||||
if not block_infos[-1].is_injective():
|
||||
return None
|
||||
|
||||
# Every block must start with at least one spatial axis, and all blocks
|
||||
# must agree on the minimum number of leading spatial axes.
|
||||
num_leading_s = None
|
||||
for bi in block_infos:
|
||||
dk = bi.dom_kind()
|
||||
if not dk or dk[0] != "S":
|
||||
return None
|
||||
n = _get_num_leading_s(dk)
|
||||
num_leading_s = n if num_leading_s is None else min(num_leading_s, n)
|
||||
if not num_leading_s:
|
||||
return None
|
||||
|
||||
# Infer dtype from the last block's write buffer.
|
||||
last_block_stmt = sch.get(block_infos[-1].block_rv)
|
||||
dtype_bits = (
|
||||
last_block_stmt.writes[0].buffer.dtype.dtype.bits if last_block_stmt.writes else 32
|
||||
)
|
||||
|
||||
# Determine vector lanes from target VLEN.
|
||||
vlen_bits = llvm_get_vector_width(target)
|
||||
if vlen_bits <= 0:
|
||||
vlen_bits = 128
|
||||
vec_lanes = max(vlen_bits // dtype_bits, 2)
|
||||
|
||||
# --- Phase 1: Parallelize spatial on the last block ---
|
||||
last_block = block_infos[-1]
|
||||
loops = sch.get_loops(last_block.block_rv)
|
||||
if num_leading_s > 1:
|
||||
spatial = sch.fuse(*loops[:num_leading_s])
|
||||
else:
|
||||
spatial = loops[0]
|
||||
sch.parallel(spatial)
|
||||
|
||||
# --- Phase 2: Vectorize the last (injective) block ---
|
||||
self._vectorize_inner(sch, last_block.block_rv, vec_lanes)
|
||||
|
||||
# --- Phase 3: compute_at all preceding blocks under spatial ---
|
||||
for block_info in reversed(block_infos[:-1]):
|
||||
sch.compute_at(block_info.block_rv, spatial, preserve_unit_loops=True)
|
||||
|
||||
# --- Phase 4: Vectorize injective, split+unroll reduction blocks ---
|
||||
for block_info in block_infos[:-1]:
|
||||
if block_info.is_injective():
|
||||
self._vectorize_inner(sch, block_info.block_rv, vec_lanes)
|
||||
else:
|
||||
self._unroll_reduction_inner(sch, block_info.block_rv, vec_lanes)
|
||||
|
||||
return sch
|
||||
|
||||
@staticmethod
|
||||
def _vectorize_inner(sch, block_rv, vec_lanes):
|
||||
"""Split the innermost loop to vec_lanes and vectorize."""
|
||||
block_loops = sch.get_loops(block_rv)
|
||||
if len(block_loops) <= 1:
|
||||
return
|
||||
inner = block_loops[-1]
|
||||
extent = get_extent(sch, inner)
|
||||
if isinstance(extent, int):
|
||||
if extent > vec_lanes:
|
||||
_, vec_loop = sch.split(inner, factors=[None, vec_lanes])
|
||||
sch.vectorize(vec_loop)
|
||||
elif extent >= 2:
|
||||
sch.vectorize(inner)
|
||||
else:
|
||||
_, vec_loop = sch.split(inner, factors=[None, vec_lanes])
|
||||
sch.vectorize(vec_loop)
|
||||
|
||||
@staticmethod
|
||||
def _unroll_reduction_inner(sch, block_rv, vec_lanes):
|
||||
"""Split the reduction inner loop and annotate for unrolling."""
|
||||
block_loops = sch.get_loops(block_rv)
|
||||
if len(block_loops) <= 1:
|
||||
return
|
||||
inner = block_loops[-1]
|
||||
extent = get_extent(sch, inner)
|
||||
if isinstance(extent, int) and extent <= vec_lanes:
|
||||
return
|
||||
_, inner_loop = sch.split(inner, factors=[None, vec_lanes])
|
||||
sch.annotate(inner_loop, ann_key="pragma_auto_unroll_max_step", ann_val=vec_lanes)
|
||||
sch.annotate(inner_loop, ann_key="pragma_unroll_explicit", ann_val=1)
|
||||
@@ -0,0 +1,30 @@
|
||||
# isort: skip_file
|
||||
# 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.
|
||||
"""
|
||||
GPU-generic schedule rules.
|
||||
For CUDA/ROCm/Vulkan/Metal-specific rules, use `tvm.s_tir.dlight.cuda/rocm/vulkan/metal` instead
|
||||
"""
|
||||
|
||||
from .gemv import GEMV
|
||||
from .low_batch_gemv import LowBatchGEMV
|
||||
from .fallback import Fallback
|
||||
from .matmul import Matmul
|
||||
from .reduction import Reduction
|
||||
from .transpose import Transpose
|
||||
from .general_reduction import GeneralReduction
|
||||
from .rmsnorm import RMSNorm
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
"""Base schedule rule for GPU operators."""
|
||||
|
||||
from tvm.target import Target
|
||||
|
||||
from ..base import ScheduleRule
|
||||
|
||||
|
||||
class GPUScheduleRule(ScheduleRule): # pylint: disable=too-few-public-methods
|
||||
"""The Schedule Rule specific to GPU targets, will return None if the target is not GPU."""
|
||||
|
||||
def is_target_available(self, target: Target) -> bool:
|
||||
"""Check whether the target is available for gpu rule.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target : Target
|
||||
The compilation target to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
available : bool
|
||||
Whether the target is available for this rule.
|
||||
"""
|
||||
return super().is_target_available(target) and "gpu" in target.keys
|
||||
@@ -0,0 +1,107 @@
|
||||
# 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
|
||||
"""A fallback schedule rule for GPU operators."""
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from .. import base
|
||||
from ..analysis import normalize_prim_func
|
||||
from ..base import try_inline
|
||||
from .base import GPUScheduleRule
|
||||
|
||||
|
||||
def _has_internal_thread_env(stmt: tirx.Stmt) -> bool:
|
||||
"""Check whether a statement already launches GPU threads internally,
|
||||
e.g. via `T.launch_thread` (AttrStmt "thread_extent") or nested
|
||||
thread-bound loops. Such blocks manage their own thread environment
|
||||
and must not be wrapped in an additional thread binding."""
|
||||
found = False
|
||||
|
||||
def _visit(node):
|
||||
nonlocal found
|
||||
if isinstance(node, tirx.AttrStmt) and node.attr_key in ("thread_extent", "virtual_thread"):
|
||||
found = True
|
||||
elif isinstance(node, tirx.For) and node.kind == tirx.ForKind.THREAD_BINDING:
|
||||
found = True
|
||||
|
||||
tirx.stmt_functor.post_order_visit(stmt, _visit)
|
||||
return found
|
||||
|
||||
|
||||
class Fallback(GPUScheduleRule):
|
||||
"""
|
||||
A fallback schedule rule for all GPU operators. It will try to inline all the blocks first,
|
||||
and then apply a simple block/grid mapping to the spatial loops on top of the remaining blocks.
|
||||
"""
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,missing-docstring
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> s_tir.Schedule:
|
||||
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
|
||||
return None
|
||||
max_threads_per_block = base.max_threads_per_block(target)
|
||||
|
||||
sch = s_tir.Schedule(func)
|
||||
block_infos = normalize_prim_func(sch)
|
||||
|
||||
if block_infos is None:
|
||||
return None
|
||||
|
||||
block_infos = try_inline(sch, block_infos)
|
||||
reduction_blocks: list[tuple[s_tir.schedule.SBlockRV, s_tir.schedule.LoopRV]] = []
|
||||
for block in block_infos:
|
||||
s_loops: list[s_tir.schedule.LoopRV] = []
|
||||
r_loops: list[s_tir.schedule.LoopRV] = []
|
||||
o_loops: list[s_tir.schedule.LoopRV] = []
|
||||
dom_kind = block.dom_kind()
|
||||
block = block.block_rv
|
||||
|
||||
if any(
|
||||
[sch.get(loop_rv).thread_binding is not None for loop_rv in sch.get_loops(block)]
|
||||
):
|
||||
continue
|
||||
|
||||
if len(sch.get_loops(block)) == 0 and _has_internal_thread_env(sch.get(block).body):
|
||||
# The block (e.g. an opaque sort kernel) launches its own
|
||||
# threads; binding an outer loop would conflict with them.
|
||||
continue
|
||||
|
||||
for loop, iter_type in zip(sch.get_loops(block), dom_kind):
|
||||
{"S": s_loops, "R": r_loops, "O": o_loops}[iter_type].append(loop)
|
||||
|
||||
if not s_loops:
|
||||
s_loops.append(sch.add_unit_loop(block))
|
||||
sch.reorder(*s_loops, *r_loops, *o_loops)
|
||||
bx, tx = sch.split( # pylint: disable=invalid-name
|
||||
sch.fuse(*s_loops),
|
||||
factors=[None, max_threads_per_block],
|
||||
)
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
|
||||
if len(r_loops) > 0:
|
||||
reduction_blocks.append((block, r_loops[0]))
|
||||
|
||||
for block, r_loop in reduction_blocks:
|
||||
sch.decompose_reduction(block, r_loop)
|
||||
|
||||
return sch
|
||||
@@ -0,0 +1,689 @@
|
||||
# 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.
|
||||
# ruff: noqa: E741, F821
|
||||
"""A rule for GEMV and DecodeGEMV."""
|
||||
|
||||
from functools import reduce
|
||||
|
||||
from tvm import s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from ..analysis import (
|
||||
SBlockInfo,
|
||||
get_max_shared_memory_per_block,
|
||||
is_broadcast_epilogue,
|
||||
is_gemv,
|
||||
normalize,
|
||||
normalize_prim_func,
|
||||
)
|
||||
from ..base import auto_vectorize, get_bytes, get_extent, try_inline_contiguous_spatial
|
||||
from .base import GPUScheduleRule
|
||||
|
||||
|
||||
class GEMV(GPUScheduleRule):
|
||||
"""A rule for GEMV and DecodeGEMV."""
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,too-many-branches,too-many-return-statements
|
||||
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
|
||||
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:
|
||||
return None
|
||||
if len(block_infos) == 1:
|
||||
epilogue = None
|
||||
elif len(block_infos) == 2:
|
||||
epilogue = block_infos[1]
|
||||
if not epilogue.is_injective():
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
block_info = block_infos[0]
|
||||
if len(block_info.iters) not in [2, 3]:
|
||||
# either [B, S, R] = [B, S, R] * [B, R]
|
||||
# or [S, R] = [S, R] * [R]
|
||||
return None
|
||||
block = block_info.block_rv
|
||||
vector_input_buffers = is_gemv(sch, block_info)
|
||||
if vector_input_buffers is None:
|
||||
return None
|
||||
|
||||
# Step 1. Normalize the block, merge spatial and reduction iters
|
||||
is_inner_reduction = normalize(sch, block_info)
|
||||
|
||||
# Step 2. Do the scheduling
|
||||
if is_inner_reduction is None:
|
||||
return None
|
||||
elif is_inner_reduction:
|
||||
return self.sch_inner_reduction(sch, target, block, vector_input_buffers, epilogue)
|
||||
else:
|
||||
ret = self.sch_outer_reduction(sch, target, block, vector_input_buffers, epilogue)
|
||||
if ret is None:
|
||||
return self.sch_outer_reduction_fallback(
|
||||
sch, target, block, vector_input_buffers, epilogue
|
||||
)
|
||||
return sch
|
||||
|
||||
def sch_inner_reduction( # pylint: disable=too-many-arguments, invalid-name, unused-argument
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
vector_input_buffers: list[tirx.Buffer],
|
||||
epilogue_info: SBlockInfo | None,
|
||||
):
|
||||
"""Schedule the inner reduction block."""
|
||||
|
||||
def get_max_factor(n, factors):
|
||||
factors = sorted(factors, reverse=True)
|
||||
for factor in factors:
|
||||
if n % factor == 0:
|
||||
return factor
|
||||
return 1
|
||||
|
||||
def apply(
|
||||
sch: s_tir.Schedule,
|
||||
gemv,
|
||||
TAG_S,
|
||||
TAG_R,
|
||||
TS,
|
||||
TR,
|
||||
TILE_S,
|
||||
TILE_R,
|
||||
VEC_LOAD,
|
||||
VEC_C,
|
||||
LOAD_V_SHARED,
|
||||
LOAD_V_VEC,
|
||||
UNROLL,
|
||||
SUPPORT_WARP_SHUFFLE,
|
||||
):
|
||||
# rfactor: reduce to tx * vec_c
|
||||
_, s, r, c = sch.get_loops(block=gemv)
|
||||
s = sch.fuse(_, s)
|
||||
r = sch.fuse(r, c)
|
||||
bx, ts, tile_s = sch.split(s, factors=[None, TS, TILE_S], preserve_unit_iters=True)
|
||||
r, tr, tile_r_vec_n, vec_c = sch.split(
|
||||
r, factors=[None, TR, TILE_R // VEC_C, VEC_C], preserve_unit_iters=True
|
||||
)
|
||||
sch.reorder(r, tile_r_vec_n, tr, vec_c)
|
||||
tr_vec_c = sch.fuse(tr, vec_c)
|
||||
rf = sch.rfactor(tr_vec_c, 0)
|
||||
|
||||
# rfactor: reduce to tx
|
||||
bx, ts, tile_s, tr_vec_c = sch.get_loops(block=gemv)
|
||||
tr, vec_c = sch.split(tr_vec_c, factors=[TR, None], preserve_unit_iters=True)
|
||||
rf2 = sch.rfactor(tr, 0)
|
||||
|
||||
# bind, vectorize compute
|
||||
bx, ts, tile_s, r, tile_r_vec_n, tr_vec_c = sch.get_loops(block=rf)
|
||||
tr, vec_c = sch.split(tr_vec_c, factors=[TR, None], preserve_unit_iters=True)
|
||||
sch.reorder(bx, ts, tr, r, tile_s, tile_r_vec_n, vec_c)
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
sch.vectorize(vec_c)
|
||||
|
||||
shared_mem_usage = 0
|
||||
for buf in vector_input_buffers:
|
||||
dtype_bytes = get_bytes(buf.dtype)
|
||||
buf_size = (
|
||||
reduce(lambda x, y: x * y, buf.shape, tirx.IntImm(buf.shape[0].ty, 1))
|
||||
* dtype_bytes
|
||||
)
|
||||
shared_mem_usage += buf_size
|
||||
if not SUPPORT_WARP_SHUFFLE:
|
||||
# When warp shuffle is not able, cross-thread allreduce
|
||||
# is implemented with shared memory.
|
||||
shared_mem_usage += TS * TR * dtype_bytes
|
||||
|
||||
max_smem = get_max_shared_memory_per_block(target)
|
||||
LOAD_V_SHARED = (
|
||||
LOAD_V_SHARED
|
||||
and isinstance(shared_mem_usage, tirx.IntImm)
|
||||
and shared_mem_usage.value <= max_smem
|
||||
)
|
||||
|
||||
# vectorize load A
|
||||
# (TODO) this is now actually problematic since the number of loops is dependent on the
|
||||
# number of dimensions of A_q
|
||||
Aq_local = sch.cache_read(rf, read_buffer_index=1, storage_scope="local")
|
||||
sch.compute_at(Aq_local, r, preserve_unit_loops=True)
|
||||
s_local, r_local = sch.get_loops(block=Aq_local)[-2:]
|
||||
fused_load = sch.fuse(s_local, r_local)
|
||||
aq_vec_len = max(1, VEC_LOAD // get_bytes(sch.get(Aq_local).reads[0].buffer.dtype))
|
||||
fused_load, vec_load = sch.split(
|
||||
fused_load, factors=[None, aq_vec_len], preserve_unit_iters=True
|
||||
)
|
||||
sch.vectorize(vec_load)
|
||||
|
||||
# load vector into shared memory, shape should be the whole vector
|
||||
if LOAD_V_SHARED:
|
||||
if len(vector_input_buffers) != 1:
|
||||
return None
|
||||
V_shared = sch.cache_read(rf, read_buffer_index=0, storage_scope="shared")
|
||||
sch.compute_at(V_shared, tr, preserve_unit_loops=True)
|
||||
l = sch.get_loops(block=V_shared)[-1]
|
||||
loop: tirx.For = sch.get(l)
|
||||
if isinstance(loop.extent, tirx.IntImm):
|
||||
# avoid introducing predicates when vector length is too large
|
||||
vec_length = max(
|
||||
min(
|
||||
get_max_factor(
|
||||
(int)(loop.extent),
|
||||
[TS * TR * 1, TS * TR * 2, TS * TR * 4, TS * TR * 8],
|
||||
)
|
||||
// TS
|
||||
// TR,
|
||||
LOAD_V_VEC,
|
||||
),
|
||||
1,
|
||||
)
|
||||
else:
|
||||
vec_length = LOAD_V_VEC
|
||||
if TAG_R == "threadIdx.x":
|
||||
_, ty, tx, vec = sch.split(
|
||||
l, factors=[None, TS, TR, vec_length], preserve_unit_iters=True
|
||||
)
|
||||
else:
|
||||
_, ty, tx, vec = sch.split(
|
||||
l, factors=[None, TR, TS, vec_length], preserve_unit_iters=True
|
||||
)
|
||||
sch.bind(ty, "threadIdx.y")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.vectorize(vec)
|
||||
|
||||
# reduce tile_s * tr * vec to tile_s * tr
|
||||
sch.reverse_compute_at(rf2, loop=bx, preserve_unit_loops=True)
|
||||
tr, vec_c, *ts_tile_s = sch.get_loops(block=rf2)[1:]
|
||||
ts_tile_s = sch.fuse(*ts_tile_s)
|
||||
ts_o, ts_i, tile_s = sch.split(
|
||||
ts_tile_s, factors=[None, TS, TILE_S], preserve_unit_iters=True
|
||||
)
|
||||
tile_s, vec_s = sch.split(
|
||||
tile_s,
|
||||
factors=[None, get_max_factor(TILE_S, [1, 2, 4, 8])],
|
||||
preserve_unit_iters=True,
|
||||
)
|
||||
assert sch.get(ts_o).extent.value == 1
|
||||
ts = sch.fuse(ts_o, ts_i)
|
||||
sch.reorder(ts, tr, tile_s, vec_s, vec_c)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
sch.vectorize(vec_s)
|
||||
|
||||
# reduce tile_s * tr to tile_s
|
||||
sch.reverse_compute_at(gemv, loop=bx, preserve_unit_loops=True)
|
||||
tr, *ts_tile_s = sch.get_loops(block=gemv)[1:]
|
||||
ts_tile_s = sch.fuse(*ts_tile_s)
|
||||
ts_o, ts_i, tile_s = sch.split(
|
||||
ts_tile_s, factors=[None, TS, TILE_S], preserve_unit_iters=True
|
||||
)
|
||||
assert sch.get(ts_o).extent.value == 1
|
||||
ts = sch.fuse(ts_o, ts_i)
|
||||
sch.reorder(tile_s, ts, tr)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
|
||||
sch.decompose_reduction(rf, loop=sch.get_loops(block=rf)[3])
|
||||
sch.decompose_reduction(rf2, loop=sch.get_loops(block=rf2)[-1])
|
||||
|
||||
sch.set_scope(rf, buffer_index=0, storage_scope="local")
|
||||
sch.set_scope(rf2, buffer_index=0, storage_scope="local")
|
||||
|
||||
unroll_factor = UNROLL
|
||||
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf)[3],
|
||||
ann_key="pragma_auto_unroll_max_step",
|
||||
ann_val=unroll_factor,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf)[3], ann_key="pragma_unroll_explicit", ann_val=1
|
||||
)
|
||||
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf2)[3],
|
||||
ann_key="pragma_auto_unroll_max_step",
|
||||
ann_val=unroll_factor,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf2)[3], ann_key="pragma_unroll_explicit", ann_val=1
|
||||
)
|
||||
|
||||
if LOAD_V_SHARED:
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(V_shared)[-4],
|
||||
ann_key="pragma_unroll_explicit",
|
||||
ann_val=unroll_factor,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(V_shared)[-4], ann_key="pragma_vectorize", ann_val=1
|
||||
)
|
||||
|
||||
# Schedule epilogue
|
||||
if epilogue_info is not None:
|
||||
epilogue = epilogue_info.block_rv
|
||||
if is_broadcast_epilogue(sch, block, epilogue):
|
||||
sch.reverse_compute_at(epilogue, bx)
|
||||
sch.set_scope(block, 0, "shared")
|
||||
_, _, *s = sch.get_loops(epilogue) # pylint: disable=invalid-name
|
||||
_, tx = sch.split(sch.fuse(*s), factors=[None, TX])
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
else:
|
||||
sch.reverse_compute_at(epilogue, bx, preserve_unit_loops=True)
|
||||
ts_tile_s = sch.fuse(*sch.get_loops(epilogue)[1:])
|
||||
ts_tile_s = sch.get_loops(epilogue)[-1]
|
||||
ts_o, ts_i, tile_s = sch.split(
|
||||
ts_tile_s, factors=[None, TS, TILE_S], preserve_unit_iters=True
|
||||
)
|
||||
assert sch.get(ts_o).extent.value == 1
|
||||
ts = sch.fuse(ts_o, ts_i)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.set_scope(block, 0, "local")
|
||||
# pylint: enable=invalid-name
|
||||
return sch
|
||||
|
||||
# Specify the `len_tx` and `len_ty` according to the loop extent
|
||||
batch, s, r, c = sch.get_loops(block=block)
|
||||
len_batch, len_s, len_r, len_c = (
|
||||
get_extent(sch, batch),
|
||||
get_extent(sch, s),
|
||||
get_extent(sch, r),
|
||||
get_extent(sch, c),
|
||||
)
|
||||
len_S = len_batch * len_s
|
||||
len_R = len_r * len_c
|
||||
|
||||
TAG_S, TAG_R = "threadIdx.y", "threadIdx.x"
|
||||
SUPPORT_WARP_SHUFFLE = False
|
||||
VEC_LOAD = 1
|
||||
if target.kind.name == "cuda":
|
||||
VEC_C = 4
|
||||
LOAD_V_SHARED = True
|
||||
LOAD_V_VEC = 8
|
||||
VEC_LOAD = 4
|
||||
UNROLL = 256
|
||||
SUPPORT_WARP_SHUFFLE = True
|
||||
if isinstance(len_S, int):
|
||||
TS, TR = 16, 32
|
||||
else:
|
||||
TS, TR = 1, 64
|
||||
elif target.kind.name == "metal":
|
||||
# Note that the following tile size is tuned on M2 Ultra for 7B
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
VEC_C = 1
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 256
|
||||
SUPPORT_WARP_SHUFFLE = True
|
||||
if isinstance(len_S, int):
|
||||
if len_S > len_R:
|
||||
TS, TR = 4, 16
|
||||
else:
|
||||
TS, TR = 2, 64
|
||||
else:
|
||||
TS, TR = 1, 64
|
||||
elif target.kind.name == "rocm":
|
||||
VEC_C = 4
|
||||
# TODO: set LOAD_V_SHARED = False for now
|
||||
# rocm might have some issues when load/store of shared do not belong to same data type
|
||||
# and only works for certain vector lens, our commonly useful vector lens are in 4
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = 8
|
||||
UNROLL = 256
|
||||
if isinstance(len_S, int):
|
||||
if len_S > len_R:
|
||||
TS, TR = 1, 128
|
||||
else:
|
||||
TS, TR = 8, 64
|
||||
else:
|
||||
TS, TR = 1, 64
|
||||
elif target.kind.name == "opencl" and (
|
||||
("android" in str(target.host)) or ("adreno" in str(target.attrs))
|
||||
):
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
VEC_C = 8
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 8
|
||||
TS, TR = 2, 32
|
||||
elif target.kind.name == "vulkan":
|
||||
VEC_C = 4
|
||||
LOAD_V_SHARED = True
|
||||
LOAD_V_VEC = 4
|
||||
UNROLL = 256
|
||||
if isinstance(len_S, int):
|
||||
if len_S > len_R:
|
||||
TS, TR = 4, 32
|
||||
else:
|
||||
TS, TR = 16, 32
|
||||
else:
|
||||
TS, TR = 1, 64
|
||||
elif target.kind.name == "opencl" and "mali" in str(target.attrs):
|
||||
VEC_C = 8
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 64
|
||||
TS, TR = 1, 64
|
||||
else:
|
||||
VEC_C = 1
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 64
|
||||
TS, TR = 1, 64
|
||||
|
||||
while TS * TR > int(target.attrs["max_num_threads"]):
|
||||
if TS > 1:
|
||||
TS //= 2
|
||||
else:
|
||||
TR //= 2
|
||||
|
||||
TILE_S, TILE_R = (
|
||||
1,
|
||||
(
|
||||
len_c
|
||||
if len_c > 1
|
||||
else max(get_max_factor(len_r, [TR * 1, TR * 2, TR * 4, TR * 8]) // TR, 1)
|
||||
),
|
||||
)
|
||||
VEC_C = min(get_max_factor(TILE_R, [1, 2, 4, 8]), VEC_C)
|
||||
|
||||
return apply(
|
||||
sch,
|
||||
gemv=block,
|
||||
TAG_S=TAG_S,
|
||||
TAG_R=TAG_R,
|
||||
TS=TS,
|
||||
TR=TR,
|
||||
TILE_S=TILE_S,
|
||||
TILE_R=TILE_R,
|
||||
VEC_LOAD=VEC_LOAD,
|
||||
VEC_C=VEC_C,
|
||||
LOAD_V_SHARED=LOAD_V_SHARED,
|
||||
LOAD_V_VEC=LOAD_V_VEC,
|
||||
UNROLL=UNROLL,
|
||||
SUPPORT_WARP_SHUFFLE=SUPPORT_WARP_SHUFFLE,
|
||||
)
|
||||
|
||||
def sch_outer_reduction( # pylint: disable=too-many-arguments, invalid-name, unused-argument
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
vector_input_buffers: list[tirx.Buffer],
|
||||
epilogue_info: SBlockInfo | None,
|
||||
):
|
||||
"""Schedule the outer reduction block."""
|
||||
|
||||
def get_max_factor(n, factors):
|
||||
factors = sorted(factors, reverse=True)
|
||||
for factor in factors:
|
||||
if n % factor == 0:
|
||||
return factor
|
||||
return 1
|
||||
|
||||
def apply(
|
||||
sch: s_tir.Schedule,
|
||||
gemv,
|
||||
TAG_S,
|
||||
TAG_R,
|
||||
TS,
|
||||
TR,
|
||||
SCALE_PACK,
|
||||
DEC_PACK,
|
||||
VEC_LOAD,
|
||||
VEC_C,
|
||||
LOAD_V_SHARED,
|
||||
LOAD_V_VEC,
|
||||
UNROLL,
|
||||
LOAD_V_TILE,
|
||||
):
|
||||
# rfactor: reduce to tx * vec_c
|
||||
batch, s, r, c = sch.get_loops(block=gemv)
|
||||
s = sch.fuse(batch, s)
|
||||
r = sch.fuse(r, c)
|
||||
bx, ts = sch.split(s, factors=[None, TS], preserve_unit_iters=True)
|
||||
r, v_tile, tr, tile_r, vec_c = sch.split(
|
||||
r, factors=[None, LOAD_V_TILE, TR, SCALE_PACK, DEC_PACK], preserve_unit_iters=True
|
||||
)
|
||||
sch.reorder(bx, ts, r, v_tile, tile_r, tr, vec_c)
|
||||
tr_vec_c = sch.fuse(tr, vec_c)
|
||||
rf = sch.rfactor(tr_vec_c, 0)
|
||||
|
||||
# rfactor: reduce to tx
|
||||
bx, ts, tr_vec_c = sch.get_loops(block=gemv)
|
||||
tr, vec_c = sch.split(tr_vec_c, factors=[TR, None], preserve_unit_iters=True)
|
||||
rf2 = sch.rfactor(tr, 0)
|
||||
|
||||
# bind, vectorize compute
|
||||
bx, ts, r, v_tile, tile_r, tr_vec_c = sch.get_loops(block=rf)
|
||||
tr, vec_c = sch.split(tr_vec_c, factors=[TR, DEC_PACK])
|
||||
sch.reorder(bx, ts, tr, r, v_tile, tile_r, vec_c)
|
||||
# sch.bind(batch, "blockIdx.z")
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
auto_vectorize(sch, vec_c, VEC_C)
|
||||
|
||||
# decompose independent scale read to outer loop
|
||||
block_rf_stmt = sch.get(rf)
|
||||
if len(block_rf_stmt.reads) >= 3:
|
||||
As_local = sch.cache_read(rf, read_buffer_index=2, storage_scope="local")
|
||||
sch.compute_at(As_local, v_tile, preserve_unit_loops=True)
|
||||
# *tile_thr, vec_s = sch.get_loops(block=As_local)
|
||||
# sch.vectorize(vec_s)
|
||||
|
||||
Aq_local = sch.cache_read(rf, read_buffer_index=1, storage_scope="local")
|
||||
sch.compute_at(Aq_local, tile_r, preserve_unit_loops=True)
|
||||
# *tile_thr, vec_s = sch.get_loops(block=Aq_local)
|
||||
# sch.vectorize(vec_s)
|
||||
|
||||
if LOAD_V_SHARED:
|
||||
V_shared = sch.cache_read(rf, read_buffer_index=0, storage_scope="shared")
|
||||
sch.compute_at(V_shared, r, preserve_unit_loops=True)
|
||||
l = sch.get_loops(block=V_shared)[-1]
|
||||
_, v_tile, ts, tr, vec = sch.split(
|
||||
l, factors=[None, LOAD_V_TILE, TS, TR, LOAD_V_VEC], preserve_unit_iters=True
|
||||
)
|
||||
sch.bind(tr, TAG_R)
|
||||
sch.bind(ts, TAG_S)
|
||||
auto_vectorize(sch, vec, LOAD_V_VEC)
|
||||
|
||||
# reduce tile_s * tr * vec to tile_s * tr
|
||||
sch.reverse_compute_at(rf2, loop=bx, preserve_unit_loops=True)
|
||||
tr, vec_c, ts = sch.get_loops(block=rf2)[1:]
|
||||
sch.reorder(ts, tr, vec_c)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
|
||||
# reduce tile_s * tr to tile_s
|
||||
sch.reverse_compute_at(gemv, loop=bx, preserve_unit_loops=True)
|
||||
tr, ts = sch.get_loops(block=gemv)[1:]
|
||||
sch.reorder(ts, tr)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
|
||||
sch.decompose_reduction(rf, loop=sch.get_loops(block=rf)[2])
|
||||
sch.decompose_reduction(rf2, loop=sch.get_loops(block=rf2)[-1])
|
||||
|
||||
sch.set_scope(rf, buffer_index=0, storage_scope="local")
|
||||
sch.set_scope(rf2, buffer_index=0, storage_scope="local")
|
||||
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf2)[3],
|
||||
ann_key="pragma_auto_unroll_max_step",
|
||||
ann_val=UNROLL,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf2)[3], ann_key="pragma_unroll_explicit", ann_val=1
|
||||
)
|
||||
|
||||
# Schedule epilogue
|
||||
if epilogue_info is not None:
|
||||
epilogue = epilogue_info.block_rv
|
||||
if is_broadcast_epilogue(sch, block, epilogue):
|
||||
sch.reverse_compute_at(epilogue, bx)
|
||||
sch.set_scope(block, 0, "shared")
|
||||
_, _, *s = sch.get_loops(epilogue) # pylint: disable=invalid-name
|
||||
_, ts = sch.split(sch.fuse(*s), factors=[None, TS])
|
||||
sch.bind(ts, TAG_S)
|
||||
else:
|
||||
sch.reverse_compute_at(epilogue, bx, preserve_unit_loops=True)
|
||||
ts_tile_s = sch.fuse(*sch.get_loops(epilogue)[1:])
|
||||
ts_tile_s = sch.get_loops(epilogue)[-1]
|
||||
ts, _ = sch.split(ts_tile_s, factors=[TS, None], preserve_unit_iters=True)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.set_scope(block, 0, "local")
|
||||
return sch
|
||||
|
||||
# Specify the `len_tx` and `len_ty` according to the loop extent
|
||||
batch, s, r, c = sch.get_loops(block=block)
|
||||
_, len_s, len_r, len_c = (
|
||||
get_extent(sch, batch),
|
||||
get_extent(sch, s),
|
||||
get_extent(sch, r),
|
||||
get_extent(sch, c),
|
||||
)
|
||||
|
||||
DEC_PACK = 8
|
||||
SCALE_PACK = 4
|
||||
|
||||
if target.kind.name == "opencl" and (
|
||||
("android" in str(target.host)) or ("adreno" in str(target.attrs))
|
||||
):
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
VEC_C = 8
|
||||
UNROLL = 8
|
||||
TS, TR = 64, 4
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = 4
|
||||
LOAD_V_TILE = 8
|
||||
elif target.kind.name == "metal":
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
VEC_C = 4
|
||||
UNROLL = 8
|
||||
TS, TR = 128, 4
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = 4
|
||||
LOAD_V_TILE = 4
|
||||
else:
|
||||
return None
|
||||
|
||||
if LOAD_V_SHARED is False:
|
||||
LOAD_V_TILE = 1
|
||||
|
||||
if not isinstance(len_r, int) or len_r < LOAD_V_TILE * TR * SCALE_PACK * DEC_PACK:
|
||||
return None
|
||||
|
||||
if not isinstance(len_s, int):
|
||||
TS, TR = 256, 1
|
||||
LOAD_V_SHARED = True
|
||||
|
||||
if isinstance(len_s, int) and len_s > 96000:
|
||||
return None
|
||||
|
||||
_, TILE_R = (
|
||||
1,
|
||||
(
|
||||
len_c
|
||||
if len_c > 1
|
||||
else max(get_max_factor(len_r, [TR * 1, TR * 2, TR * 4, TR * 8]) // TR, 1)
|
||||
),
|
||||
)
|
||||
LOAD_V_VEC = min(get_max_factor(TILE_R, [1, 2, 4, 8]), LOAD_V_VEC)
|
||||
VEC_LOAD = 1
|
||||
|
||||
return apply(
|
||||
sch,
|
||||
gemv=block,
|
||||
TAG_S=TAG_S,
|
||||
TAG_R=TAG_R,
|
||||
TS=TS,
|
||||
TR=TR,
|
||||
SCALE_PACK=SCALE_PACK,
|
||||
DEC_PACK=DEC_PACK,
|
||||
VEC_LOAD=VEC_LOAD,
|
||||
VEC_C=VEC_C,
|
||||
LOAD_V_SHARED=LOAD_V_SHARED,
|
||||
LOAD_V_VEC=LOAD_V_VEC,
|
||||
UNROLL=UNROLL,
|
||||
LOAD_V_TILE=LOAD_V_TILE,
|
||||
)
|
||||
|
||||
def sch_outer_reduction_fallback( # pylint: disable=too-many-arguments, invalid-name, unused-argument
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
vector_input_buffers: list[tirx.Buffer],
|
||||
epilogue_info: SBlockInfo | None,
|
||||
):
|
||||
"""Schedule the outer reduction block."""
|
||||
# NOTE: Only Android is supported so far
|
||||
if not (
|
||||
target.kind.name == "opencl"
|
||||
and (("android" in str(target.host)) or ("adreno" in str(target.attrs)))
|
||||
):
|
||||
return None
|
||||
batch, s, r, c = sch.get_loops(block)
|
||||
len_s = get_extent(sch, s)
|
||||
|
||||
# The config is designed for Adreno
|
||||
LOAD_V_SHARED = 1
|
||||
tx_len = 128
|
||||
vec_len = (4 if len_s > 4096 else 2) if isinstance(len_s, int) else 1
|
||||
inner_r = 4
|
||||
|
||||
bx, tx, vec = sch.split(s, factors=[None, tx_len, vec_len])
|
||||
r0, r1 = sch.split(r, factors=[None, inner_r])
|
||||
sch.bind(batch, "blockIdx.y")
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.reorder(bx, tx, r0, r1, c, vec)
|
||||
|
||||
sch.annotate(tx, ann_key="pragma_auto_unroll_max_step", ann_val=8)
|
||||
sch.annotate(tx, ann_key="pragma_unroll_explicit", ann_val=1)
|
||||
|
||||
if LOAD_V_SHARED:
|
||||
V_shared = sch.cache_read(block, vector_input_buffers[0], storage_scope="shared")
|
||||
sch.compute_at(V_shared, bx, preserve_unit_loops=True)
|
||||
l = sch.get_loops(block=V_shared)[-1]
|
||||
_, tx, vec_r = sch.split(l, factors=[None, tx_len, 8], preserve_unit_iters=True)
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.vectorize(vec_r)
|
||||
|
||||
sch.vectorize(vec)
|
||||
|
||||
# Schedule epilogue
|
||||
if epilogue_info is not None:
|
||||
sch.reverse_compute_at(epilogue_info.block_rv, bx, preserve_unit_loops=True)
|
||||
ts_tile_s = sch.get_loops(epilogue_info.block_rv)[-1]
|
||||
ts, vec = sch.split(ts_tile_s, factors=[tx_len, vec_len], preserve_unit_iters=True)
|
||||
sch.bind(ts, "threadIdx.x")
|
||||
sch.vectorize(vec)
|
||||
sch.set_scope(block, 0, "local")
|
||||
|
||||
sch.decompose_reduction(block, r0)
|
||||
|
||||
return sch
|
||||
@@ -0,0 +1,183 @@
|
||||
# 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
|
||||
@@ -0,0 +1,744 @@
|
||||
# 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.
|
||||
# ruff: noqa: E741, F821
|
||||
"""A rule for low-batch GEMM / decode-GEMM using GEMV schedule."""
|
||||
|
||||
from functools import reduce
|
||||
from typing import Literal
|
||||
|
||||
import tvm_ffi
|
||||
|
||||
from tvm import arith, s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from ..analysis import (
|
||||
SBlockInfo,
|
||||
collect_block_iter_vars_used_in_access_region,
|
||||
collect_vars_used_in_prim_expr,
|
||||
get_max_shared_memory_per_block,
|
||||
is_broadcast_epilogue,
|
||||
normalize_prim_func,
|
||||
)
|
||||
from ..base import auto_vectorize, get_bytes, get_extent, try_inline_contiguous_spatial
|
||||
from .base import GPUScheduleRule
|
||||
|
||||
|
||||
def _get_reduction_expr(block: tirx.SBlock) -> tirx.Expr | None:
|
||||
# Detect and return `Y` in `X[...] = X[...] + Y`
|
||||
buffer_store = block.body
|
||||
if not isinstance(buffer_store, tirx.BufferStore):
|
||||
return None
|
||||
if not isinstance(buffer_store.value, tirx.Add):
|
||||
return None
|
||||
if not tvm_ffi.structural_equal(
|
||||
buffer_store.value.a,
|
||||
tirx.BufferLoad(buffer_store.buffer, block.body.indices),
|
||||
map_free_vars=True,
|
||||
):
|
||||
return None
|
||||
return buffer_store.value.b
|
||||
|
||||
|
||||
def is_gemv(sch: s_tir.Schedule, block_info: SBlockInfo) -> list[tirx.Buffer] | None:
|
||||
"""Check if the block is a low batch GEMM.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
sch : s_tir.Schedule
|
||||
The schedule
|
||||
|
||||
block_info : SBlockInfo
|
||||
The block info to be checked
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
ret : Optional[List[tirx.Buffer]]
|
||||
The vector-like buffers used in the low batch GEMM if it is a low batch GEMM,
|
||||
otherwise None.
|
||||
"""
|
||||
block = block_info.block_rv
|
||||
block_stmt = sch.get(block)
|
||||
conditions = []
|
||||
conditions.append(block_info.is_reduction())
|
||||
conditions.append(len(block_stmt.reads) >= 2)
|
||||
conditions.append(len(block_stmt.writes) == 1)
|
||||
conditions.append(_get_reduction_expr(block_stmt) is not None)
|
||||
conditions.append(
|
||||
len(collect_block_iter_vars_used_in_access_region(block_stmt, block_stmt.writes[0].region))
|
||||
> 0
|
||||
)
|
||||
if not all(conditions):
|
||||
return None
|
||||
const_iter_vars = set(
|
||||
iter_var.var
|
||||
for iter_var in block_stmt.iter_vars
|
||||
if isinstance(iter_var.dom.extent, tirx.IntImm)
|
||||
)
|
||||
if len(block_stmt.iter_vars) - len(const_iter_vars) != 1:
|
||||
return None
|
||||
symbolic_iter_var = next(
|
||||
iter_var
|
||||
for iter_var in block_stmt.iter_vars
|
||||
if not isinstance(iter_var.dom.extent, tirx.IntImm)
|
||||
)
|
||||
if symbolic_iter_var.iter_type != tirx.stmt.IterVar.DataPar:
|
||||
return None
|
||||
ret = [
|
||||
read.buffer
|
||||
for read in block_stmt.reads
|
||||
if len(
|
||||
collect_block_iter_vars_used_in_access_region(block_stmt, read.region) & const_iter_vars
|
||||
)
|
||||
< len(const_iter_vars)
|
||||
and len(
|
||||
collect_block_iter_vars_used_in_access_region(block_stmt, read.region) & const_iter_vars
|
||||
)
|
||||
> 0
|
||||
]
|
||||
return ret if 0 < len(ret) < len(block_stmt.reads) else None
|
||||
|
||||
|
||||
def detect_dominant_read(block: tirx.SBlock, const_iter_vars: set[tirx.Var]) -> 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)
|
||||
& const_iter_vars
|
||||
)
|
||||
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 normalize(
|
||||
sch: s_tir.Schedule,
|
||||
block_info: SBlockInfo,
|
||||
) -> bool | None:
|
||||
"""Normalize the main block."""
|
||||
block_stmt: tirx.SBlock = sch.get(block_info.block_rv)
|
||||
const_iter_vars = set(
|
||||
iter_var.var
|
||||
for iter_var in block_stmt.iter_vars
|
||||
if isinstance(iter_var.dom.extent, tirx.IntImm)
|
||||
)
|
||||
dynamic_iter_vars = set(
|
||||
iter_var.var for iter_var in block_stmt.iter_vars if iter_var.var not in const_iter_vars
|
||||
)
|
||||
access = arith.normalize_to_iter_sum(
|
||||
detect_dominant_read(block_stmt, const_iter_vars),
|
||||
input_iters={i.var: i.dom for i in block_stmt.iter_vars},
|
||||
)
|
||||
buffers_use_vars = [
|
||||
collect_block_iter_vars_used_in_access_region(block_stmt, buf.region)
|
||||
for buf in block_stmt.writes
|
||||
]
|
||||
buffers_use_vars.extend(
|
||||
[
|
||||
collect_block_iter_vars_used_in_access_region(block_stmt, buf.region)
|
||||
for buf in block_stmt.reads
|
||||
]
|
||||
)
|
||||
if collect_vars_used_in_prim_expr(access.base) & set(
|
||||
iter_var.var for iter_var in block_stmt.iter_vars
|
||||
):
|
||||
return None
|
||||
iter_to_info = {i.var: i for i in block_info.iters}
|
||||
batch_loops, s_loops, r_loops = [], [], []
|
||||
inner_axis = access.args[-1].source.source
|
||||
is_inner_reduction = iter_to_info[inner_axis].kind == "R"
|
||||
|
||||
for split_expr in access.args:
|
||||
var = split_expr.source.source
|
||||
info = iter_to_info.get(var)
|
||||
loop = info.loop_rv
|
||||
is_reduction = info.kind == "R"
|
||||
# No C loops as we do not compute_inline weights into main block
|
||||
if is_reduction:
|
||||
r_loops.append(loop)
|
||||
elif all([var in buf_vars for buf_vars in buffers_use_vars]):
|
||||
batch_loops.append(loop)
|
||||
else:
|
||||
s_loops.append(loop)
|
||||
|
||||
assert s_loops
|
||||
assert r_loops
|
||||
dynamic_loops = [iter_to_info[var].loop_rv for var in dynamic_iter_vars]
|
||||
assert len(dynamic_loops) == 1
|
||||
sch.reorder(*dynamic_loops, *s_loops, *r_loops)
|
||||
sch.fuse(*s_loops)
|
||||
sch.fuse(*r_loops)
|
||||
return is_inner_reduction
|
||||
|
||||
|
||||
class LowBatchGEMV(GPUScheduleRule):
|
||||
"""A rule for low batch GEMM / decode-GEMM."""
|
||||
|
||||
def __init__(self, bucket=4):
|
||||
self.bucket = bucket
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,too-many-branches,too-many-return-statements
|
||||
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
|
||||
sch = s_tir.Schedule(func)
|
||||
block_infos = normalize_prim_func(sch)
|
||||
if block_infos is None:
|
||||
return None
|
||||
reduction_block_infos = [
|
||||
block_info for block_info in block_infos if block_info.is_reduction()
|
||||
]
|
||||
if len(reduction_block_infos) != 1:
|
||||
return None
|
||||
reduction_block_info = reduction_block_infos[0]
|
||||
vector_input_buffers = is_gemv(sch, reduction_block_info)
|
||||
if vector_input_buffers is None:
|
||||
return None
|
||||
batch_pad = self.bucket
|
||||
pad_value = [
|
||||
iter.dom if isinstance(iter.dom, int) else batch_pad
|
||||
for iter in reduction_block_info.iters
|
||||
]
|
||||
sch.pad_einsum(reduction_block_info.block_rv, pad_value)
|
||||
block_infos = normalize_prim_func(sch)
|
||||
dequantize_block = None
|
||||
pad_input_block = None
|
||||
for block_info in block_infos:
|
||||
if "dequantize" in block_info.name:
|
||||
dequantize_block = block_info.block_rv
|
||||
elif "pad" in block_info.name and len(sch.get_producers(block_info.block_rv)) == 0:
|
||||
pad_input_block = block_info.block_rv
|
||||
block_infos = [
|
||||
block_info
|
||||
for block_info in block_infos
|
||||
if "pad" not in block_info.name and "dequantize" not in block_info.name
|
||||
]
|
||||
block_infos = try_inline_contiguous_spatial(sch, block_infos)
|
||||
if len(block_infos) == 1:
|
||||
epilogue = None
|
||||
elif len(block_infos) == 2:
|
||||
epilogue = block_infos[1]
|
||||
if not epilogue.is_injective():
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
block_info = block_infos[0]
|
||||
if len(block_info.iters) not in [2, 3]:
|
||||
# either [B, S, R] = [B, S, R] * [B, R]
|
||||
# or [S, R] = [S, R] * [R]
|
||||
return None
|
||||
block = block_info.block_rv
|
||||
vector_input_buffers = is_gemv(sch, block_info)
|
||||
if vector_input_buffers is None:
|
||||
return None
|
||||
|
||||
# Step 1. Normalize the block, merge spatial and reduction iters
|
||||
is_inner_reduction = normalize(sch, block_info)
|
||||
# Step 2. Do the scheduling
|
||||
if is_inner_reduction is None:
|
||||
return None
|
||||
elif is_inner_reduction:
|
||||
self.sch_inner_reduction(
|
||||
sch,
|
||||
target,
|
||||
block,
|
||||
dequantize_block,
|
||||
pad_input_block,
|
||||
vector_input_buffers,
|
||||
epilogue,
|
||||
batch_pad,
|
||||
)
|
||||
return sch
|
||||
elif self.bucket <= 4:
|
||||
self.sch_outer_reduction(
|
||||
sch,
|
||||
target,
|
||||
block,
|
||||
dequantize_block,
|
||||
pad_input_block,
|
||||
vector_input_buffers,
|
||||
epilogue,
|
||||
batch_pad,
|
||||
)
|
||||
return sch
|
||||
else:
|
||||
return None
|
||||
|
||||
def sch_inner_reduction( # pylint: disable=too-many-arguments, invalid-name, unused-argument
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
dequantize_block: s_tir.schedule.SBlockRV | None,
|
||||
pad_input_block: s_tir.schedule.SBlockRV | None,
|
||||
vector_input_buffers: list[tirx.Buffer],
|
||||
epilogue_info: SBlockInfo | None,
|
||||
batch_pad: int,
|
||||
):
|
||||
"""Schedule the inner reduction block."""
|
||||
|
||||
def get_max_factor(n, factors):
|
||||
factors = sorted(factors, reverse=True)
|
||||
for factor in factors:
|
||||
if n % factor == 0:
|
||||
return factor
|
||||
return 1
|
||||
|
||||
def apply(
|
||||
sch: s_tir.Schedule,
|
||||
gemv,
|
||||
TAG_S,
|
||||
TAG_R,
|
||||
TS,
|
||||
TR,
|
||||
TILE_S,
|
||||
TILE_R,
|
||||
VEC_LOAD,
|
||||
VEC_C,
|
||||
LOAD_V_SHARED,
|
||||
LOAD_V_VEC,
|
||||
UNROLL,
|
||||
):
|
||||
# rfactor: reduce to tx * vec_c
|
||||
|
||||
_, s, r = sch.get_loops(block=gemv)
|
||||
bx, ts, tile_s = sch.split(s, factors=[None, TS, TILE_S], preserve_unit_iters=True)
|
||||
r, tr, tile_r_vec_n, vec_c = sch.split(
|
||||
r, factors=[None, TR, TILE_R // VEC_C, VEC_C], preserve_unit_iters=True
|
||||
)
|
||||
sch.reorder(r, tile_r_vec_n, tr, vec_c)
|
||||
tr_vec_c = sch.fuse(tr, vec_c)
|
||||
rf = sch.rfactor(tr_vec_c, 0)
|
||||
|
||||
# rfactor: reduce to tx
|
||||
_, bx, ts, tile_s, tr_vec_c = sch.get_loops(block=gemv)
|
||||
tr, vec_c = sch.split(tr_vec_c, factors=[TR, None], preserve_unit_iters=True)
|
||||
rf2 = sch.rfactor(tr, 0)
|
||||
# bind, vectorize compute
|
||||
batch_loop, bx, ts, tile_s, r, tile_r_vec_n, tr_vec_c = sch.get_loops(block=rf)
|
||||
tr, vec_c = sch.split(tr_vec_c, factors=[TR, None], preserve_unit_iters=True)
|
||||
sch.reorder(bx, ts, tr, r, tile_s, tile_r_vec_n, vec_c)
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
sch.vectorize(vec_c)
|
||||
by, batch = sch.split(batch_loop, factors=[None, batch_pad])
|
||||
sch.bind(by, "blockIdx.y")
|
||||
sch.reorder(bx, ts, tr, r, batch)
|
||||
|
||||
shared_mem_usage = 0
|
||||
for buf in vector_input_buffers:
|
||||
buf_size = reduce(
|
||||
lambda x, y: x * y, buf.shape, tirx.IntImm(buf.shape[0].ty, 1)
|
||||
) * get_bytes(buf.dtype)
|
||||
shared_mem_usage += buf_size
|
||||
max_smem = get_max_shared_memory_per_block(target)
|
||||
LOAD_V_SHARED = (
|
||||
LOAD_V_SHARED
|
||||
and isinstance(shared_mem_usage, tirx.IntImm)
|
||||
and shared_mem_usage.value <= max_smem
|
||||
)
|
||||
|
||||
# vectorize load A
|
||||
# (TODO) this is now actually problematic since the number of loops is dependent on the
|
||||
# number of dimensions of A_q
|
||||
if dequantize_block is not None:
|
||||
sch.compute_at(dequantize_block, r, preserve_unit_loops=True)
|
||||
sch.set_scope(dequantize_block, 0, "local")
|
||||
|
||||
s_local, r_local = sch.get_loops(block=dequantize_block)[-2:]
|
||||
s_local, vec_load = sch.split(
|
||||
s_local, factors=[None, VEC_LOAD], preserve_unit_iters=True
|
||||
)
|
||||
sch.reorder(s_local, r_local, vec_load) # either s_local or r_local should be 1
|
||||
sch.vectorize(vec_load)
|
||||
|
||||
# load vector into shared memory, shape should be the whole vector
|
||||
if LOAD_V_SHARED:
|
||||
assert len(vector_input_buffers) == 1
|
||||
V_shared = sch.cache_read(rf, read_buffer_index=0, storage_scope="shared")
|
||||
sch.compute_at(V_shared, tr, preserve_unit_loops=True)
|
||||
l = sch.get_loops(block=V_shared)[-1]
|
||||
loop: tirx.For = sch.get(l)
|
||||
if isinstance(loop.extent, tirx.IntImm):
|
||||
# avoid introducing predicates when vector length is too large
|
||||
vec_length = max(
|
||||
min(
|
||||
get_max_factor(
|
||||
(int)(loop.extent),
|
||||
[TS * TR * 1, TS * TR * 2, TS * TR * 4, TS * TR * 8],
|
||||
)
|
||||
// TS
|
||||
// TR,
|
||||
LOAD_V_VEC,
|
||||
),
|
||||
1,
|
||||
)
|
||||
else:
|
||||
vec_length = LOAD_V_VEC
|
||||
if TAG_R == "threadIdx.x":
|
||||
_, ty, tx, vec = sch.split(
|
||||
l, factors=[None, TS, TR, vec_length], preserve_unit_iters=True
|
||||
)
|
||||
else:
|
||||
_, ty, tx, vec = sch.split(
|
||||
l, factors=[None, TR, TS, vec_length], preserve_unit_iters=True
|
||||
)
|
||||
sch.bind(ty, "threadIdx.y")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.vectorize(vec)
|
||||
if pad_input_block is not None:
|
||||
sch.compute_inline(pad_input_block)
|
||||
|
||||
# reduce tile_s * tr * vec to tile_s * tr
|
||||
sch.reverse_compute_at(rf2, loop=bx, preserve_unit_loops=True)
|
||||
tr, vec_c, batch_loop, *ts_tile_s = sch.get_loops(block=rf2)[2:]
|
||||
ts_tile_s = sch.fuse(*ts_tile_s)
|
||||
ts_o, ts_i, tile_s = sch.split(
|
||||
ts_tile_s, factors=[None, TS, TILE_S], preserve_unit_iters=True
|
||||
)
|
||||
tile_s, vec_s = sch.split(
|
||||
tile_s,
|
||||
factors=[None, get_max_factor(TILE_S, [1, 2, 4, 8])],
|
||||
preserve_unit_iters=True,
|
||||
)
|
||||
assert sch.get(ts_o).extent.value == 1
|
||||
ts = sch.fuse(ts_o, ts_i)
|
||||
sch.reorder(ts, tr, tile_s, batch_loop, vec_s, vec_c)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
sch.vectorize(vec_s)
|
||||
|
||||
# reduce tile_s * tr to tile_s
|
||||
sch.reverse_compute_at(gemv, loop=bx, preserve_unit_loops=True)
|
||||
|
||||
tr, batch_loop, *ts_tile_s = sch.get_loops(block=gemv)[2:]
|
||||
ts_tile_s = sch.fuse(*ts_tile_s)
|
||||
ts_o, ts_i, tile_s = sch.split(
|
||||
ts_tile_s, factors=[None, TS, TILE_S], preserve_unit_iters=True
|
||||
)
|
||||
assert sch.get(ts_o).extent.value == 1
|
||||
ts = sch.fuse(ts_o, ts_i)
|
||||
sch.reorder(tile_s, batch_loop, ts, tr)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
|
||||
sch.decompose_reduction(rf, loop=sch.get_loops(block=rf)[4])
|
||||
sch.decompose_reduction(rf2, loop=sch.get_loops(block=rf2)[-1])
|
||||
|
||||
sch.set_scope(rf, buffer_index=0, storage_scope="local")
|
||||
sch.set_scope(rf2, buffer_index=0, storage_scope="local")
|
||||
|
||||
unroll_factor = UNROLL
|
||||
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf)[4],
|
||||
ann_key="pragma_auto_unroll_max_step",
|
||||
ann_val=unroll_factor,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf)[4], ann_key="pragma_unroll_explicit", ann_val=1
|
||||
)
|
||||
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf2)[4],
|
||||
ann_key="pragma_auto_unroll_max_step",
|
||||
ann_val=unroll_factor,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(rf2)[4], ann_key="pragma_unroll_explicit", ann_val=1
|
||||
)
|
||||
|
||||
if LOAD_V_SHARED:
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(V_shared)[-4],
|
||||
ann_key="pragma_unroll_explicit",
|
||||
ann_val=unroll_factor,
|
||||
)
|
||||
sch.annotate(
|
||||
block_or_loop=sch.get_loops(V_shared)[-4], ann_key="pragma_vectorize", ann_val=1
|
||||
)
|
||||
|
||||
epilogue = sch.get_consumers(gemv)
|
||||
# Schedule epilogue
|
||||
if epilogue:
|
||||
epilogue = epilogue[0]
|
||||
if is_broadcast_epilogue(sch, block, epilogue):
|
||||
sch.reverse_compute_at(epilogue, bx)
|
||||
sch.set_scope(block, 0, "shared")
|
||||
_, _, _, *s = sch.get_loops(epilogue) # pylint: disable=invalid-name
|
||||
_, tx = sch.split(sch.fuse(*s), factors=[None, TX])
|
||||
sch.bind(tx, TAG_S)
|
||||
else:
|
||||
sch.reverse_compute_at(epilogue, bx, preserve_unit_loops=True)
|
||||
ts_tile_s = sch.fuse(*sch.get_loops(epilogue)[3:])
|
||||
ts_tile_s = sch.get_loops(epilogue)[-1]
|
||||
ts_o, ts_i, tile_s = sch.split(
|
||||
ts_tile_s, factors=[None, TS, TILE_S], preserve_unit_iters=True
|
||||
)
|
||||
assert sch.get(ts_o).extent.value == 1
|
||||
ts = sch.fuse(ts_o, ts_i)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.set_scope(block, 0, "local")
|
||||
|
||||
return sch
|
||||
|
||||
# Specify the `len_tx` and `len_ty` according to the loop extent
|
||||
_, s, r = sch.get_loops(block=block)
|
||||
len_s, len_r = get_extent(sch, s), get_extent(sch, r)
|
||||
|
||||
TAG_S, TAG_R = "threadIdx.y", "threadIdx.x"
|
||||
if target.kind.name == "cuda":
|
||||
VEC_C = 4
|
||||
LOAD_V_SHARED = True
|
||||
LOAD_V_VEC = 8
|
||||
UNROLL = 256
|
||||
if isinstance(len_s, int):
|
||||
if len_s > len_r:
|
||||
TS, TR = 4, 64
|
||||
else:
|
||||
TS, TR = 16, 32
|
||||
elif target.kind.name == "metal":
|
||||
VEC_C = 4
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 8
|
||||
if isinstance(len_s, int):
|
||||
if len_s > len_r:
|
||||
TS, TR = 8, 32
|
||||
else:
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
TS, TR = 8, 32
|
||||
elif target.kind.name == "rocm":
|
||||
VEC_C = 4
|
||||
LOAD_V_SHARED = True
|
||||
LOAD_V_VEC = 8
|
||||
UNROLL = 256
|
||||
if isinstance(len_s, int):
|
||||
if len_s > len_r:
|
||||
TS, TR = 1, 128
|
||||
else:
|
||||
TS, TR = 8, 64
|
||||
elif target.kind.name == "opencl" and "android" in str(target.host):
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
VEC_C = 8
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 8
|
||||
TS, TR = 2, 32
|
||||
elif target.kind.name == "vulkan":
|
||||
VEC_C = 4
|
||||
LOAD_V_SHARED = True
|
||||
LOAD_V_VEC = 4
|
||||
UNROLL = 256
|
||||
if isinstance(len_s, int):
|
||||
if len_s > len_r:
|
||||
TS, TR = 4, 32
|
||||
else:
|
||||
TS, TR = 16, 32
|
||||
elif target.kind.name == "opencl" and "mali" in str(target.attrs):
|
||||
VEC_C = 8
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 64
|
||||
TS, TR = 1, 64
|
||||
else:
|
||||
VEC_C = 1
|
||||
LOAD_V_SHARED = False
|
||||
LOAD_V_VEC = -1
|
||||
UNROLL = 64
|
||||
TS, TR = 1, 64
|
||||
|
||||
if not isinstance(len_s, int):
|
||||
TS, TR = 1, 64
|
||||
|
||||
while TS * TR > int(target.attrs["max_num_threads"]):
|
||||
if TS > 1:
|
||||
TS //= 2
|
||||
else:
|
||||
TR //= 2
|
||||
|
||||
TILE_S, TILE_R = 2, max(get_max_factor(len_r, [TR * 1, TR * 2, TR * 4, TR * 8]) // TR, 1)
|
||||
VEC_C = min(get_max_factor(TILE_R, [1, 2, 4, 8]), VEC_C)
|
||||
VEC_LOAD = 1
|
||||
return apply(
|
||||
sch,
|
||||
gemv=block,
|
||||
TAG_S=TAG_S,
|
||||
TAG_R=TAG_R,
|
||||
TS=TS,
|
||||
TR=TR,
|
||||
TILE_S=TILE_S,
|
||||
TILE_R=TILE_R,
|
||||
VEC_LOAD=VEC_LOAD,
|
||||
VEC_C=VEC_C,
|
||||
LOAD_V_SHARED=LOAD_V_SHARED,
|
||||
LOAD_V_VEC=LOAD_V_VEC,
|
||||
UNROLL=UNROLL,
|
||||
)
|
||||
|
||||
def sch_outer_reduction( # pylint: disable=too-many-arguments, invalid-name, unused-argument
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
dequantize_block: s_tir.schedule.SBlockRV | None,
|
||||
pad_input_block: s_tir.schedule.SBlockRV | None,
|
||||
vector_input_buffers: list[tirx.Buffer],
|
||||
epilogue_info: SBlockInfo | None,
|
||||
batch_pad: int,
|
||||
):
|
||||
"""Schedule the outer reduction block."""
|
||||
|
||||
# Need to detect from the block
|
||||
DEC_PACK = 8
|
||||
SCALE_PACK = 4
|
||||
|
||||
def apply(
|
||||
sch: s_tir.Schedule,
|
||||
main_block: s_tir.schedule.SBlockRV,
|
||||
TAG_S: Literal["threadIdx.x", "threadIdx.y"],
|
||||
TAG_R: Literal["threadIdx.x", "threadIdx.y"],
|
||||
TS: int,
|
||||
TR: int,
|
||||
VEC: int,
|
||||
UNROLL: int,
|
||||
):
|
||||
# rfactor: reduce to tx * vec_c
|
||||
b, s, r = sch.get_loops(main_block)
|
||||
by, batch = sch.split(b, [None, batch_pad], preserve_unit_iters=True)
|
||||
bx, ts = sch.split(s, [None, TS], preserve_unit_iters=True)
|
||||
r, tr, scale_c, vec_c = sch.split(
|
||||
r, [None, TR, SCALE_PACK, DEC_PACK], preserve_unit_iters=True
|
||||
)
|
||||
sch.reorder(by, bx, ts, r, batch, scale_c, tr, vec_c)
|
||||
tr_vec_c = sch.fuse(tr, vec_c)
|
||||
rf = sch.rfactor(tr_vec_c, 0)
|
||||
|
||||
# rfactor: reduce to tx
|
||||
by, bx, ts, batch, tr_vec_c = sch.get_loops(block=main_block)
|
||||
tr, vec_c = sch.split(tr_vec_c, [TR, DEC_PACK], preserve_unit_iters=True)
|
||||
rf2 = sch.rfactor(tr, 0)
|
||||
|
||||
# bind, vectorize compute
|
||||
by, bx, ts, r, batch, scale_c, tr_vec_c = sch.get_loops(block=rf)
|
||||
tr, vec_c = sch.split(tr_vec_c, [TR, DEC_PACK], preserve_unit_iters=True)
|
||||
sch.reorder(by, bx, ts, tr, r, scale_c, batch, vec_c)
|
||||
sch.bind(by, "blockIdx.y")
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
auto_vectorize(sch, vec_c, VEC)
|
||||
|
||||
if dequantize_block is not None:
|
||||
sch.compute_at(dequantize_block, scale_c, preserve_unit_loops=True)
|
||||
sch.set_scope(dequantize_block, 0, "local")
|
||||
auto_vectorize(sch, sch.fuse(*sch.get_loops(dequantize_block)[6:]), VEC)
|
||||
|
||||
B0_local = sch.cache_read(dequantize_block, 0, "local")
|
||||
sch.compute_at(B0_local, r, preserve_unit_loops=True)
|
||||
auto_vectorize(sch, sch.fuse(*sch.get_loops(B0_local)[5:]), VEC)
|
||||
|
||||
B1_local = sch.cache_read(dequantize_block, 1, "local")
|
||||
sch.compute_at(B1_local, r, preserve_unit_loops=True)
|
||||
auto_vectorize(sch, sch.fuse(*sch.get_loops(B1_local)[5:]), VEC)
|
||||
else:
|
||||
# Only support quantized workloads for now
|
||||
sch = None
|
||||
return
|
||||
|
||||
if LOAD_V_SHARED:
|
||||
sch.set_scope(pad_input_block, 0, "shared")
|
||||
sch.compute_at(pad_input_block, r, preserve_unit_loops=True)
|
||||
sch.storage_align(pad_input_block, 0, axis=-2, factor=8, offset=1)
|
||||
tr, ts, v = sch.split(sch.fuse(*sch.get_loops(pad_input_block)[5:]), [TR, TS, None])
|
||||
sch.bind(tr, TAG_R)
|
||||
sch.bind(ts, TAG_S)
|
||||
auto_vectorize(sch, v, VEC)
|
||||
else:
|
||||
sch.compute_inline(pad_input_block)
|
||||
|
||||
# reduce tile_s * tr * vec to tile_s * tr
|
||||
sch.reverse_compute_at(rf2, bx, preserve_unit_loops=True)
|
||||
tr, vec_c, batch, ts = sch.get_loops(rf2)[2:]
|
||||
sch.reorder(ts, tr, batch, vec_c)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
|
||||
# reduce tile_s * tr to tile_s
|
||||
sch.reverse_compute_at(main_block, bx, preserve_unit_loops=True)
|
||||
tr, batch, ts = sch.get_loops(main_block)[2:]
|
||||
sch.reorder(batch, ts, tr)
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.bind(tr, TAG_R)
|
||||
# unroll(batch, 1)
|
||||
|
||||
sch.decompose_reduction(rf, loop=sch.get_loops(block=rf)[4])
|
||||
sch.decompose_reduction(rf2, loop=sch.get_loops(block=rf2)[4])
|
||||
|
||||
sch.set_scope(rf, buffer_index=0, storage_scope="local")
|
||||
sch.set_scope(rf2, buffer_index=0, storage_scope="local")
|
||||
|
||||
epilogue = sch.get_consumers(main_block)
|
||||
# Schedule epilogue
|
||||
if epilogue:
|
||||
epilogue = epilogue[0]
|
||||
if is_broadcast_epilogue( # pylint: disable=no-else-raise
|
||||
sch, main_block, epilogue
|
||||
):
|
||||
raise NotImplementedError
|
||||
else:
|
||||
sch.reverse_compute_at(epilogue, bx, preserve_unit_loops=True)
|
||||
batch, ts = sch.get_loops(epilogue)[2:]
|
||||
sch.bind(ts, TAG_S)
|
||||
sch.set_scope(main_block, 0, "local")
|
||||
|
||||
if target.kind.name == "metal":
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
TS, TR = 64, 4
|
||||
LOAD_V_SHARED = True
|
||||
VEC = 4
|
||||
UNROLL = 8
|
||||
else:
|
||||
# fallback configuration
|
||||
TAG_S, TAG_R = "threadIdx.x", "threadIdx.y"
|
||||
TS, TR = 32, 4
|
||||
LOAD_V_SHARED = False
|
||||
VEC = 1
|
||||
UNROLL = 64
|
||||
|
||||
return apply(
|
||||
sch,
|
||||
block,
|
||||
TAG_S,
|
||||
TAG_R,
|
||||
TS,
|
||||
TR,
|
||||
VEC,
|
||||
UNROLL,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,305 @@
|
||||
# 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.
|
||||
"""A rule for reduction."""
|
||||
|
||||
# TODO: combine reduction rule and general reduction rule into one file.
|
||||
from collections.abc import Mapping
|
||||
|
||||
import tvm_ffi
|
||||
|
||||
from tvm import arith, s_tir, tirx
|
||||
from tvm.target import Target
|
||||
|
||||
from ..analysis import (
|
||||
SBlockInfo,
|
||||
detect_dominant_read,
|
||||
is_broadcast_epilogue,
|
||||
normalize_prim_func,
|
||||
)
|
||||
from ..base import suggest_threads_per_block, try_inline_contiguous_spatial
|
||||
from .base import GPUScheduleRule
|
||||
|
||||
|
||||
def _get_reduction_expr(block: tirx.SBlock) -> tirx.Expr | None:
|
||||
# Detect and return `Y` in `X[...] = X[...] + Y`
|
||||
buffer_store = block.body
|
||||
if not isinstance(buffer_store, tirx.BufferStore):
|
||||
return None
|
||||
if not isinstance(buffer_store.value, tirx.Add):
|
||||
return None
|
||||
if not tvm_ffi.structural_equal(
|
||||
buffer_store.value.a,
|
||||
tirx.BufferLoad(buffer_store.buffer, block.body.indices),
|
||||
map_free_vars=True,
|
||||
):
|
||||
return None
|
||||
return buffer_store.value.b
|
||||
|
||||
|
||||
def _has_reduction_loop(block_info):
|
||||
return any([info.kind == "R" for info in block_info.iters])
|
||||
|
||||
|
||||
class Reduction(GPUScheduleRule):
|
||||
"""A rule for Reduction."""
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,too-many-branches,too-many-return-statements
|
||||
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
|
||||
sch = s_tir.Schedule(func)
|
||||
block_infos = normalize_prim_func(sch)
|
||||
if block_infos is None:
|
||||
return None
|
||||
block_infos = try_inline_contiguous_spatial(sch, block_infos)
|
||||
if len(block_infos) == 1:
|
||||
epilogue = None
|
||||
elif len(block_infos) == 2:
|
||||
epilogue = block_infos[1]
|
||||
if not epilogue.is_injective():
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
block_info = block_infos[0]
|
||||
block = block_info.block_rv
|
||||
block_stmt = sch.get(block)
|
||||
|
||||
# Step 1. Check reduction block
|
||||
if (
|
||||
(not block_info.is_reduction())
|
||||
or (not _has_reduction_loop(block_info))
|
||||
or len(block_stmt.writes) != 1
|
||||
or _get_reduction_expr(block_stmt) is None
|
||||
):
|
||||
return None
|
||||
# Step 2. Normalize the block, merge spatial and reduction iters
|
||||
is_inner_reduction, c_factor, loop_order, s_split_index = self._normalize(
|
||||
sch,
|
||||
block_info,
|
||||
arith.normalize_to_iter_sum(
|
||||
detect_dominant_read(block_stmt),
|
||||
input_iters={i.var: i.dom for i in block_stmt.iter_vars},
|
||||
),
|
||||
)
|
||||
if is_inner_reduction is None and c_factor is None:
|
||||
return None
|
||||
# Step 3. Do the scheduling
|
||||
if is_inner_reduction:
|
||||
self._sch_inner_reduction(
|
||||
sch, target, block, c_factor, epilogue, loop_order, s_split_index
|
||||
)
|
||||
else:
|
||||
self._sch_inner_spatial(
|
||||
sch, target, block, block_info, c_factor, epilogue, loop_order, s_split_index
|
||||
)
|
||||
return sch
|
||||
|
||||
def _normalize( # pylint: disable=too-many-branches
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
block_info: SBlockInfo,
|
||||
access: arith.IterSumExpr,
|
||||
) -> tuple[bool | None, int | None, Mapping[int, int] | None, int | None]:
|
||||
if access.base != 0:
|
||||
return None, None, None, None
|
||||
iter_to_info = {i.var: i for i in block_info.iters}
|
||||
s_loops, r_loops, c_loops, c_factor = [], [], [], None
|
||||
s_split_loop, s_split_index = None, None
|
||||
for split_expr in access.args:
|
||||
var = split_expr.source.source
|
||||
info = iter_to_info.pop(var)
|
||||
loop = info.loop_rv
|
||||
is_inner_reduction = info.kind == "R"
|
||||
if split_expr.lower_factor > 1:
|
||||
if c_loops:
|
||||
return None, None, None, None
|
||||
s_split_loop = loop
|
||||
s_split_index = len(s_loops)
|
||||
loop, c_loop = sch.split(loop, factors=[None, split_expr.lower_factor])
|
||||
c_loops.append(c_loop)
|
||||
if not is_inner_reduction:
|
||||
c_factor = split_expr.lower_factor
|
||||
if is_inner_reduction:
|
||||
r_loops.append(loop)
|
||||
else:
|
||||
s_loops.append(loop)
|
||||
|
||||
if iter_to_info:
|
||||
for var, info in iter_to_info.items():
|
||||
if info.kind == "S" and info.dom == 1:
|
||||
s_loops.append(info.loop_rv)
|
||||
else:
|
||||
return None, None, None, None
|
||||
|
||||
loop_order = {}
|
||||
s_block_var_loops = []
|
||||
for i in block_info.iters:
|
||||
if i.loop_rv in s_loops or i.loop_rv == s_split_loop:
|
||||
s_block_var_loops.append(i.loop_rv)
|
||||
|
||||
for i in range(len(s_block_var_loops)):
|
||||
for j in range(len(s_loops)):
|
||||
if s_block_var_loops[i] == s_loops[j]:
|
||||
loop_order[i] = j
|
||||
break
|
||||
if s_block_var_loops[i] == s_split_loop:
|
||||
loop_order[i] = s_split_index
|
||||
break
|
||||
|
||||
assert s_loops
|
||||
assert r_loops
|
||||
if len(s_loops) != len([i for i in block_info.iters if i.kind == "S"]):
|
||||
return None, None, None, None
|
||||
if not c_loops:
|
||||
c_loops = [sch.add_unit_loop(block_info.block_rv)]
|
||||
sch.reorder(*s_loops, *r_loops, *c_loops)
|
||||
sch.fuse(*s_loops)
|
||||
sch.fuse(*r_loops)
|
||||
return is_inner_reduction, c_factor, loop_order, s_split_index
|
||||
|
||||
def _sch_inner_reduction( # pylint: disable=too-many-arguments
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
target: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
unroll_spatial_factor: int | None,
|
||||
epilogue_info: SBlockInfo | None,
|
||||
loop_order,
|
||||
s_split_index,
|
||||
):
|
||||
# pylint: disable=invalid-name
|
||||
_, r, _ = sch.get_loops(block)
|
||||
(len_tx,) = suggest_threads_per_block( # pylint: disable=unbalanced-tuple-unpacking
|
||||
target, [sch.get(r)]
|
||||
)
|
||||
|
||||
_, tx = sch.split(r, factors=[None, len_tx])
|
||||
# Schedule the RF block
|
||||
rf = sch.rfactor(tx, 0)
|
||||
bx, r, tx, _ = sch.get_loops(rf)
|
||||
sch.reorder(bx, tx, r)
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.annotate(tx, ann_key="pragma_auto_unroll_max_step", ann_val=256)
|
||||
sch.annotate(tx, ann_key="pragma_unroll_explicit", ann_val=1)
|
||||
sch.set_scope(rf, 0, "local")
|
||||
sch.decompose_reduction(rf, r)
|
||||
# Schedule the write back block
|
||||
sch.reverse_compute_at(block, bx, preserve_unit_loops=True)
|
||||
_, tx, *s = sch.get_loops(block)
|
||||
|
||||
if unroll_spatial_factor:
|
||||
assert len(s) == len(loop_order)
|
||||
new_order_s = [s[loop_order[i]] for i in range(len(s))]
|
||||
sch.reorder(*new_order_s)
|
||||
new_order_s[s_split_index], c = sch.split(
|
||||
new_order_s[s_split_index], factors=[None, unroll_spatial_factor]
|
||||
)
|
||||
sch.reorder(*new_order_s, c)
|
||||
s = sch.fuse(*new_order_s)
|
||||
sch.reorder(s, tx, c)
|
||||
else:
|
||||
s = sch.fuse(*s)
|
||||
sch.reorder(s, tx)
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
# Schedule epilogue
|
||||
if epilogue_info is not None:
|
||||
epilogue = epilogue_info.block_rv
|
||||
sch.reverse_compute_at(epilogue, bx, preserve_unit_loops=True)
|
||||
if is_broadcast_epilogue(sch, block, epilogue):
|
||||
sch.set_scope(block, 0, "shared")
|
||||
_, *s = sch.get_loops(epilogue) # pylint: disable=invalid-name
|
||||
_, tx = sch.split(sch.fuse(*s), factors=[None, len_tx])
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
else:
|
||||
sch.set_scope(block, 0, "local")
|
||||
# pylint: enable=invalid-name
|
||||
|
||||
def _sch_inner_spatial(
|
||||
self,
|
||||
sch: s_tir.Schedule,
|
||||
_: Target,
|
||||
block: s_tir.schedule.SBlockRV,
|
||||
block_info: SBlockInfo,
|
||||
unroll_spatial_factor: int | None,
|
||||
epilogue_info: SBlockInfo | None,
|
||||
loop_order,
|
||||
s_split_index,
|
||||
):
|
||||
# pylint: disable=invalid-name
|
||||
s, r, _ = sch.get_loops(block)
|
||||
len_tx, len_ty = 16, 16
|
||||
s_factor = [i.dom for i in block_info.iters if i.kind == "S"][-1]
|
||||
# get perfect spatial factor, spatial factor should be divide the innermost spatial loop so
|
||||
# that the block after r_factor and be reversed compute at the original scope
|
||||
while len_tx > 1:
|
||||
if s_factor % len_tx == 0:
|
||||
break
|
||||
len_tx -= 1
|
||||
_, _ = sch.split(s, factors=[None, len_tx])
|
||||
_, ty = sch.split(r, factors=[None, len_ty])
|
||||
# Schedule the RF block
|
||||
rf = sch.rfactor(ty, 0)
|
||||
bx, tx, r, ty, _ = sch.get_loops(rf)
|
||||
sch.reorder(bx, tx, ty, r)
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.bind(ty, "threadIdx.y")
|
||||
sch.bind(bx, "blockIdx.x")
|
||||
sch.set_scope(rf, 0, "local")
|
||||
sch.decompose_reduction(rf, r)
|
||||
# Schedule the write back block
|
||||
sch.reverse_compute_at(block, bx, preserve_unit_loops=True)
|
||||
_, r, *s = sch.get_loops(block)
|
||||
if unroll_spatial_factor:
|
||||
assert len(s) == len(loop_order)
|
||||
new_order_s = [s[loop_order[i]] for i in range(len(s))]
|
||||
sch.reorder(*new_order_s)
|
||||
new_order_s[s_split_index], c = sch.split(
|
||||
new_order_s[s_split_index], factors=[None, unroll_spatial_factor]
|
||||
)
|
||||
sch.reorder(*new_order_s, c)
|
||||
s = sch.fuse(*new_order_s)
|
||||
sch.reorder(s, c, r)
|
||||
else:
|
||||
s = sch.fuse(*s)
|
||||
sch.reorder(s, r)
|
||||
sch.bind(s, "threadIdx.x")
|
||||
sch.bind(r, "threadIdx.y")
|
||||
|
||||
# Schedule epilogue
|
||||
if epilogue_info is not None:
|
||||
epilogue = epilogue_info.block_rv
|
||||
sch.reverse_compute_at(epilogue, bx, preserve_unit_loops=True)
|
||||
if is_broadcast_epilogue(sch, block, epilogue):
|
||||
sch.set_scope(block, 0, "shared")
|
||||
_, *s = sch.get_loops(epilogue) # pylint: disable=invalid-name
|
||||
_, tx, ty = sch.split(sch.fuse(*s), factors=[None, len_tx, len_ty])
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.bind(ty, "threadIdx.y")
|
||||
else:
|
||||
# The epilogue is element-wise without broadcasting.
|
||||
# Thus the remaining spatial part should be bind to tx.
|
||||
sch.set_scope(block, 0, "local")
|
||||
_, *s = sch.get_loops(epilogue) # pylint: disable=invalid-name
|
||||
tx, _ = sch.split(sch.fuse(*s), factors=[len_tx, None])
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
# pylint: enable=invalid-name
|
||||
@@ -0,0 +1,143 @@
|
||||
# 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
|
||||
"""A RMS norm schedule rule for GPU operators."""
|
||||
|
||||
import tvm
|
||||
from tvm import tirx
|
||||
from tvm.ir import Call
|
||||
from tvm.target import Target
|
||||
from tvm.tirx import BufferStore, SBlock
|
||||
from tvm.tirx.expr import BufferLoad, Cast
|
||||
|
||||
from ..base import ScheduleRule
|
||||
|
||||
|
||||
def identify_cast_or_load_block(block: SBlock) -> bool:
|
||||
if len(block.reads) != 1 or len(block.writes) != 1:
|
||||
return False
|
||||
|
||||
if not isinstance(block.body, BufferStore):
|
||||
return False
|
||||
store = block.body
|
||||
|
||||
# check types
|
||||
if isinstance(store.value, BufferLoad):
|
||||
load = store.value
|
||||
elif isinstance(store.value, Cast):
|
||||
load = store.value.value
|
||||
if not isinstance(load, BufferLoad):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
# check indices
|
||||
if len(load.indices) != len(store.indices):
|
||||
return False
|
||||
|
||||
for lhs, rhs in zip(load.indices, store.indices):
|
||||
if not lhs.same_as(rhs):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def identify_rsqrt_block(block: SBlock) -> bool:
|
||||
if len(block.reads) != 1 or len(block.writes) != 1:
|
||||
return False
|
||||
|
||||
if not isinstance(block.body, BufferStore):
|
||||
return False
|
||||
store = block.body
|
||||
|
||||
if not isinstance(store.value, Call):
|
||||
return False
|
||||
call = store.value
|
||||
op = call.op
|
||||
|
||||
return op == tvm.ir.op.Op.get("tirx.rsqrt")
|
||||
|
||||
|
||||
class RMSNorm(ScheduleRule):
|
||||
"""A rule for RMS norm."""
|
||||
|
||||
def apply( # pylint: disable=too-many-locals,missing-docstring
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> "tvm.s_tir.Schedule":
|
||||
if target.kind.name == "cuda":
|
||||
num_tx = 512
|
||||
elif target.kind.name == "opencl":
|
||||
num_tx = 256
|
||||
else:
|
||||
num_tx = 64
|
||||
|
||||
sch = tvm.s_tir.Schedule(func)
|
||||
root = sch.get_sblock(name="root", func_name="main")
|
||||
|
||||
blocks = sch.get_child_blocks(root)
|
||||
|
||||
if not any([identify_rsqrt_block(sch.get(block)) for block in blocks]):
|
||||
return None
|
||||
|
||||
read = sch.cache_read(block=blocks[0], read_buffer_index=0, storage_scope="local")
|
||||
write = sch.cache_write(block=blocks[-1], write_buffer_index=0, storage_scope="local")
|
||||
|
||||
for block in blocks:
|
||||
if identify_cast_or_load_block(sch.get(block)):
|
||||
sch.compute_inline(block)
|
||||
|
||||
blocks = sch.get_child_blocks(root)
|
||||
|
||||
read, sqr, redsum, rsqrt, norm, write = blocks
|
||||
|
||||
if not identify_rsqrt_block(sch.get(rsqrt)):
|
||||
return None
|
||||
|
||||
for name in [read, sqr, redsum, rsqrt, norm, write]:
|
||||
loops = sch.get_loops(name)
|
||||
sch.fuse(*loops[:-1])
|
||||
|
||||
block_loop, loops = sch.get_loops(block=read)
|
||||
thread_loop, _, _ = sch.split(
|
||||
loop=loops, factors=[num_tx, None, 8], preserve_unit_iters=True
|
||||
)
|
||||
sch.bind(block_loop, thread_axis="blockIdx.x")
|
||||
sch.bind(thread_loop, thread_axis="threadIdx.x")
|
||||
sch.vectorize(sch.get_loops(block=read)[-1])
|
||||
sch.reverse_compute_at(block=sqr, loop=thread_loop)
|
||||
sch.reverse_compute_at(block=redsum, loop=thread_loop)
|
||||
|
||||
sch.reverse_compute_at(block=rsqrt, loop=block_loop, index=-1)
|
||||
sch.reverse_compute_at(block=norm, loop=block_loop, index=-1)
|
||||
block_loop, loops = sch.get_loops(block=norm)
|
||||
thread_loop, _, _ = sch.split(
|
||||
loop=loops, factors=[num_tx, None, 8], preserve_unit_iters=True
|
||||
)
|
||||
sch.bind(thread_loop, thread_axis="threadIdx.x")
|
||||
|
||||
sch.reverse_compute_at(block=write, loop=thread_loop, index=-1)
|
||||
sch.vectorize(sch.get_loops(block=write)[-1])
|
||||
|
||||
sch.set_scope(block=sqr, buffer_index=0, storage_scope="local")
|
||||
sch.set_scope(block=redsum, buffer_index=0, storage_scope="local")
|
||||
sch.set_scope(block=rsqrt, buffer_index=0, storage_scope="shared")
|
||||
sch.set_scope(block=norm, buffer_index=0, storage_scope="local")
|
||||
|
||||
return sch
|
||||
@@ -0,0 +1,129 @@
|
||||
# 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.
|
||||
"""Reduction rule for operators including softmax, layer norm, RMS norm, etc"""
|
||||
|
||||
from tvm import arith, s_tir, tirx
|
||||
from tvm.s_tir import Schedule
|
||||
from tvm.s_tir.schedule import SBlockRV
|
||||
from tvm.target import Target
|
||||
|
||||
from ..analysis import detect_dominant_read, normalize_prim_func
|
||||
from ..base import try_inline_contiguous_spatial
|
||||
from .base import GPUScheduleRule
|
||||
|
||||
|
||||
class Transpose(GPUScheduleRule):
|
||||
"""Schedule rule for transpose"""
|
||||
|
||||
def is_transpose(self, sch: Schedule, block_rv: SBlockRV):
|
||||
block = sch.get(block_rv)
|
||||
if isinstance(block.body, tirx.BufferStore):
|
||||
rhs = block.body.value
|
||||
if isinstance(rhs, tirx.BufferLoad):
|
||||
lhs_indices = block.body.indices
|
||||
rhs_indices = rhs.indices
|
||||
if list(lhs_indices) != list(rhs_indices) and set(lhs_indices) == set(rhs_indices):
|
||||
return True
|
||||
return False
|
||||
|
||||
def apply( # pylint: disable=too-many-locals
|
||||
self,
|
||||
func: tirx.PrimFunc,
|
||||
target: Target,
|
||||
_: bool,
|
||||
) -> None | s_tir.Schedule | list[s_tir.Schedule]:
|
||||
# pylint: disable=invalid-name
|
||||
if not isinstance(func, tirx.PrimFunc) or not self.is_target_available(target):
|
||||
return None
|
||||
if target.kind.name == "cuda":
|
||||
len_tx = 16
|
||||
len_ty = 8
|
||||
unroll_depth = 256
|
||||
elif target.kind.name == "opencl":
|
||||
len_tx = 16
|
||||
len_ty = 8
|
||||
unroll_depth = 64
|
||||
else:
|
||||
len_tx = 8
|
||||
len_ty = 4
|
||||
unroll_depth = 64
|
||||
len_vec = 4
|
||||
|
||||
sch = s_tir.Schedule(func)
|
||||
blocks = normalize_prim_func(sch)
|
||||
transpose_block_idx = -1
|
||||
for idx, block in reversed(list(enumerate(blocks))):
|
||||
if self.is_transpose(sch, block.block_rv):
|
||||
transpose_block_idx = idx
|
||||
break
|
||||
if not block.is_injective():
|
||||
return None
|
||||
if transpose_block_idx == -1:
|
||||
return None
|
||||
transpose_block = blocks[transpose_block_idx].block_rv
|
||||
|
||||
prologue = None # the optional decoding block
|
||||
if transpose_block_idx > 0:
|
||||
spatials = try_inline_contiguous_spatial(sch, blocks[: transpose_block_idx - 1])
|
||||
assert len(spatials) == 0
|
||||
prologue = blocks[transpose_block_idx - 1].block_rv
|
||||
|
||||
loops = sch.get_loops(transpose_block)
|
||||
if len(loops) != 2:
|
||||
# transpose with more than 2 axes is not supported
|
||||
return None
|
||||
|
||||
c_factor = 1
|
||||
if prologue is not None:
|
||||
block_stmt = sch.get(prologue)
|
||||
result = arith.normalize_to_iter_sum(
|
||||
detect_dominant_read(block_stmt),
|
||||
input_iters={i.var: i.dom for i in block_stmt.iter_vars},
|
||||
)
|
||||
if len(result.args) > 0:
|
||||
c_factor = int(result.args[0].lower_factor)
|
||||
|
||||
i, j = loops
|
||||
i, vi = sch.split(i, factors=[None, c_factor], preserve_unit_iters=True)
|
||||
bi, ti = sch.split(i, factors=[None, len_ty], preserve_unit_iters=True)
|
||||
bj, tj = sch.split(j, factors=[None, len_tx], preserve_unit_iters=True)
|
||||
sch.reorder(bi, bj, ti, tj, vi)
|
||||
sch.bind(bi, "blockIdx.y")
|
||||
sch.bind(bj, "blockIdx.x")
|
||||
sch.bind(ti, "threadIdx.y")
|
||||
sch.bind(tj, "threadIdx.x")
|
||||
len_vec = min(len_vec, c_factor)
|
||||
_, vi = sch.split(vi, factors=[None, len_vec])
|
||||
if len_vec > 1:
|
||||
sch.vectorize(vi)
|
||||
|
||||
cache_read = sch.cache_read(transpose_block, read_buffer_index=0, storage_scope="shared")
|
||||
sch.compute_at(cache_read, bj)
|
||||
loops = sch.get_loops(cache_read)[2:]
|
||||
fused = sch.fuse(*loops)
|
||||
_, ty, tx, v = sch.split(fused, factors=[None, len_ty, len_tx, c_factor])
|
||||
sch.bind(ty, "threadIdx.y")
|
||||
sch.bind(tx, "threadIdx.x")
|
||||
sch.unroll(v)
|
||||
sch.storage_align(block=cache_read, buffer_index=0, axis=0, factor=32, offset=1)
|
||||
|
||||
sch.annotate(bi, ann_key="pragma_auto_unroll_max_step", ann_val=unroll_depth)
|
||||
sch.annotate(bi, ann_key="pragma_unroll_explicit", ann_val=1)
|
||||
|
||||
if prologue is not None:
|
||||
sch.compute_inline(prologue)
|
||||
return sch
|
||||
Reference in New Issue
Block a user