chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
CI / MacOS (push) Has been cancelled
CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:36:25 +08:00
commit 26446540fa
3151 changed files with 974126 additions and 0 deletions
@@ -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.
"""LLM support for PyTorch-like API to build IRModules."""
from . import kv_cache, position_embedding
from .position_embedding import llama_rope
from .tree_attn import tree_attn
from .kv_cache import PagedKVCache
@@ -0,0 +1,526 @@
# 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
# fmt: off
"""Single-token decode attention kernels and attention-state merge helpers.
Contents:
- ``_attention_decode_cpu`` / ``_attention_decode`` — paged-KV decode (one Q token
per sequence), CPU scalar and GPU allreduce variants.
- ``_merge_state_inplace_cpu`` / ``_merge_state_inplace`` — combine two
log-sum-exp attention outputs in place. Used by multi-stage decoding and by
the distributed KV-transfer path.
"""
# pylint: disable=too-many-statements,too-many-arguments,invalid-name,line-too-long
import math
from typing import Any
from tvm.script import tirx as T
from tvm.target import Target
from ._kernel_common import (
_declare_length_info,
_get_kv_chunk_len,
_get_seq_offset,
_rope,
_var,
_var_cpu,
check_thread_limits,
get_max_num_threads_per_block,
)
def _attention_decode_cpu(num_kv_heads, num_qo_heads, head_dim, qkv_dtype, sliding_window: bool, rope_scaling: dict[str, Any], page_size: int = 16):
H_qo = num_qo_heads
H_kv = num_kv_heads
D = head_dim
group_size = num_qo_heads // num_kv_heads
global_symbol = "batch_decode_paged_kv_cpu"
if sliding_window:
global_symbol += "_sliding_window"
@T.prim_func(s_tir=True)
def batch_decode_paged_kv(
Q_handle: T.handle,
pages_handle: T.handle,
page_table_indptr_handle: T.handle,
page_table_values_handle: T.handle,
var_length_info: T.handle, # [b] when sliding window = False, or otherwise [3, b]
k_rope_pos_offset_handle: T.handle,
q_rope_position_handle: T.handle,
output_handle: T.handle,
lse_handle: T.handle,
rotary_mode: T.int32,
rope_scale: T.float32,
rope_theta: T.float32,
sm_scale: T.float32,
):
T.func_attr({"tirx.is_scheduled": True, "global_symbol": global_symbol})
B = T.int32()
nnz_pages = T.int32()
max_num_pages = T.int32()
page_indptr_elem_offset = T.int32()
page_values_elem_offset = T.int32()
k_rope_pos_offset_elem_offset = T.int32()
q_rope_position_elem_offset = T.int32()
length_info_elem_offset = T.int32()
Q = T.match_buffer(Q_handle, (B, H_qo, D), qkv_dtype)
pages = T.match_buffer(pages_handle, (max_num_pages, 2, H_kv, page_size, D), qkv_dtype)
page_table_indptr = T.match_buffer(page_table_indptr_handle, (B + 1,), "int32", elem_offset=page_indptr_elem_offset)
page_table_values = T.match_buffer(page_table_values_handle, (nnz_pages,), "int32", elem_offset=page_values_elem_offset)
k_rope_pos_offset = T.match_buffer(k_rope_pos_offset_handle, (B,), "int32", elem_offset=k_rope_pos_offset_elem_offset)
q_rope_position = T.match_buffer(q_rope_position_handle, (B,), "int32", elem_offset=q_rope_position_elem_offset)
output = T.match_buffer(output_handle, (B, H_qo, D), qkv_dtype)
lse = T.match_buffer(lse_handle, (B, H_qo), "float32") # pylint: disable=unused-variable
# The length information of the sequences.
# - It is in shape `(3, batch_size)` when sliding window is enabled.
# For a sequence "i", location
# - "(0, i)" is the number of KV slots used in the last page of the seq ("last_page_len"),
# - "(1, i)" is the starting offset of the sliding window in the seq,
# - "(2, i)" is the attn sink length of the sequence.
# - It is in shape `(batch_size,)` when sliding window is disabled,
# denoting the "last_page_len".
length_info = _declare_length_info(var_length_info, B, sliding_window, length_info_elem_offset)
for b in T.serial(B):
with T.sblock("attn"):
O_local = T.sblock_alloc_buffer((D,), "float32")
Q_local = T.sblock_alloc_buffer((D,), "float32")
K_local = T.sblock_alloc_buffer((D,), "float32")
V_local = T.sblock_alloc_buffer((D,), "float32")
kv_chunk_len = T.sblock_alloc_buffer((1,), "int32")
m_val = T.sblock_alloc_buffer((1,), "float32")
new_m = T.sblock_alloc_buffer((1,), "float32")
d_val = T.sblock_alloc_buffer((1,), "float32")
S_val = T.sblock_alloc_buffer((1,), "float32")
scale_O = T.sblock_alloc_buffer((1,), "float32")
factor = T.sblock_alloc_buffer((1,), "float32")
cur_page_indptr_begin: T.let[T.int32] = page_table_indptr[b]
cur_page_indptr_end: T.let[T.int32] = page_table_indptr[b + 1]
kv_chunk_len[0] = T.if_then_else(
cur_page_indptr_begin != cur_page_indptr_end,
_get_kv_chunk_len(cur_page_indptr_end - cur_page_indptr_begin, page_size, b, length_info, sliding_window),
0,
)
for h_qo in T.serial(H_qo):
m_val[0] = -5e4
d_val[0] = 1.0
for d in T.serial(D):
O_local[d] = 0.0
for d in T.serial(D):
Q_local[d] = T.if_then_else(
rotary_mode == 1,
_rope(Q, q_rope_position[b], head_dim, rope_theta, rope_scale, (b, h_qo, d), qkv_dtype, rope_scaling),
Q[b, h_qo, d],
)
for row_idx in T.serial(kv_chunk_len[0]):
seq_offset: T.let[T.int32()] = _get_seq_offset(row_idx, b, length_info, sliding_window)
page_no: T.let[T.int32()] = page_table_values[cur_page_indptr_begin + (seq_offset // page_size)]
page_offset: T.let[T.int32()] = seq_offset % page_size
for d in T.serial(D):
K_local[d] = T.if_then_else(
rotary_mode == 1,
_rope(pages, k_rope_pos_offset[b] + row_idx, head_dim, rope_theta, rope_scale, (page_no, 0, h_qo // group_size, page_offset, d), qkv_dtype, rope_scaling),
pages[page_no, 0, h_qo // group_size, page_offset, d],
)
S_val[0] = 0.0
for d in T.serial(D):
S_val[0] += Q_local[d] * K_local[d]
S_val[0] *= sm_scale * math.log2(math.exp(1))
new_m[0] = T.max(m_val[0], S_val[0])
d_val[0] = (d_val[0] * T.exp2(m_val[0] - new_m[0])) + T.exp2(S_val[0] - new_m[0])
scale_O[0] = T.exp2(m_val[0] - new_m[0])
for d in T.serial(D):
O_local[d] = O_local[d] * scale_O[0]
m_val[0] = new_m[0]
for d in T.serial(D):
V_local[d] = pages[page_no, 1, h_qo // group_size, page_offset, d]
factor[0] = T.exp2(S_val[0] - m_val[0])
for d in T.serial(D):
O_local[d] = O_local[d] + V_local[d] * factor[0]
for d in T.serial(D):
O_local[d] = O_local[d] / d_val[0]
output[b, h_qo, d] = O_local[d]
lse[b, h_qo] = m_val[0] + T.log2(d_val[0])
return batch_decode_paged_kv
def _attention_decode(num_kv_heads, num_qo_heads, head_dim, qkv_dtype, sliding_window: bool, rope_scaling: dict[str, Any], target: Target, page_size: int = 16):
qkv_dtype_bytes = 2
H_qo = num_qo_heads
H_kv = num_kv_heads
D = head_dim
THREAD_LIMIT = 512
TILE_SIZE_PER_BDX = 2
if target.kind.name == "opencl" and (("android" in str(target.host)) or ("adreno" in str(target.attrs))):
# Keeping lower thread limit for this kernel on adreno target
# to avoid register spill
THREAD_LIMIT = 256
TILE_SIZE_PER_BDX = 1
max_num_threads_per_block = get_max_num_threads_per_block(target)
thread_limit = min(max_num_threads_per_block, THREAD_LIMIT)
GROUP_SIZE = H_qo // H_kv
VEC_SIZE = min(max(8 // qkv_dtype_bytes, D // 32), 4)
bdx = D // VEC_SIZE
bdy = GROUP_SIZE
while bdx * bdy > thread_limit and bdy > 1:
bdy //= 2
gdz = GROUP_SIZE // bdy
threads_per_CTA = max(thread_limit, bdx * bdy)
bdz = threads_per_CTA // (bdx * bdy)
tile_size_per_bdx = TILE_SIZE_PER_BDX if GROUP_SIZE == 1 else 1
check_thread_limits(target, bdx=bdx, bdy=bdy, bdz=bdz, gdz=1)
global_symbol = "batch_decode_paged_kv"
if sliding_window:
global_symbol += "_sliding_window"
# pylint: disable=too-many-branches
@T.prim_func(s_tir=True)
def batch_decode_paged_kv(
Q_handle: T.handle,
pages_handle: T.handle,
page_table_indptr_handle: T.handle,
page_table_values_handle: T.handle,
var_length_info: T.handle, # [b] when sliding window = False, or otherwise [3, b]
k_rope_pos_offset_handle: T.handle,
q_rope_position_handle: T.handle,
output_handle: T.handle,
lse_handle: T.handle,
rotary_mode: T.int32,
rope_scale: T.float32,
rope_theta: T.float32,
sm_scale: T.float32,
):
T.func_attr({"tirx.is_scheduled": True, "global_symbol": global_symbol})
B = T.int32()
nnz_pages = T.int32()
max_num_pages = T.int32()
pages_elem_offset = T.int64()
page_indptr_elem_offset = T.int32()
page_values_elem_offset = T.int32()
k_rope_pos_offset_elem_offset = T.int32()
q_rope_position_elem_offset = T.int32()
length_info_elem_offset = T.int32()
Q = T.match_buffer(Q_handle, (B, H_qo, D), qkv_dtype)
pages = T.match_buffer(pages_handle, (max_num_pages, 2, H_kv, page_size, D), qkv_dtype, elem_offset=pages_elem_offset)
page_table_indptr = T.match_buffer(page_table_indptr_handle, (B + 1,), "int32", elem_offset=page_indptr_elem_offset)
page_table_values = T.match_buffer(page_table_values_handle, (nnz_pages,), "int32", elem_offset=page_values_elem_offset)
k_rope_pos_offset = T.match_buffer(k_rope_pos_offset_handle, (B,), "int32", elem_offset=k_rope_pos_offset_elem_offset)
q_rope_position = T.match_buffer(q_rope_position_handle, (B,), "int32", elem_offset=q_rope_position_elem_offset)
output = T.match_buffer(output_handle, (B, H_qo, D), qkv_dtype)
lse = T.match_buffer(lse_handle, (B, H_qo), "float32") # pylint: disable=unused-variable
length_info = _declare_length_info(var_length_info, B, sliding_window, length_info_elem_offset)
for bx in T.thread_binding(B, thread="blockIdx.x"):
for fused_by_bz in T.thread_binding(H_kv * gdz, thread="blockIdx.y"):
for ty in T.thread_binding(bdy, thread="threadIdx.y"):
for tx in T.thread_binding(bdx, thread="threadIdx.x"):
for tz in T.thread_binding(bdz, thread="threadIdx.z"):
with T.sblock("attn"):
Q_local = T.sblock_alloc_buffer((VEC_SIZE,), qkv_dtype, scope="local")
kv_chunk_len = T.sblock_alloc_buffer((1,), "int32", scope="local")
K_smem = T.sblock_alloc_buffer((bdz * bdy * tile_size_per_bdx, D), qkv_dtype, scope="shared")
V_smem = T.sblock_alloc_buffer((bdz * bdy * tile_size_per_bdx, D), qkv_dtype, scope="shared")
O_allreduce = T.sblock_alloc_buffer((bdz, bdy, D), "float32", scope="shared")
md_allreduce = T.sblock_alloc_buffer((bdz, bdy, 2), "float32", scope="shared")
S_reduce_local = T.sblock_alloc_buffer((1,), "float32", scope="local")
t0 = T.sblock_alloc_buffer((1,), "float32", scope="local")
S_local = T.sblock_alloc_buffer((bdy * tile_size_per_bdx), "float32", scope="local")
QK_local = T.sblock_alloc_buffer((VEC_SIZE,), "float32", scope="local")
V_local = T.sblock_alloc_buffer((VEC_SIZE,), qkv_dtype, scope="local")
m_prev = T.sblock_alloc_buffer((1,), "float32", scope="local")
d_prev = T.sblock_alloc_buffer((1,), "float32", scope="local")
other_m = T.sblock_alloc_buffer((1,), "float32", scope="local")
other_d = T.sblock_alloc_buffer((1,), "float32", scope="local")
exp_mprev = T.sblock_alloc_buffer((1,), "float32", scope="local")
exp_otherm = T.sblock_alloc_buffer((1,), "float32", scope="local")
other_o = T.sblock_alloc_buffer((VEC_SIZE,), "float32", scope="local")
st_m = T.sblock_alloc_buffer((1,), "float32", scope="local")
st_d = T.sblock_alloc_buffer((1,), "float32", scope="local")
O_local = T.sblock_alloc_buffer((VEC_SIZE,), "float32", scope="local")
by: T.let[T.int32] = fused_by_bz % H_kv
bz: T.let[T.int32] = fused_by_bz // H_kv
batch_idx: T.let[T.int32] = bx
cur_page_indptr_begin: T.let[T.int32] = page_table_indptr[batch_idx]
cur_page_indptr_end: T.let[T.int32] = page_table_indptr[batch_idx + 1]
kv_chunk_len[0] = T.if_then_else(
cur_page_indptr_begin != cur_page_indptr_end,
_get_kv_chunk_len(cur_page_indptr_end - cur_page_indptr_begin, page_size, batch_idx, length_info, sliding_window),
0
)
# init states
st_m[0] = -5e4
st_d[0] = 1.0
for vec in T.vectorized(VEC_SIZE):
O_local[vec] = 0.0
# load q
for vec in T.vectorized(VEC_SIZE):
Q_local[vec] = T.if_then_else(
rotary_mode == 1,
_rope(Q, q_rope_position[batch_idx], head_dim, rope_theta, rope_scale, (bx, by * GROUP_SIZE + bz * bdy + ty, tx * VEC_SIZE + vec), qkv_dtype, rope_scaling),
Q[bx, by * GROUP_SIZE + bz * bdy + ty, tx * VEC_SIZE + vec]
)
for iterator in T.serial(T.ceildiv(kv_chunk_len[0], tile_size_per_bdx * bdy * bdz)):
tile_start_s: T.let[T.int32()] = (tz * bdy + ty) * tile_size_per_bdx # type: ignore
tile_start_g: T.let[T.int32()] = ((iterator * bdz + tz) * bdy + ty) * tile_size_per_bdx # type: ignore
# load KV from global memory to shared memory
for j in T.serial(tile_size_per_bdx):
with T.sblock("KV_load"):
T.reads()
T.writes()
row_g: T.let[T.int32()] = tile_start_g + j # type: ignore
if row_g < kv_chunk_len[0]:
seq_offset: T.let[T.int32()] = _get_seq_offset(row_g, batch_idx, length_info, sliding_window) # type: ignore
page_no: T.let[T.int32()] = page_table_values[cur_page_indptr_begin + T.floordiv(seq_offset, page_size)] # type: ignore
page_offset: T.let[T.int32()] = T.floormod(seq_offset, page_size) # type: ignore
for vec in T.vectorized(VEC_SIZE):
K_smem[tile_start_s + j, tx * VEC_SIZE + vec] = T.if_then_else(
rotary_mode == 1,
_rope(pages, k_rope_pos_offset[batch_idx] + row_g, head_dim, rope_theta, rope_scale, (page_no, 0, by, page_offset, tx * VEC_SIZE + vec), qkv_dtype, rope_scaling),
pages[page_no, 0, by, page_offset, tx * VEC_SIZE + vec]
)
V_smem[tile_start_s + j, tx * VEC_SIZE + vec] = pages[page_no, 1, by, page_offset, tx * VEC_SIZE + vec]
else:
for vec in T.vectorized(VEC_SIZE):
K_smem[tile_start_s + j, tx * VEC_SIZE + vec] = 0.0
V_smem[tile_start_s + j, tx * VEC_SIZE + vec] = 0.0
T.tvm_storage_sync("shared")
# compute QK
m_prev[0] = st_m[0]
for j in T.serial(bdy * tile_size_per_bdx):
# compute S = Q * K * sm_scale
for vec in T.vectorized(VEC_SIZE):
QK_local[vec] = T.cast(Q_local[vec], "float32") * T.cast(K_smem[tz * bdy * tile_size_per_bdx + j, tx * VEC_SIZE + vec], "float32") * sm_scale * math.log2(math.exp(1))
S_reduce_local[0] = 0
for vec in T.unroll(VEC_SIZE):
S_reduce_local[0] += QK_local[vec]
with T.sblock("block_cross_thread"):
T.reads(S_reduce_local[0])
T.writes(t0[0])
T.attr(
T.comm_reducer(lambda x0, y0: x0 + y0, [T.float32(0)]),
"reduce_scope",
T.int32(0),
)
T.tvm_thread_allreduce(T.uint32(1), S_reduce_local[0], True, t0[0], tx, dtype="void")
S_local[j] = -5e4
if (iterator * bdz + tz) * bdy * tile_size_per_bdx + j < kv_chunk_len[0]:
S_local[j] = t0[0]
# update st_m
st_m[0] = T.max(st_m[0], S_local[j])
# update st_d, st_O
o_scale: T.let[T.float32] = T.exp2(m_prev[0] - st_m[0])
st_d[0] *= o_scale
for j in T.serial(bdy * tile_size_per_bdx):
S_local[j] = T.exp2(S_local[j] - st_m[0])
st_d[0] += S_local[j]
for j in T.vectorized(VEC_SIZE):
O_local[j] *= o_scale
# load V from shared memory to local memory
# compute O
for j in T.serial(bdy * tile_size_per_bdx):
for vec in T.vectorized(VEC_SIZE):
V_local[vec] = V_smem[tz * bdy * tile_size_per_bdx + j, tx * VEC_SIZE + vec]
for vec in T.vectorized(VEC_SIZE):
O_local[vec] += T.cast(V_local[vec], "float32") * S_local[j]
if bdz > 1:
# allreduce over bdz
for vec in T.vectorized(VEC_SIZE):
O_allreduce[tz, ty, tx * VEC_SIZE + vec] = O_local[vec]
md_allreduce[tz, ty, 0] = st_m[0]
md_allreduce[tz, ty, 1] = st_d[0]
T.tvm_storage_sync("shared")
st_m[0] = -5e4
st_d[0] = 1.0
for vec in T.vectorized(VEC_SIZE):
O_local[vec] = 0.0
for j in T.serial(bdz):
m_prev[0] = st_m[0]
d_prev[0] = st_d[0]
other_m[0] = md_allreduce[j, ty, 0]
other_d[0] = md_allreduce[j, ty, 1]
for vec in T.vectorized(VEC_SIZE):
other_o[vec] = O_allreduce[j, ty, tx * VEC_SIZE + vec]
st_m[0] = T.max(st_m[0], other_m[0])
st_d[0] = d_prev[0] * T.exp2(m_prev[0] - st_m[0]) + other_d[0] * T.exp2(other_m[0] - st_m[0])
exp_mprev[0] = T.exp2(m_prev[0] - st_m[0])
exp_otherm[0] = T.exp2(other_m[0] - st_m[0])
for vec in T.vectorized(VEC_SIZE):
O_local[vec] = O_local[vec] * exp_mprev[0] + other_o[vec] * exp_otherm[0]
# normalize O
for vec in T.vectorized(VEC_SIZE):
O_local[vec] /= st_d[0]
# store O to global memory
for vec in T.vectorized(VEC_SIZE):
output[batch_idx, by * GROUP_SIZE + bz * bdy + ty, tx * VEC_SIZE + vec] = O_local[vec]
# store lse to global memory
lse[batch_idx, by * GROUP_SIZE + bz * bdy + ty] = st_m[0] + T.log2(st_d[0])
# pylint: enable=too-many-branches
return batch_decode_paged_kv
def _merge_state_inplace_cpu(v_dtype):
@T.prim_func(s_tir=True)
def merge_state_inplace_cpu(
v: T.handle,
s: T.handle,
v_other: T.handle,
s_other: T.handle,
):
T.func_attr({"tirx.is_scheduled": True})
N = T.int32()
H = T.int32()
D = T.int32()
V = T.match_buffer(v, (N, H, D), v_dtype)
S = T.match_buffer(s, (N, H), "float32")
V_other = T.match_buffer(v_other, (N, H, D), v_dtype)
S_other = T.match_buffer(s_other, (N, H), "float32")
for n in T.serial(N):
for h in T.serial(H):
with T.sblock("merge"):
s_val = _var_cpu("float32")
s_other_val = _var_cpu("float32")
s_max = _var_cpu("float32")
scale = _var_cpu("float32")
other_scale = _var_cpu("float32")
s_val[0] = S[n, h]
s_other_val[0] = S_other[n, h]
s_max[0] = T.max(s_val[0], s_other_val[0])
s_val[0] = T.exp2(s_val[0] - s_max[0])
s_other_val[0] = T.exp2(s_other_val[0] - s_max[0])
scale[0] = s_val[0] / (s_val[0] + s_other_val[0])
other_scale[0] = s_other_val[0] / (s_val[0] + s_other_val[0])
for d in T.serial(D):
V[n, h, d] = V[n, h, d] * scale[0] + V_other[n, h, d] * other_scale[0]
S[n, h] = T.log2(s_val[0] + s_other_val[0]) + s_max[0]
return merge_state_inplace_cpu
def _merge_state_inplace(num_heads, head_dim, v_dtype, target: Target, global_symbol: str | None = None):
v_dtype_bytes = 2
VEC_SIZE = min(max(8 // v_dtype_bytes, head_dim // 32), 4)
bdx = head_dim // VEC_SIZE
bdy = num_heads
max_num_threads_per_block = get_max_num_threads_per_block(target)
while bdx * bdy > max_num_threads_per_block and bdy > 1:
bdy //= 2
gdy = num_heads // bdy
check_thread_limits(target, bdx=bdx, bdy=bdy, bdz=1, gdz=1)
@T.prim_func(s_tir=True)
def merge_state_inplace(
v: T.handle,
s: T.handle,
v_other: T.handle,
s_other: T.handle,
):
T.func_attr({"tirx.is_scheduled": True})
N = T.int32()
H = T.int32()
D = T.int32()
V = T.match_buffer(v, (N, H, D), v_dtype)
S = T.match_buffer(s, (N, H), "float32")
V_other = T.match_buffer(v_other, (N, H, D), v_dtype)
S_other = T.match_buffer(s_other, (N, H), "float32")
for bx in T.thread_binding(N, thread="blockIdx.x"):
for by in T.thread_binding(gdy, thread="blockIdx.y"):
for ty in T.thread_binding(bdy, thread="threadIdx.y"):
for tx in T.thread_binding(bdx, thread="threadIdx.x"):
with T.sblock("merge"):
s_val = _var("float32")
s_other_val = _var("float32")
s_max = _var("float32")
scale = _var("float32")
other_scale = _var("float32")
v_vec = T.sblock_alloc_buffer((VEC_SIZE,), v_dtype, scope="local")
v_other_vec = T.sblock_alloc_buffer((VEC_SIZE,), v_dtype, scope="local")
s_val[0] = S[bx, ty + by * bdy]
s_other_val[0] = S_other[bx, ty + by * bdy]
s_max[0] = T.max(s_val[0], s_other_val[0])
s_val[0] = T.exp2(s_val[0] - s_max[0])
s_other_val[0] = T.exp2(s_other_val[0] - s_max[0])
scale[0] = s_val[0] / (s_val[0] + s_other_val[0])
other_scale[0] = s_other_val[0] / (s_val[0] + s_other_val[0])
# load v
for vec in T.vectorized(VEC_SIZE):
v_vec[vec] = V[bx, ty + by * bdy, tx * VEC_SIZE + vec]
# load v_other
for vec in T.vectorized(VEC_SIZE):
v_other_vec[vec] = V_other[bx, ty + by * bdy, tx * VEC_SIZE + vec]
# merge
for vec in T.serial(VEC_SIZE):
v_vec[vec] = v_vec[vec] * scale[0] + v_other_vec[vec] * other_scale[0]
# store v
for vec in T.vectorized(VEC_SIZE):
V[bx, ty + by * bdy, tx * VEC_SIZE + vec] = v_vec[vec]
# store s
S[bx, ty + by * bdy] = T.log2(s_val[0] + s_other_val[0]) + s_max[0]
func = merge_state_inplace
if global_symbol:
func = func.with_attr("global_symbol", global_symbol)
return func
@@ -0,0 +1,569 @@
# 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
@@ -0,0 +1,293 @@
# 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
# fmt: off
"""TIR kernels that operate on paged KV-cache storage (without doing attention).
This module contains:
- Append helpers that transpose/write new K/V tokens into the paged layout
(``_kv_cache_transpose_append`` and its MLA variant).
- Debug helpers that extract K/V from the paged layout for inspection
(``_kv_cache_debug_get_kv``, ``_kv_cache_debug_get_kv_mla``).
- Copy helpers used by the cache runtime for forking/sharing pages
(``_copy_single_page``, ``_copy_single_page_mla``, ``_copy_single_page_cpu``).
- Compact helpers that reorganise pages after removals
(``_compact_kv_copy``, ``_compact_kv_copy_cpu``).
"""
# pylint: disable=too-many-statements,too-many-arguments,invalid-name,line-too-long
from tvm.script import tirx as T
from tvm.target import Target
from ._kernel_common import get_max_num_threads_per_block
def _kv_cache_transpose_append(num_key_value_heads, head_dim, dtype, page_size: int = 16):
"""Return the TIR function that appends new k/v data to PagedKVCache."""
@T.prim_func(s_tir=True)
def tir_kv_cache_transpose_append(
var_pages: T.handle,
var_k_data: T.handle,
var_v_data: T.handle,
var_position_map: T.handle,
):
T.func_attr({"tirx.noalias": True})
ntoken = T.Var("num_tokens_excluding_cache", "int64")
num_pages = T.int64()
pages_elem_offset = T.int64()
position_map_elem_offset = T.int32()
pages = T.match_buffer(var_pages, (num_pages, 2, num_key_value_heads, page_size, head_dim), dtype, elem_offset=pages_elem_offset)
k_data = T.match_buffer(var_k_data, (ntoken, num_key_value_heads, head_dim), dtype)
v_data = T.match_buffer(var_v_data, (ntoken, num_key_value_heads, head_dim), dtype)
position_map = T.match_buffer(var_position_map, (ntoken,), "int32", elem_offset=position_map_elem_offset)
for global_pos, h, f in T.grid(ntoken, num_key_value_heads, head_dim):
if position_map[global_pos] != T.int32(-1):
with T.sblock("k_transpose_append"):
vgpos, vh, vf = T.axis.remap("SSS", [global_pos, h, f])
T.reads(position_map[vgpos], k_data[vgpos, vh, vf])
T.writes(pages[position_map[vgpos] // page_size, 0, vh, position_map[vgpos] % page_size, vf])
position: T.int32 = position_map[vgpos] # type: ignore
pages[T.floordiv(position, page_size), 0, vh, T.floormod(position, page_size), vf] = k_data[vgpos, vh, vf]
with T.sblock("v_transpose_append"):
vgpos, vh, vf = T.axis.remap("SSS", [global_pos, h, f])
T.reads(position_map[vgpos], v_data[vgpos, vh, vf])
T.writes(pages[position_map[vgpos] // page_size, 1, vh, position_map[vgpos] % page_size, vf])
position: T.int32 = position_map[vgpos] # type: ignore[name-defined,no-redef]
pages[T.floordiv(position, page_size), 1, vh, T.floormod(position, page_size), vf] = v_data[vgpos, vh, vf]
return tir_kv_cache_transpose_append
def _kv_cache_transpose_append_mla(d_qk: int, dtype, page_size: int = 16):
"""Return the TIR function that appends new compressed KV data to PagedKVCache for MLA."""
@T.prim_func(s_tir=True)
def tir_kv_cache_transpose_append_mla(
var_pages: T.handle,
var_kv_data: T.handle,
var_position_map: T.handle,
):
T.func_attr({"tirx.noalias": True})
ntoken = T.Var("num_tokens_excluding_cache", "int64")
num_pages = T.int64()
pages_elem_offset = T.int64()
position_map_elem_offset = T.int32()
pages = T.match_buffer(var_pages, (num_pages, page_size, d_qk), dtype, elem_offset=pages_elem_offset)
kv_data = T.match_buffer(var_kv_data, (ntoken, d_qk), dtype)
position_map = T.match_buffer(var_position_map, (ntoken,), "int32", elem_offset=position_map_elem_offset)
for global_pos, f in T.grid(ntoken, d_qk):
if position_map[global_pos] != T.int32(-1):
with T.sblock("k_transpose_append"):
vgpos, vf = T.axis.remap("SS", [global_pos, f])
T.reads(position_map[vgpos], kv_data[vgpos, vf])
T.writes(pages[position_map[vgpos] // page_size, position_map[vgpos] % page_size, vf])
position: T.int32 = position_map[vgpos] # type: ignore
pages[T.floordiv(position, page_size), T.floormod(position, page_size), vf] = kv_data[vgpos, vf]
return tir_kv_cache_transpose_append_mla
def _kv_cache_debug_get_kv(num_hidden_layers, num_key_value_heads, head_dim, dtype):
"""Return the TIR function that fetches the k/v data on given positions and layer."""
@T.prim_func(s_tir=True)
def tir_kv_cache_debug_get_kv(
var_pages: T.handle,
var_position_map: T.handle,
var_k_data: T.handle,
var_v_data: T.handle,
layer_id: T.int64,
):
T.func_attr({"tirx.noalias": True})
seqlen = T.Var("num_tokens_including_cache", "int64")
page_size = T.Var("page_size", "int64")
num_pages = T.int64()
pages_elem_offset = T.int64()
position_map_elem_offset = T.int64()
pages = T.match_buffer(var_pages, (num_pages, 2, num_key_value_heads, page_size, head_dim), dtype,elem_offset=pages_elem_offset)
position_map = T.match_buffer(var_position_map, (seqlen,), "int32", elem_offset=position_map_elem_offset)
k_data = T.match_buffer(var_k_data, (num_hidden_layers, seqlen, num_key_value_heads, head_dim), dtype)
v_data = T.match_buffer(var_v_data, (num_hidden_layers, seqlen, num_key_value_heads, head_dim), dtype)
for p, h, d in T.grid(seqlen, num_key_value_heads, head_dim):
with T.sblock("copy0"):
vp, vh, vd = T.axis.remap("SSS", [p, h, d])
T.reads(position_map[vp], pages[position_map[vp] // page_size, 0:2, vh, position_map[vp] % page_size, vd])
T.writes(k_data[layer_id, vp, vh, vd], v_data[layer_id, vp, vh, vd])
position: T.int32 = position_map[vp] # type: ignore[name-defined]
k_data[layer_id, vp, vh, vd] = pages[T.floordiv(position, page_size), 0, vh, T.floormod(position, page_size), vd]
v_data[layer_id, vp, vh, vd] = pages[T.floordiv(position, page_size), 1, vh, T.floormod(position, page_size), vd]
return tir_kv_cache_debug_get_kv
def _kv_cache_debug_get_kv_mla(num_hidden_layers, d_qk, dtype):
"""Return the TIR function that fetches the k/v data on given positions and layer."""
@T.prim_func(s_tir=True)
def tir_kv_cache_debug_get_kv_mla(
var_pages: T.handle,
var_position_map: T.handle,
var_compressed_kv_with_k_pe_data: T.handle,
layer_id: T.int64,
):
T.func_attr({"tirx.noalias": True})
seqlen = T.Var("num_tokens_including_cache", "int64")
page_size = T.Var("page_size", "int64")
num_pages = T.int64()
pages_elem_offset = T.int64()
position_map_elem_offset = T.int64()
pages = T.match_buffer(var_pages, (num_pages, page_size, d_qk), dtype, elem_offset=pages_elem_offset)
position_map = T.match_buffer(var_position_map, (seqlen,), "int32", elem_offset=position_map_elem_offset)
compressed_kv_with_k_pe_data = T.match_buffer(var_compressed_kv_with_k_pe_data, (num_hidden_layers, seqlen, d_qk), dtype)
for p, d in T.grid(seqlen, d_qk):
with T.sblock("copy0"):
vp, vd = T.axis.remap("SS", [p, d])
T.reads(position_map[vp], pages[position_map[vp] // page_size, position_map[vp] % page_size, vd])
T.writes(compressed_kv_with_k_pe_data[layer_id, vp, vd])
position: T.int32 = position_map[vp] # type: ignore[name-defined]
compressed_kv_with_k_pe_data[layer_id, vp, vd] = pages[T.floordiv(position, page_size), T.floormod(position, page_size), vd]
return tir_kv_cache_debug_get_kv_mla
def _copy_single_page(num_heads, page_size, head_dim, dtype, target: Target):
tx = get_max_num_threads_per_block(target)
@T.prim_func(s_tir=True)
def copy_single_page(var_pages: T.handle, src_page_id: T.int64, tgt_page_id: T.int64, copy_length: T.int64):
T.func_attr({"tirx.is_scheduled": True})
num_pages = T.int32()
pages_elem_offset = T.int64()
pages = T.match_buffer(var_pages, (num_pages, 2, num_heads, page_size, head_dim), dtype, elem_offset=pages_elem_offset)
for b in T.thread_binding((copy_length * num_heads * head_dim + tx - 1) // tx, thread="blockIdx.x"):
for t in T.thread_binding(tx, thread="threadIdx.x"):
with T.sblock("copy"):
T.where(b * tx + t < copy_length * num_heads * head_dim)
vh = T.axis.spatial(num_heads, T.Cast("int32", (b * tx + t) // (copy_length * head_dim)))
vp = T.axis.spatial(copy_length, (b * tx + t) % (copy_length * head_dim) // head_dim)
vd = T.axis.spatial(head_dim, T.Cast("int32", (b * tx + t) % head_dim))
pages[tgt_page_id, 0, vh, vp, vd] = pages[src_page_id, 0, vh, vp, vd]
pages[tgt_page_id, 1, vh, vp, vd] = pages[src_page_id, 1, vh, vp, vd]
return copy_single_page
def _copy_single_page_mla(page_size, head_dim, dtype, target: Target):
tx = get_max_num_threads_per_block(target)
@T.prim_func(s_tir=True)
def copy_single_page_mla(var_pages: T.handle, src_page_id: T.int64, tgt_page_id: T.int64, copy_length: T.int64):
T.func_attr({"tirx.is_scheduled": True})
num_pages = T.int32()
pages_elem_offset = T.int64()
pages = T.match_buffer(var_pages, (num_pages, page_size, head_dim), dtype, elem_offset=pages_elem_offset)
for b in T.thread_binding((copy_length * head_dim + tx - 1) // tx, thread="blockIdx.x"):
for t in T.thread_binding(tx, thread="threadIdx.x"):
with T.sblock("copy"):
T.where(b * tx + t < copy_length * head_dim)
vp = T.axis.spatial(copy_length, (b * tx + t) // head_dim)
vd = T.axis.spatial(head_dim, T.Cast("int32", (b * tx + t) % head_dim))
pages[tgt_page_id, vp, vd] = pages[src_page_id, vp, vd]
return copy_single_page_mla
def _copy_single_page_cpu(num_heads, page_size, head_dim, dtype):
tx = 1
@T.prim_func(s_tir=True)
def copy_single_page_cpu(var_pages: T.handle, src_page_id: T.int64, tgt_page_id: T.int64, copy_length: T.int64):
T.func_attr({"tirx.is_scheduled": True})
num_pages = T.int32()
pages = T.match_buffer(var_pages, (num_pages, 2, num_heads, page_size, head_dim), dtype)
for b in T.serial((copy_length * num_heads * head_dim + tx - 1) // tx):
for t in T.serial(tx):
with T.sblock("copy"):
T.where(b * tx + t < copy_length * num_heads * head_dim)
vh = T.axis.spatial(num_heads, T.Cast("int32", (b * tx + t) // (copy_length * head_dim)))
vp = T.axis.spatial(copy_length, (b * tx + t) % (copy_length * head_dim) // head_dim)
vd = T.axis.spatial(head_dim, T.Cast("int32", (b * tx + t) % head_dim))
pages[tgt_page_id, 0, vh, vp, vd] = pages[src_page_id, 0, vh, vp, vd]
pages[tgt_page_id, 1, vh, vp, vd] = pages[src_page_id, 1, vh, vp, vd]
return copy_single_page_cpu
def _compact_kv_copy(num_heads, head_dim, dtype, target: Target, page_size: int = 16):
tx = get_max_num_threads_per_block(target)
@T.prim_func(s_tir=True)
def compact_kv_copy(var_pages: T.handle, var_copy_length_indptr: T.handle, var_copy_src_dst_pos: T.handle, batch_size: T.int32):
T.func_attr({"tirx.is_scheduled": True})
num_pages = T.int32()
total_copy_length = T.int32()
copy_length_indptr_elem_offset = T.int32()
copy_src_dst_pos_elem_offset = T.int32()
pages_elem_offset = T.int64()
pages = T.match_buffer(var_pages, (num_pages, 2, num_heads, page_size, head_dim), dtype, elem_offset=pages_elem_offset)
copy_length_indptr = T.match_buffer(var_copy_length_indptr, (batch_size + 1,), "int32", elem_offset=copy_length_indptr_elem_offset)
copy_src_dst_pos = T.match_buffer(var_copy_src_dst_pos, (2, total_copy_length), "int32", elem_offset=copy_src_dst_pos_elem_offset)
with T.sblock("root"):
for bhd_o in T.thread_binding((batch_size * num_heads * head_dim + tx - 1) // tx, thread="blockIdx.x"):
for bhd_i in T.thread_binding(tx, thread="threadIdx.x"):
b: T.int32 = (bhd_o * tx + bhd_i) // (num_heads * head_dim)
h: T.int32 = (bhd_o * tx + bhd_i) // head_dim % num_heads
d: T.int32 = (bhd_o * tx + bhd_i) % head_dim
if (bhd_o * tx + bhd_i) < batch_size * num_heads * head_dim:
for i in T.serial(copy_length_indptr[b + 1] - copy_length_indptr[b]):
src_pos: T.int32 = copy_src_dst_pos[0, copy_length_indptr[b] + i]
dst_pos: T.int32 = copy_src_dst_pos[1, copy_length_indptr[b] + i]
pages[dst_pos // page_size, 0, h, dst_pos % page_size, d] = pages[src_pos // page_size, 0, h, src_pos % page_size, d]
pages[dst_pos // page_size, 1, h, dst_pos % page_size, d] = pages[src_pos // page_size, 1, h, src_pos % page_size, d]
return compact_kv_copy
def _compact_kv_copy_cpu(num_heads, head_dim, dtype, page_size: int = 16):
tx = 8
@T.prim_func(s_tir=True)
def compact_kv_copy_cpu(var_pages: T.handle, var_copy_length_indptr: T.handle, var_copy_src_dst_pos: T.handle, batch_size: T.int32):
T.func_attr({"tirx.is_scheduled": True})
num_pages = T.int32()
total_copy_length = T.int32()
copy_length_indptr_elem_offset = T.int32()
copy_src_dst_pos_elem_offset = T.int32()
pages = T.match_buffer(var_pages, (num_pages, 2, num_heads, page_size, head_dim), dtype)
copy_length_indptr = T.match_buffer(var_copy_length_indptr, (batch_size + 1,), "int32", elem_offset=copy_length_indptr_elem_offset)
copy_src_dst_pos = T.match_buffer(var_copy_src_dst_pos, (2, total_copy_length), "int32", elem_offset=copy_src_dst_pos_elem_offset)
with T.sblock("root"):
for bhd_o in T.serial((batch_size * num_heads * head_dim + tx - 1) // tx):
for bhd_i in T.serial(tx):
b: T.int32 = (bhd_o * tx + bhd_i) // (num_heads * head_dim)
h: T.int32 = (bhd_o * tx + bhd_i) // head_dim % num_heads
d: T.int32 = (bhd_o * tx + bhd_i) % head_dim
if (bhd_o * tx + bhd_i) < batch_size * num_heads * head_dim:
for i in T.serial(copy_length_indptr[b + 1] - copy_length_indptr[b]):
src_pos: T.int32 = copy_src_dst_pos[0, copy_length_indptr[b] + i]
dst_pos: T.int32 = copy_src_dst_pos[1, copy_length_indptr[b] + i]
pages[dst_pos // page_size, 0, h, dst_pos % page_size, d] = pages[src_pos // page_size, 0, h, src_pos % page_size, d]
pages[dst_pos // page_size, 1, h, dst_pos % page_size, d] = pages[src_pos // page_size, 1, h, src_pos % page_size, d]
return compact_kv_copy_cpu
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,686 @@
# 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, RUF012
# fmt: off
"""Attention KV cache modeling.
This module exposes the public ``PagedKVCache`` classes (``FlashInferPagedKVCache``
and ``TIRPagedKVCache``). The kernel factories that build the underlying TIR
functions are split across sibling private modules:
- ``_kernel_common``: shared helpers (enums, RoPE, mask, tile allocators,
``@T.macro`` bundle, tiling config, scheduling).
- ``_page_kernels``: page management (append, debug, copy, compact).
- ``_prefill_kernels``: prefill attention kernels (paged/ragged/MLA/dense).
- ``_decode_kernels``: decode attention kernels and state-merge helpers.
The private-named kernel factories are re-exported from this module so the
test suite can continue to import them via ``tvm.relax.frontend.nn.llm.kv_cache``.
"""
# pylint: disable=too-many-statements,too-many-arguments,invalid-name,line-too-long
import math
from typing import Any, Literal
import tvm
from tvm import relax as rx
from tvm import tirx
from tvm.relax.frontend.nn import Object, Tensor
from tvm.target import Target
# Re-export enums + kernel factories so existing ``from kv_cache import ...``
# users (test suite, tree_attn.py, mlc-llm, etc.) continue to work after the
# split. These names are referenced in ``__all__`` below to signal to linters
# that the imports are intentional public API (not dead code).
from ._decode_kernels import (
_attention_decode,
_attention_decode_cpu,
_merge_state_inplace,
_merge_state_inplace_cpu,
)
from ._kernel_common import AttnKind, RopeMode
from ._page_kernels import (
_compact_kv_copy,
_compact_kv_copy_cpu,
_copy_single_page,
_copy_single_page_cpu,
_copy_single_page_mla,
_kv_cache_debug_get_kv,
_kv_cache_debug_get_kv_mla,
_kv_cache_transpose_append,
_kv_cache_transpose_append_mla,
)
from ._prefill_kernels import (
_attention_prefill,
_attention_prefill_cpu,
_attention_prefill_mla,
_attention_prefill_ragged,
_attention_prefill_ragged_cpu,
_attention_sequence_prefill,
_attention_sequence_prefill_with_mask,
)
from .position_embedding import llama_rope_with_position_map
from .tree_attn import (
tree_attn,
tree_attn_cpu,
tree_attn_with_paged_kv_cache,
tree_attn_with_paged_kv_cache_cpu,
)
__all__ = [
"AttnKind",
"FlashInferPagedKVCache",
"PagedKVCache",
"RopeMode",
"TIRPagedKVCache",
"_attention_decode",
"_attention_decode_cpu",
"_attention_prefill",
"_attention_prefill_cpu",
"_attention_prefill_mla",
"_attention_prefill_ragged",
"_attention_prefill_ragged_cpu",
"_attention_sequence_prefill",
"_attention_sequence_prefill_with_mask",
"_compact_kv_copy",
"_compact_kv_copy_cpu",
"_copy_single_page",
"_copy_single_page_cpu",
"_copy_single_page_mla",
"_kv_cache_debug_get_kv",
"_kv_cache_debug_get_kv_mla",
"_kv_cache_transpose_append",
"_kv_cache_transpose_append_mla",
"_merge_state_inplace",
"_merge_state_inplace_cpu",
"llama_rope_with_position_map",
"tree_attn",
"tree_attn_cpu",
"tree_attn_with_paged_kv_cache",
"tree_attn_with_paged_kv_cache_cpu",
]
class PagedKVCache(Object): # pylint: disable=too-few-public-methods
"""The Paged KV Cache used in LLM batching for efficient attention computation."""
extern_mods: list[tvm.runtime.Module] = []
def attention_with_fused_qkv(
self,
layer_id: int,
qkv: Tensor,
num_qo_heads: int,
sm_scale: float,
) -> Tensor:
"""Compute attention with the given fused q/k/v data and in-cache k/v data
on the specified layer. Rotary position embeddings are applied to k/v
within this function.
- For prefill, the input qkv and output tensor have shape
(1, total_seq_len) for the first two dimensions.
- For decode, the input qkv and output tensor have shape
(batch_size, 1) for the first two dimensions.
- The input qkv have `2 * num_qo_heads + num_kv_heads` at the third dim.
- The output tensor have `num_qo_heads` at the third dim.
- The input qkv and output tensor have `head_dim` at the last dim.
"""
# pylint: disable=protected-access
b, s, _, d = qkv._expr.ty.shape
qkv = qkv.reshape(b * s, qkv.shape[2], d)
return Tensor(
_expr=rx.BlockBuilder.current().emit(
rx.call_dps_packed(
"vm.builtin.attention_kv_cache_attention_with_fused_qkv",
[
self._expr,
rx.prim_value(layer_id), # type: ignore[arg-type]
rx.prim_value(sm_scale),
qkv._expr,
],
out_ty=rx.TensorType((b * s, num_qo_heads, d), qkv.dtype),
)
)
).reshape(b, s, num_qo_heads, d)
def self_attention( # pylint: disable=too-many-locals
self,
layer_id: int,
q: Tensor,
k: Tensor,
v: Tensor,
sm_scale: float,
) -> tuple[Tensor, Tensor]:
"""Fine-grained API that computes ragged self attention with Q/K/V data."""
# pylint: disable=protected-access
b, s, h_qo, d_qk = q._expr.ty.shape
_, _, h_kv, d_v = v._expr.ty.shape
q = q.reshape(b * s, h_qo, d_qk)
k = k.reshape(b * s, h_kv, d_qk)
v = v.reshape(b * s, h_kv, d_v)
bb = rx.BlockBuilder.current()
attn_results = bb.emit(
rx.call_dps_packed(
"vm.builtin.attention_kv_cache_self_attention",
[
self._expr,
rx.prim_value(layer_id), # type: ignore[arg-type]
rx.prim_value(sm_scale),
q._expr,
k._expr,
v._expr,
],
out_ty=[
rx.TensorType((b * s, h_qo, d_v), q.dtype),
rx.TensorType((b * s, h_qo), "float32"),
],
)
)
assert isinstance(attn_results.ty, rx.TupleType)
assert len(attn_results.ty.fields) == 2
o = Tensor(_expr=bb.emit(rx.TupleGetItem(attn_results, 0))).reshape(b, s, h_qo, d_v)
lse = Tensor(_expr=bb.emit(rx.TupleGetItem(attn_results, 1))).reshape(b, s, h_qo)
return o, lse
def cross_attention(
self,
layer_id: int,
q: Tensor,
v_head_dim: int,
sm_scale: float,
) -> tuple[Tensor, Tensor]:
"""Fine-grained API that computes paged cross attention with Q and in-cache KV data."""
# pylint: disable=protected-access
b, s, h_qo, d_qk = q._expr.ty.shape
q = q.reshape(b * s, h_qo, d_qk)
bb = rx.BlockBuilder.current()
attn_results = bb.emit(
rx.call_dps_packed(
"vm.builtin.attention_kv_cache_cross_attention",
[
self._expr,
rx.prim_value(layer_id), # type: ignore[arg-type]
rx.prim_value(sm_scale),
q._expr,
],
out_ty=[
rx.TensorType((b * s, h_qo, v_head_dim), q.dtype),
rx.TensorType((b * s, h_qo), "float32"),
],
)
)
assert isinstance(attn_results.ty, rx.TupleType)
assert len(attn_results.ty.fields) == 2
o = Tensor(_expr=bb.emit(rx.TupleGetItem(attn_results, 0))).reshape(b, s, h_qo, v_head_dim)
lse = Tensor(_expr=bb.emit(rx.TupleGetItem(attn_results, 1))).reshape(b, s, h_qo)
return o, lse
def append_mla_kv(self, layer_id: int, kv: Tensor) -> "PagedKVCache":
"""Fine-grained API that appends the MLA K/V data to KV cache."""
# pylint: disable=protected-access
b, s, _, d_qk = kv._expr.ty.shape
kv = kv.reshape(b * s, d_qk)
return PagedKVCache(
_expr=rx.call_pure_packed(
"vm.builtin.attention_kv_cache_append_mla_kv",
self._expr,
rx.prim_value(layer_id), # type: ignore[arg-type]
kv._expr,
ty_args=rx.AnyType(),
),
_name="paged_kv_cache",
)
def merge_attn_output_inplace(
self,
o_self_attn: Tensor,
lse_self_attn: Tensor,
o_cross_attn: Tensor,
lse_cross_attn: Tensor,
) -> tuple[Tensor, Tensor]:
"""Fine-grained API that merges the attention output from two sources.
The first two tensors will be inplace updated.
"""
# pylint: disable=protected-access
b, s, h_qo, d_v = o_self_attn._expr.ty.shape
o_self_attn = o_self_attn.reshape(b * s, h_qo, d_v)
lse_self_attn = lse_self_attn.reshape(b * s, h_qo)
o_cross_attn = o_cross_attn.reshape(b * s, h_qo, d_v)
lse_cross_attn = lse_cross_attn.reshape(b * s, h_qo)
bb = rx.BlockBuilder.current()
merge_results = bb.emit(
rx.call_pure_packed(
"vm.builtin.attention_kv_cache_merge_attn_output_inplace",
self._expr,
o_self_attn._expr,
lse_self_attn._expr,
o_cross_attn._expr,
lse_cross_attn._expr,
ty_args=rx.TupleType(
[o_self_attn._expr.ty, lse_self_attn._expr.ty]
),
)
)
assert isinstance(merge_results.ty, rx.TupleType)
assert len(merge_results.ty.fields) == 2
o_self_attn = Tensor(_expr=bb.emit(rx.TupleGetItem(merge_results, 0))).reshape(
b, s, h_qo, d_v
)
lse_self_attn = Tensor(_expr=bb.emit(rx.TupleGetItem(merge_results, 1))).reshape(b, s, h_qo)
return o_self_attn, lse_self_attn
def get_query_positions(self, total_length: tirx.Expr) -> Tensor:
"""Get the in-sequence positions of each slot in the query,
which are needed for applying positional embeddings in some models.
Parameters
----------
total_length : tirx.Expr
The summed-up total sequence length of queries in
the batch being forwarded.
Returns
-------
q_positions : Tensor
The in-sequence query positions, in shape `(total_length,)`
"""
return Tensor(
_expr=rx.BlockBuilder.current().emit(
rx.call_pure_packed(
"vm.builtin.attention_kv_cache_get_query_positions",
self._expr,
ty_args=rx.TensorType((total_length,), "int32"),
)
)
)
# pylint: enable=protected-access
def _prepare_yarn_rope_scaling(rope_scaling: dict[str, Any] | None, rope_theta: float | None) -> dict[str, Any] | None:
"""Ensure Yarn-specific scaling configs include the theta metadata."""
if rope_scaling is None:
return None
if rope_scaling.get("rope_type") != "yarn":
return rope_scaling
rope_scaling_updated = dict(rope_scaling)
if "inv_theta_log_scale" not in rope_scaling_updated and rope_theta is not None:
theta_value = float(rope_theta)
rope_scaling_updated["inv_theta_log_scale"] = 1.0 / (2 * math.log(theta_value))
return rope_scaling_updated
class FlashInferPagedKVCache(PagedKVCache): # pylint: disable=too-few-public-methods
"""Paged KV cache using FlashInfer (CUDA) kernels."""
def __init__( # pylint: disable=too-many-locals
self,
attn_kind: Literal["mha", "mla"] | list[Literal["mha", "mla", "mha_sliding"]],
max_batch_size: tirx.Var,
max_total_seq_len: tirx.Var,
prefill_chunk_size: tirx.Var,
page_size: tirx.Var,
support_sliding_window: tirx.Var,
layer_partition: rx.ShapeExpr,
num_hidden_layers: int,
num_attention_heads: int,
num_key_value_heads: int,
qk_head_dim: int,
v_head_dim: int,
mla_original_qk_head_dim: int,
mla_original_v_head_dim: int,
rope_mode: RopeMode,
rope_scale: int,
rope_theta: int,
rope_scaling: dict[str, Any],
rope_ext_factors: rx.Expr,
rotary_dim: int,
enable_disaggregation: bool,
dtype: str,
target: Target,
name: str = "paged_kv_cache",
) -> None:
"""Create a paged KV cache object with FlashInfer kernels.
Parameters
----------
max_batch_size : tirx.Var
The maximum allowed batch size of the KV cache.
It is a symbolic variable whose concrete value is specified
at runtime.
max_total_seq_len : tirx.Var
The maximum allowed total sequence length of the KV cache.
It is a symbolic variable whose concrete value is specified
at runtime.
prefill_chunk_size : tirx.Var
The maximum total sequence length in a prefill.
It is a symbolic variable whose concrete value is specified
at runtime.
page_size : tirx.Var
The size (a.k.a. number of tokens) of each page.
It is a symbolic variable whose concrete value is specified
at runtime.
support_sliding_window : tirx.Var
0 or 1, denoting whether the KV cache supports sliding window.
It is a symbolic variable whose concrete value is specified
at runtime.
layer_partition : rx.ShapeExpr
The KV cache layer partition for pipeline stages.
It is an indptr array, denoting the starting layer of each pipeline stage.
rope_mode : RopeMode
The RoPE mode of the Paged KV cache.
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.
rope_scale : int
The scale of rotary position embedding.
rope_theta : int
The base of rotary position embedding.
rope_scaling: Dict[str, Any]
The RoPE scaling information dict.
rope_ext_factors: rx.Expr
The RoPE extension factors when "longrope" mode RoPE scaling is enabled.
rotary_dim : int
The number of dimensions in the embedding that RoPE is applied to.
enable_disaggregation : bool
Whether to enable disaggregation in the KV cache.
"""
assert rope_mode != RopeMode.INLINE, "FlashInfer RoPE does not support inline mode."
rope_scaling = _prepare_yarn_rope_scaling(rope_scaling, rope_theta)
attn_kind_single = attn_kind[0] if isinstance(attn_kind, list) else attn_kind
if attn_kind_single == "mha_sliding":
attn_kind_single = "mha"
flashinfer_prefill_mods = rx.backend.cuda.flashinfer.gen_flashinfer_prefill_module(
dtype_q=dtype,
dtype_kv=dtype,
dtype_o=dtype,
qk_head_dim=(qk_head_dim if attn_kind_single == "mha" else mla_original_qk_head_dim),
v_head_dim=(v_head_dim if attn_kind_single == "mha" else mla_original_v_head_dim),
enable_inline_rope=False,
return_static_libs=True,
)
flashinfer_decode_mods = (
rx.backend.cuda.flashinfer.gen_flashinfer_decode_module(
dtype_q=dtype,
dtype_kv=dtype,
dtype_o=dtype,
qk_head_dim=qk_head_dim,
v_head_dim=v_head_dim,
enable_inline_rope=False,
return_static_libs=True,
)
if attn_kind_single == "mha"
else []
)
flashinfer_mla_mods = (
rx.backend.cuda.flashinfer.gen_flashinfer_mla_module(
dtype_q=dtype,
dtype_kv=dtype,
dtype_o=dtype,
head_dim_ckv=v_head_dim,
head_dim_kpe=qk_head_dim - v_head_dim,
return_static_libs=True,
)
if attn_kind_single == "mla"
else []
)
self.extern_mods = flashinfer_prefill_mods + flashinfer_decode_mods + flashinfer_mla_mods
bb = rx.BlockBuilder.current()
mha_functions = (
[
rx.Tuple([rx.StringImm("flashinfer"), rx.ExternFunc("batch_prefill_paged_run"), rx.ExternFunc("batch_prefill_plan")]),
rx.Tuple([rx.StringImm("flashinfer"), rx.ExternFunc("batch_decode_run"), rx.ExternFunc("batch_decode_plan")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, True, rope_scaling, target), "tir_attention_prefill_sliding_window")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_decode(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, True, rope_scaling, target), "tir_attention_decode_sliding_window")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(tree_attn_with_paged_kv_cache(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, rope_scaling, target), "tir_attention_prefill_with_tree_mask_with_paged_kv_cache")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(tree_attn(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, rope_scaling, target), "tir_attention_prefill_with_tree_mask")]),
]
if attn_kind_single == "mha"
else [rx.Tuple([]) for _ in range(6)]
)
ragged_prefill_function = rx.Tuple([rx.StringImm("flashinfer"), rx.ExternFunc("batch_prefill_ragged_run"), rx.ExternFunc("batch_prefill_plan")]) if attn_kind_single == "mha" else rx.Tuple([rx.StringImm("flashinfer"), rx.ExternFunc("batch_prefill_ragged_run"), rx.ExternFunc("batch_prefill_plan"), rx.prim_value(mla_original_qk_head_dim), rx.prim_value(mla_original_v_head_dim)])
mla_function = rx.Tuple([rx.StringImm("flashinfer"), rx.ExternFunc("batch_mla_run"), rx.ExternFunc("batch_mla_plan")] if attn_kind_single == "mla" else [])
attn_merge_functions = [
bb.add_func(_merge_state_inplace(num_attention_heads, v_head_dim, dtype, target, "tir_attention_merge_state"), "tir_attention_merge_state"),
]
if attn_kind_single == "mla":
attn_merge_functions.append(bb.add_func(_merge_state_inplace(num_attention_heads, mla_original_v_head_dim, dtype, target, "tir_attention_merge_state_mla"), "tir_attention_merge_state_mla"))
if isinstance(attn_kind, list):
attn_kind = [int(getattr(AttnKind, layer_kind.upper())) for layer_kind in attn_kind]
else:
attn_kind = [int(getattr(AttnKind, attn_kind.upper())) for _ in range(num_hidden_layers)]
args = [
rx.ShapeExpr(
[
max_batch_size,
max_total_seq_len,
prefill_chunk_size,
page_size,
support_sliding_window,
]
),
layer_partition,
rx.prim_value(num_attention_heads),
rx.prim_value(num_key_value_heads),
rx.prim_value(qk_head_dim),
rx.prim_value(v_head_dim),
rx.ShapeExpr(attn_kind),
rx.prim_value(enable_disaggregation),
rx.prim_value(rope_mode),
rx.prim_value(rope_scale),
rx.prim_value(rope_theta),
rope_ext_factors,
rx.op.zeros((), dtype),
bb.add_func(_kv_cache_transpose_append(num_key_value_heads, qk_head_dim, dtype), "kv_cache_transpose_append"),
bb.add_func(_kv_cache_transpose_append_mla(qk_head_dim, dtype), "kv_cache_transpose_append_mla"),
ragged_prefill_function,
*mha_functions,
mla_function,
rx.Tuple(attn_merge_functions),
bb.add_func(llama_rope_with_position_map(rope_theta, rope_scale, qk_head_dim, num_attention_heads, num_key_value_heads, dtype, rope_scaling, rotary_dim), "tir_split_rotary"),
bb.add_func(_copy_single_page(num_key_value_heads, page_size, qk_head_dim, dtype, target) if attn_kind_single == "mha" else _copy_single_page_mla(page_size, qk_head_dim, dtype, target), "kv_cache_copy_single_page"),
bb.add_func(_kv_cache_debug_get_kv(num_hidden_layers, num_key_value_heads, qk_head_dim, dtype), "kv_cache_debug_get_kv"),
bb.add_func(_compact_kv_copy(num_key_value_heads, qk_head_dim, dtype, target), "kv_cache_compact_kv_copy"),
]
super().__init__(
_expr=rx.call_pure_packed(
"vm.builtin.paged_attention_kv_cache_create",
*args,
ty_args=rx.AnyType(),
),
_name=name,
)
class TIRPagedKVCache(PagedKVCache): # pylint: disable=too-few-public-methods
"""Paged KV cache using TIR kernels."""
def __init__( # pylint: disable=too-many-locals
self,
attn_kind: Literal["mha", "mla"] | list[Literal["mha", "mla", "mha_sliding"]],
max_batch_size: tirx.Var,
max_total_seq_len: tirx.Var,
prefill_chunk_size: tirx.Var,
page_size: tirx.Var,
support_sliding_window: tirx.Var,
layer_partition: rx.ShapeExpr,
num_hidden_layers: int,
num_attention_heads: int,
num_key_value_heads: int,
qk_head_dim: int,
v_head_dim: int,
mla_original_qk_head_dim: int,
mla_original_v_head_dim: int,
rope_mode: RopeMode,
rope_scale: int,
rope_theta: int,
rope_scaling: dict[str, Any],
rope_ext_factors: rx.Expr,
rotary_dim: int,
enable_disaggregation: bool,
dtype: str,
target: Target,
name: str = "paged_kv_cache",
) -> None:
"""Create a paged KV cache object with TIR kernels.
Parameters
----------
max_batch_size : tirx.Var
The maximum allowed batch size of the KV cache.
It is a symbolic variable whose concrete value is specified
at runtime.
max_total_seq_len : tirx.Var
The maximum allowed total sequence length of the KV cache.
It is a symbolic variable whose concrete value is specified
at runtime.
prefill_chunk_size : tirx.Var
The maximum total sequence length in a prefill.
It is a symbolic variable whose concrete value is specified
at runtime.
page_size : tirx.Var
The size (a.k.a. number of tokens) of each page.
It is a symbolic variable whose concrete value is specified
at runtime.
support_sliding_window : tirx.Var
0 or 1, denoting whether the KV cache supports sliding window.
It is a symbolic variable whose concrete value is specified
at runtime.
layer_partition : rx.ShapeExpr
The KV cache layer partition for pipeline stages.
It is an indptr array, denoting the starting layer of each pipeline stage.
rope_mode : RopeMode
The RoPE mode of the Paged KV cache.
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.
rope_scale : int
The scale of rotary position embedding.
rope_theta : int
The base of rotary position embedding.
rope_scaling: Dict[str, Any]
The RoPE scaling information dict.
rope_ext_factors: rx.Expr
The RoPE extension factors when "longrope" mode RoPE scaling is enabled.
rotary_dim : int
The number of dimensions in the embedding that RoPE is applied to.
enable_disaggregation : bool
Whether to enable disaggregation in the KV cache.
target : Target
The target to build the model to.
"""
rope_scaling = _prepare_yarn_rope_scaling(rope_scaling, rope_theta)
attn_kind_single = attn_kind[0] if isinstance(attn_kind, list) else attn_kind
if attn_kind_single == "mha_sliding":
attn_kind_single = "mha"
if isinstance(attn_kind, list):
attn_kind = [int(getattr(AttnKind, layer_kind.upper())) for layer_kind in attn_kind]
else:
attn_kind = [int(getattr(AttnKind, attn_kind.upper())) for _ in range(num_hidden_layers)]
bb = rx.BlockBuilder.current()
args = [
rx.ShapeExpr(
[
max_batch_size,
max_total_seq_len,
prefill_chunk_size,
page_size,
support_sliding_window,
]
),
layer_partition,
rx.prim_value(num_attention_heads),
rx.prim_value(num_key_value_heads),
rx.prim_value(qk_head_dim),
rx.prim_value(v_head_dim),
rx.ShapeExpr(attn_kind),
rx.prim_value(enable_disaggregation),
rx.prim_value(rope_mode),
rx.prim_value(rope_scale),
rx.prim_value(rope_theta),
rope_ext_factors,
rx.op.zeros((), dtype),
bb.add_func(_kv_cache_transpose_append(num_key_value_heads, qk_head_dim, dtype), "kv_cache_transpose_append"),
bb.add_func(_kv_cache_transpose_append_mla(qk_head_dim, dtype), "kv_cache_transpose_append_mla"),
]
if target.kind.name == "llvm":
if attn_kind_single == "mla":
raise ValueError("MLA is not supported in TIR kernels for now.")
args.extend(
[
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill_ragged_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, v_head_dim, dtype, rope_scaling), "tir_attention_prefill_ragged_cpu")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, False, rope_scaling), "tir_attention_prefill_cpu")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_decode_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, False, rope_scaling), "tir_attention_decode_cpu")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, True, rope_scaling), "tir_attention_prefill_cpu_sliding_window")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_decode_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, True, rope_scaling), "tir_attention_decode_cpu_sliding_window")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(tree_attn_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, rope_scaling), "tir_attention_prefill_with_tree_mask_cpu")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(tree_attn_with_paged_kv_cache_cpu(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, rope_scaling), "tir_attention_prefill_with_tree_mask_with_paged_kv_cache_cpu")]),
rx.Tuple([]), # f_mla_prefill
rx.Tuple([bb.add_func(_merge_state_inplace_cpu(dtype), "tir_attention_merge_state_cpu")]),
bb.add_func(llama_rope_with_position_map(rope_theta, rope_scale, qk_head_dim, num_attention_heads, num_key_value_heads, dtype, rope_scaling, rotary_dim), "tir_split_rotary"),
bb.add_func(_copy_single_page_cpu(num_key_value_heads, page_size, qk_head_dim, dtype), "kv_cache_copy_single_page_cpu"),
bb.add_func(_kv_cache_debug_get_kv(num_hidden_layers, num_key_value_heads, qk_head_dim, dtype), "kv_cache_debug_get_kv"),
bb.add_func(_compact_kv_copy_cpu(num_key_value_heads, qk_head_dim, dtype), "kv_cache_compact_kv_copy_cpu"),
]
)
else:
ragged_qk_head_dim = qk_head_dim if attn_kind_single == "mha" else mla_original_qk_head_dim
ragged_v_head_dim = v_head_dim if attn_kind_single == "mha" else mla_original_v_head_dim
args.append(rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill_ragged(num_key_value_heads if attn_kind_single == "mha" else num_attention_heads, num_attention_heads, ragged_qk_head_dim, ragged_v_head_dim, dtype, rope_scaling, target), "tir_attention_prefill_ragged")]))
mha_functions = (
[
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, False, rope_scaling, target), "tir_attention_prefill")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_decode(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, False, rope_scaling, target), "tir_attention_decode")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, True, rope_scaling, target), "tir_attention_prefill_sliding_window")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_decode(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, True, rope_scaling, target), "tir_attention_decode_sliding_window")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(tree_attn_with_paged_kv_cache(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, rope_scaling, target), "tir_attention_prefill_with_tree_mask_with_paged_kv_cache")]),
rx.Tuple([rx.StringImm("tirx"), bb.add_func(tree_attn(num_key_value_heads, num_attention_heads, qk_head_dim, dtype, rope_scaling, target), "tir_attention_prefill_with_tree_mask")]),
]
if attn_kind_single == "mha"
else [rx.Tuple([]) for _ in range(6)]
)
mla_function = rx.Tuple([rx.StringImm("tirx"), bb.add_func(_attention_prefill_mla(num_attention_heads, v_head_dim, qk_head_dim - v_head_dim, dtype, False, target), "tir_attention_prefill_mla")] if attn_kind_single == "mla" else [])
attn_merge_functions = [
bb.add_func(_merge_state_inplace(num_attention_heads, v_head_dim, dtype, target, "tir_attention_merge_state"), "tir_attention_merge_state"),
]
if attn_kind_single == "mla":
attn_merge_functions.append(bb.add_func(_merge_state_inplace(num_attention_heads, mla_original_v_head_dim, dtype, target, "tir_attention_merge_state_mla"), "tir_attention_merge_state_mla"))
args.extend(mha_functions)
args.append(mla_function)
args.extend(
[
rx.Tuple(attn_merge_functions),
bb.add_func(llama_rope_with_position_map(rope_theta, rope_scale, qk_head_dim, num_attention_heads, num_key_value_heads, dtype, rope_scaling, rotary_dim), "tir_split_rotary"),
bb.add_func(_copy_single_page(num_key_value_heads, page_size, qk_head_dim, dtype, target) if attn_kind_single == "mha" else _copy_single_page_mla(page_size, qk_head_dim, dtype, target), "kv_cache_copy_single_page"),
bb.add_func(_kv_cache_debug_get_kv(num_hidden_layers, num_key_value_heads, qk_head_dim, dtype), "kv_cache_debug_get_kv"),
bb.add_func(_compact_kv_copy(num_key_value_heads, qk_head_dim, dtype, target), "kv_cache_compact_kv_copy"),
]
)
super().__init__(
_expr=rx.call_pure_packed(
"vm.builtin.paged_attention_kv_cache_create",
*args,
ty_args=rx.AnyType(),
),
_name=name,
)
@@ -0,0 +1,894 @@
# 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.
"""Operators for positional embeddings, e.g. RoPE."""
import math
from collections.abc import Callable
from functools import partial
from typing import Any
from tvm import tirx
from tvm.relax.frontend.nn import Tensor, op
from tvm.script import tirx as T
# pylint: disable=invalid-name
def rope_freq_default(s: tirx.Var, d: tirx.Var, d_range: int, theta: float, dtype: str):
"""Compute the inverse frequency of RoPE and then return the cosine and sine of it.
Parameters
----------
s : tirx.Var
The position index.
d : tirx.Var
The dimension index.
d_range : int
The maximum dimension index.
theta : float
The theta value in RoPE, which controls the frequency.
dtype : str
The data type of the output.
Returns
-------
cos_freq : Tensor
The cosine of the inverse frequency.
sin_freq : Tensor
The sine of the inverse frequency.
var_map: Dict[tirx.Var, tirx.Expr]
The common expression map.
"""
freq = s / tirx.power(theta, d * 2 % d_range / tirx.const(d_range, "float32"))
freq_var = tirx.Var("freq", "float32")
cos_freq = tirx.cos(freq_var).astype(dtype)
sin_freq = tirx.sin(freq_var).astype(dtype)
return cos_freq, sin_freq, {freq_var: freq}
def rope_freq_gptj(s: tirx.Var, d: tirx.Var, d_range: int, theta: float, dtype: str):
"""Compute the inverse frequency of RoPE for gptj RoPE scaling."""
freq = s / tirx.power(theta, 2 * (d // 2) % d_range / tirx.const(d_range, "float32"))
freq_var = tirx.Var("freq", "float32")
cos_freq = tirx.cos(freq_var).astype(dtype)
sin_freq = tirx.sin(freq_var).astype(dtype)
return cos_freq, sin_freq, {freq_var: freq}
def rope_freq_llama4( # pylint: disable=too-many-arguments,too-many-locals
s: tirx.Var,
d: tirx.Var,
d_range: int,
theta: float,
dtype: str,
factor: float,
low_freq_factor: float,
high_freq_factor: float,
original_max_position_embeddings: float,
):
"""Compute the inverse frequency of RoPE for llama4 RoPE scaling."""
orig_freq = tirx.const(1, "float32") / tirx.power(
theta, 2 * (d // 2) / tirx.const(d_range, "float32")
)
orig_freq_var = tirx.Var("orig_freq", "float32")
llama4_inv_scaling_factor = 1.0 / factor
if high_freq_factor == low_freq_factor:
wavelength = tirx.const(2 * math.pi, "float32") / orig_freq_var
threshold_wavelen = tirx.const(
original_max_position_embeddings / low_freq_factor, "float32"
)
scaled_freq = tirx.if_then_else(
wavelength > threshold_wavelen, orig_freq_var / factor, orig_freq_var
)
smoothed_freq = s * scaled_freq
else:
# Original smooth interpolation logic
inv_diff_freq_factor = 1.0 / (high_freq_factor - low_freq_factor)
llama4_alpha = original_max_position_embeddings / (2 * math.pi) * inv_diff_freq_factor
llama4_beta = low_freq_factor * inv_diff_freq_factor
smooth = tirx.max(0.0, tirx.min(1.0, llama4_alpha * orig_freq_var - llama4_beta))
smoothed_freq = s * (
(1.0 - smooth) * orig_freq_var * llama4_inv_scaling_factor + smooth * orig_freq_var
)
smoothed_freq_var = tirx.Var("smoothed_freq", "float32")
cos_freq = tirx.cos(smoothed_freq_var).astype(dtype)
sin_freq = tirx.sin(smoothed_freq_var).astype(dtype)
return (
cos_freq,
sin_freq,
{smoothed_freq_var: smoothed_freq, orig_freq_var: orig_freq},
)
def rope_freq_llama3( # pylint: disable=too-many-arguments,too-many-locals
s: tirx.Var,
d: tirx.Var,
d_range: int,
theta: float,
dtype: str,
factor: float,
low_freq_factor: float,
high_freq_factor: float,
original_max_position_embeddings: float,
):
"""Compute the inverse frequency of RoPE for llama3 RoPE scaling."""
orig_freq = tirx.const(1, "float32") / tirx.power(
theta, d * 2 % d_range / tirx.const(d_range, "float32")
)
orig_freq_var = tirx.Var("orig_freq", "float32")
inv_diff_freq_factor = 1.0 / (high_freq_factor - low_freq_factor)
llama3_inv_scaling_factor = 1.0 / factor
llama3_alpha = original_max_position_embeddings / (2 * math.pi) * inv_diff_freq_factor
llama3_beta = low_freq_factor * inv_diff_freq_factor
smooth = tirx.max(0.0, tirx.min(1.0, llama3_alpha * orig_freq_var - llama3_beta))
smoothed_freq = s * (
(1.0 - smooth) * orig_freq_var * llama3_inv_scaling_factor + smooth * orig_freq_var
)
smoothed_freq_var = tirx.Var("smoothed_freq", "float32")
cos_freq = tirx.cos(smoothed_freq_var).astype(dtype)
sin_freq = tirx.sin(smoothed_freq_var).astype(dtype)
return (
cos_freq,
sin_freq,
{smoothed_freq_var: smoothed_freq, orig_freq_var: orig_freq},
)
def rope_freq_longrope( # pylint: disable=too-many-arguments
s: tirx.Var,
d: tirx.Var,
d_range: int,
theta: float,
dtype: str,
max_position_embeddings: int,
original_max_position_embeddings: int,
ext_factors: T.Buffer | None = None,
):
"""Compute the inverse frequency of RoPE for longrope scaling."""
scale = max_position_embeddings / original_max_position_embeddings
scaling_factor = (
math.sqrt(1 + math.log(scale) / math.log(original_max_position_embeddings))
if scale > 1.0
else 1.0
)
divisor = tirx.power(theta, d * 2 % d_range / tirx.const(d_range, "float32"))
if ext_factors is not None:
divisor = ext_factors[d % (d_range // 2)] * divisor
freq = s / divisor
freq_var = tirx.Var("freq", "float32")
cos_freq = (tirx.cos(freq_var) * scaling_factor).astype(dtype)
sin_freq = (tirx.sin(freq_var) * scaling_factor).astype(dtype)
return cos_freq, sin_freq, {freq_var: freq}
def yarn_find_correction_dim(
num_rotations: int,
d: tirx.Var,
max_position_embeddings: int,
inv_theta_log_scale: float | tirx.Expr | None = None,
):
"""Inverse dim formula to find dim based on number of rotations"""
return (
d * math.log(max_position_embeddings / (num_rotations * 2 * math.pi)) * inv_theta_log_scale
)
def yarn_find_correction_range(
low_rot: int,
high_rot: int,
d: tirx.Var,
max_position_embeddings: int,
inv_theta_log_scale: float | tirx.Expr | None = None,
):
"""Find the correction range based on the number of rotations"""
low = yarn_find_correction_dim(
low_rot, d, max_position_embeddings, inv_theta_log_scale=inv_theta_log_scale
)
high = yarn_find_correction_dim(
high_rot, d, max_position_embeddings, inv_theta_log_scale=inv_theta_log_scale
)
return tirx.max(low, 0), tirx.min(high, d - 1)
def rope_freq_yarn(
s: tirx.Var,
d: tirx.Var,
d_range: int,
theta: float | tirx.Expr,
dtype: str,
original_max_position_embeddings: int,
scaling_factor: float,
beta_fast: int,
beta_slow: int,
inv_theta_log_scale: float | tirx.Expr | None = None,
): # pylint: disable=too-many-arguments, too-many-locals
"""Compute the inverse frequency of RoPE for yarn RoPE scaling."""
exponent = d * 2 % d_range / tirx.const(d_range, "float32")
freq_power = tirx.power(theta, exponent)
freq_extra = tirx.const(1, "float32") / freq_power
freq_inter = tirx.const(1, "float32") / (scaling_factor * freq_power)
low, high = yarn_find_correction_range(
beta_fast,
beta_slow,
d_range,
original_max_position_embeddings,
inv_theta_log_scale=inv_theta_log_scale,
)
high = tirx.if_then_else(low == high, high + 0.001, high)
inv_freq_mask = tirx.const(1, "float32") - tirx.max(
tirx.min((d - low) / (high - low), 1.0), 0.0
).astype("float32")
inv_freq = freq_inter * (1 - inv_freq_mask) + freq_extra * inv_freq_mask
freq = s * inv_freq
freq_var = tirx.Var("freq", "float32")
cos_freq = tirx.cos(freq_var).astype(dtype)
sin_freq = tirx.sin(freq_var).astype(dtype)
return cos_freq, sin_freq, {freq_var: freq}
def switch_rope_freq_func(rope_scaling: dict[str, Any]) -> Callable:
"""Return the RoPE inverse frequency computation function based
on the given RoPE scaling.
"""
if "rope_type" not in rope_scaling:
return rope_freq_default
if rope_scaling["rope_type"] == "gptj":
return rope_freq_gptj
if rope_scaling["rope_type"] == "llama3":
return partial(
rope_freq_llama3,
factor=rope_scaling["factor"],
low_freq_factor=rope_scaling["low_freq_factor"],
high_freq_factor=rope_scaling["high_freq_factor"],
original_max_position_embeddings=rope_scaling["original_max_position_embeddings"],
)
if rope_scaling["rope_type"] == "llama4":
return partial(
rope_freq_llama4,
factor=rope_scaling["factor"],
low_freq_factor=rope_scaling["low_freq_factor"],
high_freq_factor=rope_scaling["high_freq_factor"],
original_max_position_embeddings=rope_scaling["original_max_position_embeddings"],
)
if rope_scaling["rope_type"] == "longrope":
return partial(
rope_freq_longrope,
max_position_embeddings=rope_scaling["max_position_embeddings"],
original_max_position_embeddings=rope_scaling["original_max_position_embeddings"],
)
if rope_scaling["rope_type"] == "yarn":
inv_theta_log_scale = rope_scaling.get("inv_theta_log_scale")
assert inv_theta_log_scale is not None, "inv_theta_log_scale must be precomputed for YaRN"
return partial(
rope_freq_yarn,
original_max_position_embeddings=rope_scaling["original_max_position_embeddings"],
scaling_factor=rope_scaling["factor"],
beta_fast=rope_scaling["beta_fast"],
beta_slow=rope_scaling["beta_slow"],
inv_theta_log_scale=inv_theta_log_scale,
)
raise ValueError(f"Unsupported RoPE scaling type: {rope_scaling['rope_type']}")
# mypy: disable-error-code="attr-defined"
def llama_rope( # pylint: disable=too-many-arguments
qkv: Tensor,
total_seq_len: tirx.Var,
theta: float,
scale: float,
num_q_heads: int,
num_kv_heads: int,
rope_scaling: dict[str, Any],
rotary_dim: int | None = None,
) -> tuple[Tensor, Tensor, Tensor]:
"""Llama-style RoPE. Given a fused QKV tensor, it returns three tensors, Q, K, and V, where Q
and K are rotated by RoPE while V remains unchanged.
Parameters
----------
qkv : Tensor
The fused QKV tensor of shape: [batch_size, seq_len, #q_heads + #kv_heads * 2, head_dim]
total_seq_len : tirx.Var
The total sequence length after being concatenated with KVCache. It is used to compute the
offset of RoPE.
theta : float
The theta value, or "base" in RoPE, which controls the frequency.
scale : float
The RoPE scaling factor.
num_q_heads : int
The number of query heads.
num_kv_heads : int
The number of key/value heads. It differs from `num_q_heads` in group-query attention.
rope_scaling : Dict
The configuration of RoPE scaling.
rotary_dim : Optional[int]
The number of dimensions in the embedding that RoPE is applied to. By default, the
rotary_dim is the same as head_dim.
Returns
-------
q : Tensor
The query tensor of shape [batch_size, seq_len, #q_heads, head_dim] w/ RoPE applied
k : Tensor
The key tensor of shape [batch_size, seq_len, #kv_heads, head_dim] w/ RoPE applied
v : Tensor
The value tensor of shape [batch_size, seq_len, #kv_heads, head_dim] w/o RoPE applied
"""
_, _, fused_heads, head_dim = qkv.shape
assert fused_heads == num_q_heads + num_kv_heads * 2
if rotary_dim is None:
rotary_dim = head_dim
dtype = qkv.dtype
scale = tirx.const(scale, dtype)
def _rope( # pylint: disable=too-many-arguments
x: T.Buffer,
b: tirx.Var,
s: tirx.Var,
h: tirx.Var,
d: tirx.Var,
offset: tirx.Var,
):
cos_freq, sin_freq, var_map = switch_rope_freq_func(rope_scaling)(
(s + offset) * scale, d, rotary_dim, theta, dtype
)
cos = cos_freq * x[b, s, h, d]
if rope_scaling["rope_type"] == "gptj":
sin = sin_freq * tirx.if_then_else(
d % 2 == 0,
-x[b, s, h, d + 1],
x[b, s, h, d - 1],
)
else:
sin = sin_freq * tirx.if_then_else(
d < rotary_dim // 2,
-x[b, s, h, d + rotary_dim // 2],
x[b, s, h, d - rotary_dim // 2],
)
expr = cos + sin
for var, value in var_map.items():
expr = tirx.Let(var, value, expr)
return expr
@T.prim_func(private=True, s_tir=True)
def fused_rope( # pylint: disable=too-many-locals
var_qkv: T.handle,
var_q: T.handle,
var_k: T.handle,
var_v: T.handle,
total_seq_len: T.int64,
):
T.func_attr(
{
"op_pattern": 8, # 2 means injective, 8 means opaque
"tirx.noalias": True,
}
)
batch_size = T.int64()
seq_len = T.int64()
qkv = T.match_buffer(var_qkv, (batch_size, seq_len, fused_heads, head_dim), dtype)
q = T.match_buffer(var_q, (batch_size, seq_len, num_q_heads, head_dim), dtype)
k = T.match_buffer(var_k, (batch_size, seq_len, num_kv_heads, head_dim), dtype)
v = T.match_buffer(var_v, (batch_size, seq_len, num_kv_heads, head_dim), dtype)
for iters in T.grid(batch_size, seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
b, s, h, d = T.axis.remap("SSSS", iters)
if h < num_q_heads:
q[b, s, h, d] = T.if_then_else(
d < rotary_dim,
_rope(qkv, b, s, h, d, total_seq_len - seq_len),
qkv[b, s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[b, s, h - num_q_heads, d] = T.if_then_else(
d < rotary_dim,
_rope(qkv, b, s, h, d, total_seq_len - seq_len),
qkv[b, s, h, d],
)
else:
v[b, s, h - (num_q_heads + num_kv_heads), d] = qkv[b, s, h, d]
b, s, _, _ = qkv.shape
return op.tensor_ir_op( # pylint: disable=no-member
fused_rope,
"llama_rope",
args=[qkv, total_seq_len],
out=(
Tensor.placeholder((b, s, num_q_heads, head_dim), dtype),
Tensor.placeholder((b, s, num_kv_heads, head_dim), dtype),
Tensor.placeholder((b, s, num_kv_heads, head_dim), dtype),
),
)
def llama_rope_with_position_map( # pylint: disable=too-many-arguments
theta: float,
scale: float,
head_dim: int,
num_q_heads: int,
num_kv_heads: int,
dtype: str,
rope_scaling: dict[str, Any],
rotary_dim: int | None = None,
):
"""Return the TIR function that computes Llama-style RoPE with q position map.
Parameters
----------
theta : float
The theta value, or "base" in RoPE, which controls the frequency.
scale : float
The RoPE scaling factor.
head_dim : int
The number of features on each head.
num_q_heads : int
The number of query heads.
num_kv_heads : int
The number of key/value heads. It differs from `num_q_heads` in group-query attention.
dtype : str
The dtype of qkv data.
rope_scaling : Dict
The configuration of RoPE scaling.
rotary_dim : int
The number of dimensions in the embedding that RoPE is applied to. By default, the
rotary_dim is the same as head_dim.
"""
fused_heads = num_q_heads + num_kv_heads * 2
if rotary_dim is None:
rotary_dim = head_dim
scale = tirx.const(scale, "float32")
is_longrope_scaling = rope_scaling.get("rope_type") == "longrope"
if is_longrope_scaling and "original_max_position_embeddings" in rope_scaling:
original_max_position_embeddings = rope_scaling["original_max_position_embeddings"]
else:
original_max_position_embeddings = 0
def _rope( # pylint: disable=too-many-arguments
x: T.Buffer,
s: tirx.Var,
h: tirx.Var,
d: tirx.Var,
pos: tirx.Var,
ext_factors: T.Buffer | None = None,
):
kwargs = {}
if ext_factors:
kwargs["ext_factors"] = ext_factors
cos_freq, sin_freq, var_map = switch_rope_freq_func(rope_scaling)(
pos * scale, d, rotary_dim, theta, "float32", **kwargs
)
cos = cos_freq * x[s, h, d].astype("float32")
if "rope_type" in rope_scaling and rope_scaling["rope_type"] == "gptj":
sin = sin_freq * tirx.if_then_else(
d % 2 == 0,
-x[s, h, d + 1],
x[s, h, d - 1],
).astype("float32")
else:
sin = sin_freq * tirx.if_then_else(
d < rotary_dim // 2,
-x[s, h, d + rotary_dim // 2],
x[s, h, d - rotary_dim // 2],
).astype("float32")
expr = (cos + sin).astype(dtype)
for var, value in var_map.items():
expr = tirx.Let(var, value, expr)
return expr
@T.prim_func(s_tir=True)
def fused_rope( # pylint: disable=too-many-locals
var_qkv: T.handle,
var_position_map: T.handle,
var_q: T.handle,
var_k: T.handle,
var_v: T.handle,
apply_rope: T.int64,
):
T.func_attr(
{
"op_pattern": 8, # 2 means injective, 8 means opaque
"tirx.noalias": True,
}
)
seq_len = T.int32()
position_map_elem_offset = T.int32()
qkv = T.match_buffer(var_qkv, (seq_len, fused_heads, head_dim), dtype)
q = T.match_buffer(var_q, (seq_len, num_q_heads, head_dim), dtype)
k = T.match_buffer(var_k, (seq_len, num_kv_heads, head_dim), dtype)
v = T.match_buffer(var_v, (seq_len, num_kv_heads, head_dim), dtype)
position_map = T.match_buffer(
var_position_map, (seq_len,), "int32", elem_offset=position_map_elem_offset
)
for iters in T.grid(seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
s, h, d = T.axis.remap("SSS", iters)
if h < num_q_heads:
q[s, h, d] = T.if_then_else(
apply_rope > 0 and d < rotary_dim,
_rope(qkv, s, h, d, position_map[s]),
qkv[s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[s, h - num_q_heads, d] = T.if_then_else(
apply_rope > 0 and d < rotary_dim,
_rope(qkv, s, h, d, position_map[s]),
qkv[s, h, d],
)
else:
v[s, h - (num_q_heads + num_kv_heads), d] = qkv[s, h, d]
@T.prim_func(s_tir=True)
def fused_rope_longrope_scaling( # pylint: disable=too-many-locals
var_qkv: T.handle,
var_position_map: T.handle,
var_q: T.handle,
var_k: T.handle,
var_v: T.handle,
ext_factors: T.Buffer((rotary_dim,), "float32"), # type: ignore
):
T.func_attr(
{
"op_pattern": 8, # 2 means injective, 8 means opaque
"tirx.noalias": True,
}
)
seq_len = T.int64()
position_map_elem_offset = T.int64()
qkv = T.match_buffer(var_qkv, (seq_len, fused_heads, head_dim), dtype)
q = T.match_buffer(var_q, (seq_len, num_q_heads, head_dim), dtype)
k = T.match_buffer(var_k, (seq_len, num_kv_heads, head_dim), dtype)
v = T.match_buffer(var_v, (seq_len, num_kv_heads, head_dim), dtype)
position_map = T.match_buffer(
var_position_map, (seq_len,), "int32", elem_offset=position_map_elem_offset
)
# long factors is the first half, short factors is the second half
long_factors = T.decl_buffer((rotary_dim // 2,), "float32", data=ext_factors.data)
short_factors = T.decl_buffer(
(rotary_dim // 2,),
"float32",
data=ext_factors.data,
elem_offset=(rotary_dim // 2),
)
if seq_len > original_max_position_embeddings:
for iters in T.grid(seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
s, h, d = T.axis.remap("SSS", iters)
if h < num_q_heads:
q[s, h, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
long_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[s, h - num_q_heads, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
long_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
else:
v[s, h - (num_q_heads + num_kv_heads), d] = qkv[s, h, d]
else:
for iters in T.grid(seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
s, h, d = T.axis.remap("SSS", iters)
if h < num_q_heads:
q[s, h, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
short_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[s, h - num_q_heads, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
short_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
else:
v[s, h - (num_q_heads + num_kv_heads), d] = qkv[s, h, d]
if is_longrope_scaling:
return fused_rope_longrope_scaling
return fused_rope
def llama4_rope_with_position_map( # pylint: disable=too-many-arguments
theta: float,
scale: float,
head_dim: int,
num_q_heads: int,
num_kv_heads: int,
dtype: str,
rope_scaling: dict[str, Any],
rotary_dim: int | None = None,
):
"""Return the TIR function that computes Llama-style RoPE with q position map.
Parameters
----------
theta : float
The theta value, or "base" in RoPE, which controls the frequency.
scale : float
The RoPE scaling factor.
head_dim : int
The number of features on each head.
num_q_heads : int
The number of query heads.
num_kv_heads : int
The number of key/value heads. It differs from `num_q_heads` in group-query attention.
dtype : str
The dtype of qkv data.
rope_scaling : Dict
The configuration of RoPE scaling.
rotary_dim : int
The number of dimensions in the embedding that RoPE is applied to. By default, the
rotary_dim is the same as head_dim.
"""
fused_heads = num_q_heads + num_kv_heads * 2
if rotary_dim is None:
rotary_dim = head_dim
scale = tirx.const(scale, "float32")
is_longrope_scaling = rope_scaling.get("rope_type") == "longrope"
if is_longrope_scaling and "original_max_position_embeddings" in rope_scaling:
original_max_position_embeddings = rope_scaling["original_max_position_embeddings"]
else:
original_max_position_embeddings = 0
def _rope( # pylint: disable=too-many-arguments
x: T.Buffer,
s: tirx.Var,
h: tirx.Var,
d: tirx.Var,
pos: tirx.Var,
ext_factors: T.Buffer | None = None,
):
kwargs = {}
if ext_factors:
kwargs["ext_factors"] = ext_factors
cos_freq, sin_freq, var_map = switch_rope_freq_func(rope_scaling)(
pos * scale, d, rotary_dim, theta, "float32", **kwargs
)
cos = cos_freq * x[s, h, d].astype("float32")
if "rope_type" in rope_scaling and rope_scaling["rope_type"] == "gptj":
sin = sin_freq * tirx.if_then_else(
d % 2 == 0,
-x[s, h, d + 1],
x[s, h, d - 1],
).astype("float32")
else:
# Data layout is different for llama4 vs llama3
sin = sin_freq * tirx.if_then_else(
d % 2 == 0,
-x[s, h, d + 1],
x[s, h, d - 1],
).astype("float32")
expr = (cos + sin).astype(dtype)
for var, value in var_map.items():
expr = tirx.Let(var, value, expr)
return expr
@T.prim_func(private=True, s_tir=True)
def fused_rope( # pylint: disable=too-many-locals
var_qkv: T.handle,
var_position_map: T.handle,
var_q: T.handle,
var_k: T.handle,
var_v: T.handle,
apply_rope: T.int64,
):
T.func_attr(
{
"op_pattern": 8, # 2 means injective, 8 means opaque
"tirx.noalias": True,
}
)
seq_len = T.int32()
position_map_elem_offset = T.int32()
qkv = T.match_buffer(var_qkv, (seq_len, fused_heads, head_dim), dtype)
q = T.match_buffer(var_q, (seq_len, num_q_heads, head_dim), dtype)
k = T.match_buffer(var_k, (seq_len, num_kv_heads, head_dim), dtype)
v = T.match_buffer(var_v, (seq_len, num_kv_heads, head_dim), dtype)
position_map = T.match_buffer(
var_position_map, (seq_len,), "int32", elem_offset=position_map_elem_offset
)
for iters in T.grid(seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
s, h, d = T.axis.remap("SSS", iters)
if h < num_q_heads:
q[s, h, d] = T.if_then_else(
apply_rope > 0 and d < rotary_dim,
_rope(qkv, s, h, d, position_map[s]),
qkv[s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[s, h - num_q_heads, d] = T.if_then_else(
apply_rope > 0 and d < rotary_dim,
_rope(qkv, s, h, d, position_map[s]),
qkv[s, h, d],
)
else:
v[s, h - (num_q_heads + num_kv_heads), d] = qkv[s, h, d]
@T.prim_func(s_tir=True)
def fused_rope_longrope_scaling( # pylint: disable=too-many-locals
var_qkv: T.handle,
var_position_map: T.handle,
var_q: T.handle,
var_k: T.handle,
var_v: T.handle,
ext_factors: T.Buffer((rotary_dim,), "float32"), # type: ignore
):
T.func_attr(
{
"op_pattern": 8, # 2 means injective, 8 means opaque
"tirx.noalias": True,
}
)
seq_len = T.int64()
position_map_elem_offset = T.int64()
qkv = T.match_buffer(var_qkv, (seq_len, fused_heads, head_dim), dtype)
q = T.match_buffer(var_q, (seq_len, num_q_heads, head_dim), dtype)
k = T.match_buffer(var_k, (seq_len, num_kv_heads, head_dim), dtype)
v = T.match_buffer(var_v, (seq_len, num_kv_heads, head_dim), dtype)
position_map = T.match_buffer(
var_position_map, (seq_len,), "int32", elem_offset=position_map_elem_offset
)
# long factors is the first half, short factors is the second half
long_factors = T.decl_buffer((rotary_dim // 2,), "float32", data=ext_factors.data)
short_factors = T.decl_buffer(
(rotary_dim // 2,),
"float32",
data=ext_factors.data,
elem_offset=(rotary_dim // 2),
)
if seq_len > original_max_position_embeddings:
for iters in T.grid(seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
s, h, d = T.axis.remap("SSS", iters)
if h < num_q_heads:
q[s, h, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
long_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[s, h - num_q_heads, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
long_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
else:
v[s, h - (num_q_heads + num_kv_heads), d] = qkv[s, h, d]
else:
for iters in T.grid(seq_len, fused_heads, head_dim):
with T.sblock("llama_fused_rope"):
s, h, d = T.axis.remap("SSS", iters)
if h < num_q_heads:
q[s, h, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
short_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
elif h < num_q_heads + num_kv_heads:
k[s, h - num_q_heads, d] = T.if_then_else(
d < rotary_dim,
_rope(
qkv,
s,
h,
d,
position_map[s],
short_factors if is_longrope_scaling else None,
),
qkv[s, h, d],
)
else:
v[s, h - (num_q_heads + num_kv_heads), d] = qkv[s, h, d]
if is_longrope_scaling:
return fused_rope_longrope_scaling
return fused_rope
File diff suppressed because it is too large Load Diff