Files
wehub-resource-sync 26446540fa
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:25 +08:00

570 lines
25 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: E501, E731, RUF005
# fmt: off
"""Shared TIR helpers used by KV-cache / attention kernels in this package.
This module consolidates constructs reused by the prefill/decode/paged/tree
attention kernels so each kernel file can focus on its own specialised logic.
Contents:
- Thread-limit checks (``get_max_num_threads_per_block``, ``check_thread_limits``)
- KV-cache enums (``AttnKind``, ``RopeMode``)
- Small TVMScript helpers (``_var``, ``_var_cpu``, ``_causal_mask``, ``_rope``)
- Length-info accessors for sliding-window-aware indexing
- Buffer allocators for the tiled online-softmax state used by every prefill kernel
- ``_make_prefill_macros`` — the ``@T.macro`` bundle invoked by the prefill kernels
- Tiling config (``_get_prefill_kernel_config``) and scheduling (``_schedule_prefill_kernel``)
"""
# pylint: disable=too-many-statements,too-many-arguments,invalid-name,line-too-long
import enum
import math
from typing import Any
import tvm
from tvm import s_tir, tirx
from tvm.runtime import DataType
from tvm.script import tirx as T
from tvm.target import Target
from .position_embedding import switch_rope_freq_func
def _var(dtype):
return T.sblock_alloc_buffer((1,), dtype, scope="local")
def _var_cpu(dtype):
return T.sblock_alloc_buffer((1,), dtype)
def get_max_num_threads_per_block(target: Target) -> int:
"""
max(max_num_threads, max_threads_per_block); if latter does not exist, return max_num_threads.
We add this method since some targets have both fields and `max_threads_per_block` is larger.
"""
max_num_threads = int(target.attrs["max_num_threads"])
max_threads_per_block = target.attrs.get("max_threads_per_block", None)
if max_threads_per_block is None:
return max_num_threads
return max(max_num_threads, max_threads_per_block)
def check_thread_limits(target: Target, bdx: int, bdy: int, bdz: int, gdz: int):
"""
Check whether max num threads exceeded given a target.
Parameters
----------
bdx: threadIdx.x
bdy: threadIdx.y
bdz: threadIdx.z
gdz: blockIdx.z
"""
max_num_threads_per_block = get_max_num_threads_per_block(target)
assert bdx * bdy * bdz <= max_num_threads_per_block, (
f"{target.kind} max num threads exceeded: {bdx}*{bdy}*{bdz}>{max_num_threads_per_block}"
)
if target.kind.name == "webgpu":
# https://gpuweb.github.io/gpuweb/#dom-supported-limits-maxcomputeworkgroupsizez
assert bdz <= 64, f"webgpu's threadIdx.z cannot exceed 64, but got bdz={bdz}"
assert gdz == 1, f"webgpu's blockIdx.z should be 1, but got gdz={gdz}"
class AttnKind(enum.IntEnum):
"""The attention kind class.
MHA denotes multi-head attention, multi-query attention or grouped query attention.
MLA denotes multi-head latent attention.
"""
MHA = 0
MLA = 1
MHA_SLIDING = 3
class RopeMode(enum.IntEnum):
"""The RoPE mode of the Paged KV cache.
If it is none, the KV cache will not apply RoPE to q and k.
If it is normal, RoPE will be applied to k before adding k to cache.
Otherwise, RoPE will be applied to q/k in attention kernel on-the-fly.
"""
NONE = 0
NORMAL = 1
INLINE = 2
def _rope(buffer: T.Buffer, offset: tirx.Var, rotary_dim: int, theta: tirx.Var, scale: tirx.Var, indices: tuple[tirx.Var, ...], qkv_dtype: str, rope_scaling: dict[str, Any]):
d = indices[-1]
cos_freq, sin_freq, var_map = switch_rope_freq_func(rope_scaling)(offset * scale, d, rotary_dim, theta, "float32")
cos = cos_freq * buffer[indices].astype("float32")
sin = sin_freq * tirx.if_then_else(
d < rotary_dim // 2,
-buffer[indices[:-1] + (d + rotary_dim // 2,)],
buffer[indices[:-1] + (d - rotary_dim // 2,)],
).astype("float32")
expr = (cos + sin).astype(qkv_dtype)
for var, value in var_map.items():
expr = tirx.Let(var, value, expr)
return expr
def _causal_mask(causal, row, col, kv_len, qo_len):
return T.if_then_else(
causal > 0,
col < kv_len - qo_len + row + 1,
col < kv_len,
)
def _declare_length_info(var_length_info, batch_size, sliding_window, elem_offset):
return (
T.match_buffer(var_length_info, (3, batch_size), "int32", elem_offset=elem_offset)
if sliding_window
else T.match_buffer(var_length_info, (batch_size,), "int32", elem_offset=elem_offset)
)
def _get_kv_chunk_len(num_pages, page_size, seq_id, length_info, sliding_window):
if not sliding_window:
return (num_pages - 1) * page_size + length_info[seq_id]
# ((num_pages - 1) * page_size + last_page_len) - sliding_window_offset + sink_size
return (num_pages - 1) * page_size + length_info[0, seq_id] - length_info[1, seq_id] + length_info[2, seq_id]
def _get_seq_offset(pos, seq_id, length_info, sliding_window):
if not sliding_window:
return pos
# pos if pos < sink_size else pos - sink_size + sliding_window_offset
return T.if_then_else(
pos < length_info[2, seq_id],
pos,
pos - length_info[2, seq_id] + length_info[1, seq_id],
)
def _alloc_softmax_state_buffers(tile_x, tile_z, bdx, num_warps):
"""Allocate the shared/local online-softmax working state used by every tiled prefill kernel.
Returns ``(S_smem, S_local, m_smem, m_prev_smem, d_smem, m_new, m_prev, d_new)``.
"""
S_smem = T.sblock_alloc_buffer((tile_x, tile_z), "float32", scope="shared")
S_local = T.sblock_alloc_buffer((tile_x, tile_z), "float32", scope="local")
m_smem = T.sblock_alloc_buffer((tile_x,), "float32", scope="shared")
m_prev_smem = T.sblock_alloc_buffer((tile_x,), "float32", scope="shared")
d_smem = T.sblock_alloc_buffer((tile_x,), "float32", scope="shared")
md_shape = (math.ceil(tile_x / (bdx * num_warps)),)
m_new = T.sblock_alloc_buffer(md_shape, "float32", scope="local")
m_prev = T.sblock_alloc_buffer(md_shape, "float32", scope="local")
d_new = T.sblock_alloc_buffer(md_shape, "float32", scope="local")
return S_smem, S_local, m_smem, m_prev_smem, d_smem, m_new, m_prev, d_new
def _alloc_mha_qkvo_buffers(tile_x, tile_z, d_qk, d_v, dtype):
"""Allocate Q/K/V shared + O local buffers for standard MHA/GQA prefill kernels."""
Q_smem = T.sblock_alloc_buffer((tile_x, d_qk), dtype, scope="shared")
K_smem = T.sblock_alloc_buffer((tile_z, d_qk), dtype, scope="shared")
V_smem = T.sblock_alloc_buffer((tile_z, d_v), dtype, scope="shared")
O_local = T.sblock_alloc_buffer((tile_x, d_v), "float32", scope="local")
return Q_smem, K_smem, V_smem, O_local
def _alloc_mla_qkvo_buffers(tile_x, tile_z, d_qk, d_latent, dtype):
"""Allocate Q + combined KV shared + O local for MLA prefill (V reuses the KV buffer)."""
Q_smem = T.sblock_alloc_buffer((tile_x, d_qk), dtype, scope="shared")
KV_smem = T.sblock_alloc_buffer((tile_z, d_qk), dtype, scope="shared")
O_local = T.sblock_alloc_buffer((tile_x, d_latent), "float32", scope="local")
return Q_smem, KV_smem, O_local
def _alloc_tile_walk_state():
"""Return (tile_id, batch_idx, batch_tiles, batch_rows, iterator, kv_chunk_len) int32 scalars for the paged/ragged/MLA tile-walk state machine."""
return _var("int32"), _var("int32"), _var("int32"), _var("int32"), _var("int32"), _var("int32")
def _make_prefill_macros(tile_x, tile_y, tile_z, tile_o, bdx, num_warps, group_size):
"""Build @T.macro helpers shared across tiled online-softmax prefill kernels.
Parameters
----------
tile_x : int # query/output row tile
tile_y : int # QK reduction dim (head_dim for MHA, d_qk for MLA/ragged)
tile_z : int # key/value column tile
tile_o : int # output/V column dim (d for MHA/sequence, d_v for ragged, d_latent for MLA)
"""
@T.macro
def init_states(
m_smem: T.Buffer, d_smem: T.Buffer, O_local: T.Buffer, ty: T.int32, tx: T.int32,
):
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
m_smem[row] = -5e4
d_smem[row] = 1.0
for li, lj in T.grid(tile_x, tile_o):
with T.sblock("O_init"):
i, j = T.axis.remap("SS", [li, lj])
O_local[i, j] = 0.0
T.tvm_storage_sync("shared")
@T.macro
def compute_s_gemm(
Q_smem: T.Buffer, K_smem: T.Buffer, S_local: T.Buffer, S_smem: T.Buffer, sm_scale: T.float32,
):
with T.sblock():
for li, lj, lk in T.grid(tile_x, tile_z, tile_y):
with T.sblock("S_gemm"):
i, j, k = T.axis.remap("SSR", [li, lj, lk])
with T.init():
S_local[i, j] = 0.0
S_local[i, j] += T.cast(Q_smem[i, k], "float32") * T.cast(K_smem[j, k], "float32") * sm_scale * math.log2(math.exp(1))
T.tvm_storage_sync("shared")
for li, lj in T.grid(tile_x, tile_z):
with T.sblock("S_store"):
i, j = T.axis.remap("SS", [li, lj])
S_smem[i, j] = S_local[i, j]
T.tvm_storage_sync("shared")
@T.macro
def softmax_update_causal(
S_smem: T.Buffer, m_smem: T.Buffer, d_smem: T.Buffer, m_prev_smem: T.Buffer,
m_new: T.Buffer, m_prev: T.Buffer, d_new: T.Buffer,
ty: T.int32, tx: T.int32, LH_start: T.int32, L_kv_start: T.int32,
causal: T.int32, kv_len: T.int32, qo_len: T.int32,
):
# Phase 1: compute m_new = max(masked S over kv tile), d_new = d_prev * exp2(m_prev - m_new)
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
with T.sblock("update1"):
m_prev[i] = m_smem[row]
m_new[i] = m_smem[row]
row_: T.let[T.int32] = (LH_start + row) // group_size
for j in T.serial(tile_z):
if _causal_mask(causal, row=row_, col=L_kv_start + j, kv_len=kv_len, qo_len=qo_len):
m_new[i] = T.max(m_new[i], S_smem[row, j])
d_new[i] = d_smem[row] * T.exp2(m_prev[i] - m_new[i])
# Phase 2: exp-and-scale S_smem; masked-out entries use -inf
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
with T.sblock("update"):
for j in T.serial(tile_z):
# predicate sits inside loop so sync stays outside conditional branches
if row < tile_x:
row_: T.let[T.int32] = (LH_start + row) // group_size
if _causal_mask(causal, row=row_, col=L_kv_start + j, kv_len=kv_len, qo_len=qo_len):
S_smem[row, j] = T.exp2(S_smem[row, j] - m_new[i])
else:
S_smem[row, j] = T.exp2(-5e4 - m_new[i])
# Phase 3: d_new += sum(S_smem[row, :]); write m/d/m_prev back to smem
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
with T.sblock("update"):
for j in T.serial(tile_z):
d_new[i] += S_smem[row, j]
m_smem[row] = m_new[i]
d_smem[row] = d_new[i]
m_prev_smem[row] = m_prev[i]
T.tvm_storage_sync("shared")
@T.macro
def compute_o_gemm(
S_smem: T.Buffer, V_smem: T.Buffer, O_local: T.Buffer,
m_prev_smem: T.Buffer, m_smem: T.Buffer,
):
with T.sblock():
for li, lj, lk in T.grid(tile_x, tile_o, tile_z):
with T.sblock("O_gemm"):
i, j, k = T.axis.remap("SSR", [li, lj, lk])
with T.init():
O_local[i, j] *= T.exp2(m_prev_smem[i] - m_smem[i])
O_local[i, j] += S_smem[i, k] * T.cast(V_smem[k, j], "float32")
@T.macro
def paged_store_output_lse(
output: T.Buffer, lse: T.Buffer, O_local: T.Buffer, m_smem: T.Buffer, d_smem: T.Buffer,
q_indptr: T.Buffer, b_idx: T.int32, by: T.int32, LH_start: T.int32,
):
"""Paged-style (q_indptr-based) O_store + lse_store epilogue.
Used by paged prefill, ragged prefill and MLA prefill. MLA passes ``by=0`` so
the ``by * group_size`` term drops to zero at compile time.
"""
for li, lj in T.grid(tile_x, tile_o):
with T.sblock("O_store"):
i, j = T.axis.remap("SS", [li, lj])
cur_L: T.let[T.int32] = q_indptr[b_idx] + (LH_start + i) // group_size
cur_H_qo: T.let[T.int32] = by * group_size + (LH_start + i) % group_size
if cur_L < q_indptr[b_idx + 1]:
output[cur_L, cur_H_qo, j] = O_local[i, j] / d_smem[i]
for li in T.grid(tile_x):
with T.sblock("lse_store"):
i = T.axis.remap("S", [li])
cur_L: T.let[T.int32] = q_indptr[b_idx] + (LH_start + i) // group_size
cur_H_qo: T.let[T.int32] = by * group_size + (LH_start + i) % group_size
if cur_L < q_indptr[b_idx + 1]:
lse[cur_L, cur_H_qo] = m_smem[i] + T.log2(d_smem[i])
@T.macro
def advance_tile_batch(
tile_id: T.Buffer, batch_idx: T.Buffer, batch_tiles: T.Buffer, batch_rows: T.Buffer,
q_indptr: T.Buffer, batch_size: T.int32,
):
"""Advance tile_id/batch_idx past exhausted batches.
After the loop, either batch_idx[0] >= batch_size (all tiles consumed) or
tile_id[0] < batch_tiles[0] (the current batch still has work to do).
"""
while tile_id[0] >= batch_tiles[0] and batch_idx[0] < batch_size:
tile_id[0] -= batch_tiles[0]
batch_idx[0] += 1
if batch_idx[0] < batch_size:
b_idx: T.let[T.int32] = batch_idx[0]
batch_rows[0] = (q_indptr[b_idx + 1] - q_indptr[b_idx]) * group_size
batch_tiles[0] = T.ceildiv(batch_rows[0], tile_x)
@T.macro
def softmax_update_valid_length(
S_smem: T.Buffer, m_smem: T.Buffer, d_smem: T.Buffer, m_prev_smem: T.Buffer,
m_new: T.Buffer, m_prev: T.Buffer, d_new: T.Buffer,
ty: T.int32, tx: T.int32, LH_start: T.int32, L_kv_start: T.int32,
valid_len: T.int32, qo_len: T.int32, kv_len: T.int32,
):
# Same three-phase online softmax as softmax_update_causal but with a
# per-batch right-padding mask in place of causal masking.
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
with T.sblock("update1"):
m_prev[i] = m_smem[row]
m_new[i] = m_smem[row]
row_: T.let[T.int32] = (LH_start + row) // group_size
for j in T.serial(tile_z):
if tirx.And(tirx.And(row_ < qo_len, row_ < valid_len), L_kv_start + j < valid_len):
m_new[i] = T.max(m_new[i], S_smem[row, j])
d_new[i] = d_smem[row] * T.exp2(m_prev[i] - m_new[i])
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
with T.sblock("update"):
for j in T.serial(tile_z):
if row < tile_x:
row_: T.let[T.int32] = (LH_start + row) // group_size
if tirx.And(tirx.And(row_ < qo_len, row_ < valid_len), L_kv_start + j < valid_len):
S_smem[row, j] = T.exp2(S_smem[row, j] - m_new[i])
else:
S_smem[row, j] = T.exp2(-5e4 - m_new[i])
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
with T.sblock("update"):
for j in T.serial(tile_z):
d_new[i] += S_smem[row, j]
m_smem[row] = m_new[i]
d_smem[row] = d_new[i]
m_prev_smem[row] = m_prev[i]
T.tvm_storage_sync("shared")
@T.macro
def softmax_update_causal_padded_left(
S_smem: T.Buffer, m_smem: T.Buffer, d_smem: T.Buffer, m_prev_smem: T.Buffer,
m_new: T.Buffer, m_prev: T.Buffer, d_new: T.Buffer,
ty: T.int32, tx: T.int32, LH_start: T.int32, L_kv_start: T.int32,
valid_len: T.int32, qo_len: T.int32, kv_len: T.int32,
):
# Three-phase online softmax with left-padding + causal mask. Real
# queries occupy [qo_len - valid_len, qo_len); real keys occupy
# [kv_len - valid_len, kv_len). Causal keeps
# col <= row + (kv_len - qo_len) within those valid suffixes.
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
with T.sblock("update1"):
m_prev[i] = m_smem[row]
m_new[i] = m_smem[row]
row_: T.let[T.int32] = (LH_start + row) // group_size
pad_q: T.let[T.int32] = qo_len - valid_len
pad_kv: T.let[T.int32] = kv_len - valid_len
for j in T.serial(tile_z):
col_: T.let[T.int32] = L_kv_start + j
if tirx.And(tirx.And(row_ < qo_len, row_ >= pad_q), tirx.And(col_ >= pad_kv, col_ < kv_len - qo_len + row_ + 1)):
m_new[i] = T.max(m_new[i], S_smem[row, j])
d_new[i] = d_smem[row] * T.exp2(m_prev[i] - m_new[i])
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
with T.sblock("update"):
for j in T.serial(tile_z):
if row < tile_x:
row_: T.let[T.int32] = (LH_start + row) // group_size
pad_q: T.let[T.int32] = qo_len - valid_len
pad_kv: T.let[T.int32] = kv_len - valid_len
col_: T.let[T.int32] = L_kv_start + j
if tirx.And(tirx.And(row_ < qo_len, row_ >= pad_q), tirx.And(col_ >= pad_kv, col_ < kv_len - qo_len + row_ + 1)):
S_smem[row, j] = T.exp2(S_smem[row, j] - m_new[i])
else:
S_smem[row, j] = T.exp2(-5e4 - m_new[i])
for i in T.serial(T.ceildiv(tile_x, bdx * num_warps)):
row: T.let[T.int32] = i * bdx * num_warps + ty * bdx + tx
if row < tile_x:
with T.sblock("update"):
for j in T.serial(tile_z):
d_new[i] += S_smem[row, j]
m_smem[row] = m_new[i]
d_smem[row] = d_new[i]
m_prev_smem[row] = m_prev[i]
T.tvm_storage_sync("shared")
return init_states, compute_s_gemm, softmax_update_causal, compute_o_gemm, softmax_update_valid_length, advance_tile_batch, paged_store_output_lse, softmax_update_causal_padded_left
def _get_prefill_kernel_config(h_kv, h_q, d, dtype, target: Target):
NUM_BLKS = 16
LOAD_VEC = 8 // ((DataType(dtype).bits + 7) // 8) # 8 bytes
group_size = h_q // h_kv
bdx = 32
num_warps = 4
tile_x, tile_y, tile_z = (
64 // ((DataType(dtype).bits + 7) // 8) // max(d // 128, 1),
d,
64 // ((DataType(dtype).bits + 7) // 8) // max(d // 128, 1),
)
original_tile_y = tile_y
original_tile_z = tile_z
while (tile_x * tile_z) % (bdx * num_warps) != 0:
tile_z += original_tile_z
while (tile_x * tile_y) % (bdx * num_warps) != 0:
tile_y += original_tile_y
# Otherwise we would exceed maxComputeWorkgroupStorageSize
if (
target.kind.name == "webgpu"
and ((d + 127) // 128) * ((DataType(dtype).bits + 15) // 16) >= 4
):
tile_z = 8
num_warps = 2
if target.kind.name == "opencl" and (
("android" in str(target.host)) or ("adreno" in str(target.attrs))
):
LOAD_VEC = 16 // ((DataType(dtype).bits + 7) // 8) # 16 bytes
NUM_BLKS = group_size * 8
check_thread_limits(target, bdx=bdx, bdy=num_warps, bdz=1, gdz=1)
return NUM_BLKS, LOAD_VEC, group_size, bdx, num_warps, tile_x, tile_y, tile_z
def _schedule_prefill_kernel(sch: s_tir.Schedule, load_vec, bdx, num_warps, tile_x, tile_y, tile_z, transform_k_load: bool, merged_qk_load: bool) -> tvm.s_tir.Schedule:
get_extent = lambda *lps: [int(sch.get(lp).extent) for lp in lps]
def get_vecsize(extent):
return min(load_vec, (extent & ~(extent - 1)))
def getxy_vecsize(x, y, t):
assert (x * y) % t == 0
return min(get_vecsize(y), get_vecsize(x * y // t))
def get_tile_size(x, y, t):
cnt = (x * y) // t
assert (x * y) % t == 0
tile_y = math.ceil(math.sqrt(cnt))
while (cnt % tile_y != 0 or y % tile_y != 0 or x % (cnt // tile_y) != 0) and tile_y <= cnt:
tile_y += 1
assert tile_y <= cnt
tile_x = cnt // tile_y
return tile_x, tile_y
def apply_to_qkv_load(sch: s_tir.Schedule, block):
loop_x, loop_y = sch.get_loops(block)[-2:]
x_extent, y_extent = get_extent(loop_x, loop_y)
vec_size = getxy_vecsize(x_extent, y_extent, bdx * num_warps)
yo, yv = sch.split(loop_y, [None, vec_size])
yo_extent = y_extent // vec_size
tile_x, tile_y = get_tile_size(x_extent, yo_extent, (bdx * num_warps))
xo, xi = sch.split(loop_x, [tile_x, None])
yo, yi = sch.split(yo, [tile_y, None])
sch.reorder(xi, yi, xo, yo)
t = sch.fuse(xi, yi)
ty, tx = sch.split(t, [num_warps, bdx])
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
sch.vectorize(yv)
def apply_to_so_ewise(sch: s_tir.Schedule, block, tile):
loop_x, loop_y = sch.get_loops(block)[-2:]
xo, xi = sch.split(loop_x, factors=[None, tile[0]])
yo, yi = sch.split(loop_y, factors=[None, tile[1]])
sch.reorder(xo, yo, xi, yi)
yiv_extent = get_vecsize(tile[1])
yio, yiv = sch.split(yi, [None, yiv_extent])
sch.unroll(yio)
sch.vectorize(yiv)
t = sch.fuse(xo, yo)
ty, tx = sch.split(t, factors=[None, bdx])
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
def apply_to_gemm(sch: s_tir.Schedule, block, tile, r_len=16, k_major=False):
loop_x, loop_y, loop_z = sch.get_loops(block)[-3:]
xo, xi = sch.split(loop_x, factors=[None, tile[0]])
yo, yi = sch.split(loop_y, factors=[None, tile[1]])
sch.reorder(xo, yo, xi, yi)
t = sch.fuse(xo, yo)
ty, tx = sch.split(t, factors=[None, bdx])
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
ko, ki = sch.split(loop_z, factors=[None, r_len])
if k_major:
sch.reorder(ko, xi, yi, ki)
else:
sch.reorder(ko, ki, xi, yi)
yiv_extent = get_vecsize(tile[1])
yio, yiv = sch.split(yi, [None, yiv_extent])
sch.unroll(yio)
sch.vectorize(yiv)
sch.unroll(xi)
sch.decompose_reduction(block, ty)
def apply_to_md(sch, block):
loop = sch.get_loops(block)[-1]
_, ty, tx = sch.split(loop, factors=[None, num_warps, bdx])
sch.bind(ty, "threadIdx.y")
sch.bind(tx, "threadIdx.x")
if transform_k_load and not merged_qk_load:
sch.transform_layout("K_load", ("write", 0), lambda i, j: (j, i))
tile_s = get_tile_size(tile_x, tile_z, bdx * num_warps)
tile_o = get_tile_size(tile_x, tile_y, bdx * num_warps)
apply_to_gemm(sch, sch.get_sblock("S_gemm"), tile_s, k_major=True)
apply_to_gemm(sch, sch.get_sblock("O_gemm"), tile_o, k_major=False)
apply_to_so_ewise(sch, sch.get_sblock("S_store"), tile_s)
apply_to_so_ewise(sch, sch.get_sblock("O_init"), tile_o)
apply_to_so_ewise(sch, sch.get_sblock("O_store"), tile_o)
apply_to_qkv_load(sch, sch.get_sblock("Q_load"))
if not merged_qk_load:
apply_to_qkv_load(sch, sch.get_sblock("K_load"))
apply_to_qkv_load(sch, sch.get_sblock("V_load"))
else:
apply_to_qkv_load(sch, sch.get_sblock("KV_load"))
apply_to_md(sch, sch.get_sblock("lse_store"))
return sch