527 lines
28 KiB
Python
527 lines
28 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
# ruff: noqa: E501
|
|
# 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
|