# 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