Files
wehub-resource-sync 770d92cb1f
Lint / lint (push) Waiting to run
Windows CI / Windows (push) Waiting to run
Build Docs / Deploy Docs (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:23:58 +08:00

244 lines
8.1 KiB
Python

import math
import numpy as np
import pytest
import tvm
import tvm.testing
from tvm.relax.frontend.nn.llm import tree_attn
# test category "op_correctness"
pytestmark = [pytest.mark.op_correctness]
@pytest.mark.parametrize("nbatch", [1, 4, 32])
@pytest.mark.parametrize("h_q", [8, 16])
@pytest.mark.parametrize("h_kv", [4, 8])
@pytest.mark.parametrize("d", [128])
@pytest.mark.parametrize("rotary_mode", [0, 1])
def test_tree_attn(nbatch, h_q, h_kv, d, rotary_mode):
np.random.seed(0)
np.set_printoptions(linewidth=10000)
def gen_chain(num_nodes):
mask = np.tril(np.ones((num_nodes, num_nodes)))
return num_nodes, list(mask.flatten()), np.arange(num_nodes)
def gen_full_binary_tree(height):
mask = list()
pos = list()
num_nodes = 2**height - 1
for i in range(num_nodes):
if i == 0:
mask_0 = [0] * num_nodes
mask_0[0] = 1
mask.append(mask_0)
pos.append(0)
else:
mask_i = mask[(i + 1) // 2 - 1].copy()
mask_i[i] = 1
mask.append(mask_i)
pos.append(pos[(i + 1) // 2 - 1] + 1)
return num_nodes, list(np.array(mask).flatten()), pos
### Inputs
num_nodes = 0
m_list = list()
mn_list = list()
mask_list = list()
q_pos_list = list()
mn_list.append(0)
for _ in range(nbatch):
choice = np.random.choice(2, 1, p=[1, 0])
if choice == 0:
nodes_batch = np.random.randint(3, 32)
res = gen_chain(nodes_batch)
num_nodes += nodes_batch
else:
height = np.random.randint(2, 6)
res = gen_full_binary_tree(height)
num_nodes += 2**height - 1
m_list.append(res[0])
mn_list.append(res[0] ** 2)
mask_list.extend(res[1])
q_pos_list.extend(res[2])
qkv_indptr = np.array(np.cumsum([0, *m_list])).astype(np.int32)
m_list = np.array(m_list).astype(np.int32)
mn_list = np.array(mn_list).astype(np.int32)
mn_list = np.cumsum(mn_list).astype(np.int32)
mask_list = np.array(mask_list).astype(np.int32)
q_pos_list = np.array(q_pos_list).astype(np.int32)
# print("qkv_indptr:", qkv_indptr)
# print("m_list:", m_list)
# print("mn_list:", mn_list)
# for num_nodes, base in zip(m_list, mn_list):
# print("num_nodes:", num_nodes)
# print("indptr:", base)
# print(
# "mask:",
# mask_list[base : base + num_nodes * num_nodes].reshape(num_nodes, num_nodes),
# )
# print("q_pos:", q_pos_list[base : base + num_nodes])
q = np.random.rand(num_nodes, h_q, d).astype(np.float16)
q_indptr = qkv_indptr
k = np.random.rand(num_nodes, h_kv, d).astype(np.float16)
v = np.random.rand(num_nodes, h_kv, d).astype(np.float16)
kv_indptr = qkv_indptr
q_rope_position = q_pos_list
m_arr = m_list
mn_indptr = mn_list
mask = mask_list
output = np.zeros((num_nodes, h_q, d), dtype=np.float16)
lse = np.zeros((num_nodes, h_q), dtype=np.float32)
rotary_scale = 1.0
rotary_theta = 10000.0
attn_score_scaling_factor = 1.0
### TVM Inputs
dev = tvm.cuda(0)
q_tvm = tvm.runtime.tensor(q, dev)
q_indptr_tvm = tvm.runtime.tensor(q_indptr, dev)
k_tvm = tvm.runtime.tensor(k, dev)
v_tvm = tvm.runtime.tensor(v, dev)
kv_indptr_tvm = tvm.runtime.tensor(kv_indptr, dev)
q_rope_position_tvm = tvm.runtime.tensor(q_rope_position, dev)
# m_arr_tvm = tvm.runtime.tensor(m_arr, dev)
mn_indptr_tvm = tvm.runtime.tensor(mn_indptr, dev)
mask_tvm = tvm.runtime.tensor(mask, dev)
output_tvm = tvm.runtime.tensor(output, dev)
lse_tvm = tvm.runtime.tensor(lse, dev)
target = tvm.target.Target("cuda")
kernel = tree_attn(h_kv=h_kv, h_q=h_q, d=d, dtype="float16", rope_scaling={}, target=target)
mod = tvm.build(kernel, target=target)
mod(
q_tvm,
q_indptr_tvm,
k_tvm,
v_tvm,
kv_indptr_tvm,
q_rope_position_tvm,
# m_arr_tvm,
mn_indptr_tvm,
mask_tvm,
output_tvm,
lse_tvm,
rotary_mode,
rotary_scale,
rotary_theta,
attn_score_scaling_factor,
nbatch,
)
### Numpy reference
def numpy_reference(
q,
q_indptr,
k,
v,
kv_indptr,
q_rope_position,
m_arr,
mn_indptr,
mask,
rotary_mode,
rotary_scale,
rotary_theta,
attn_score_scaling_factor,
output_tvm,
):
def rope_freq(s, d, d_range, theta, dtype):
freq = s / math.pow(theta, (d * 2 % d_range) / float(d_range))
cos_freq = np.cos(freq).astype(dtype)
sin_freq = np.sin(freq).astype(dtype)
return cos_freq, sin_freq
def rope(buffer, offset, rotary_dim, theta, scale, dtype):
result = buffer.copy()
for pos, h, d in np.ndindex(buffer.shape):
cos_freq, sin_freq = rope_freq(offset[pos] * scale, d, rotary_dim, theta, dtype)
cos = cos_freq * buffer[pos, h, d]
sin = sin_freq * (
-buffer[pos, h, d + rotary_dim // 2]
if d < rotary_dim // 2
else buffer[pos, h, d - rotary_dim // 2]
)
result[pos, h, d] = cos + sin
return result
for i in range(len(m_arr)):
num_nodes = m_arr[i]
base = mn_indptr[i]
q_base = q_indptr[i]
kv_base = kv_indptr[i]
q_pos = q_rope_position[q_base : q_base + num_nodes] # (num_nodes,)
q_i = q[q_base : q_base + num_nodes] # (num_nodes, h_q, d)
k_i = k[kv_base : kv_base + num_nodes] # (num_nodes, h_kv, d)
v_i = v[kv_base : kv_base + num_nodes] # (num_nodes, h_kv, d)
mask_i = mask[base : base + num_nodes * num_nodes].reshape(num_nodes, num_nodes)
if rotary_mode == 1:
q_i = rope(q_i, q_pos, d, rotary_theta, rotary_scale, q_i.dtype)
k_i = rope(k_i, q_pos, d, rotary_theta, rotary_scale, k_i.dtype)
# group attention
# q: (num_nodes, h_q, d)
# k: (num_nodes, h_kv, d)
# v: (num_nodes, h_kv, d)
group_size = h_q // h_kv
q_reshape = q_i.transpose(1, 0, 2) # (h_q, num_nodes, d)
k_reshape = k_i.transpose(1, 2, 0) # (h_kv, d, num_nodes)
v_reshape = v_i.transpose(1, 0, 2) # (h_kv, num_nodes, d)
# expand k_reshape
k_reshape = k_reshape.reshape(h_kv, 1, d, num_nodes)
k_reshape = np.repeat(k_reshape, group_size, axis=1)
k_reshape = k_reshape.reshape(h_q, d, num_nodes)
# expand v_reshape
v_reshape = v_reshape.reshape(h_kv, 1, num_nodes, d)
v_reshape = np.repeat(v_reshape, group_size, axis=1)
v_reshape = v_reshape.reshape(h_q, num_nodes, d)
# print("q_reshape:", q_reshape.shape)
# print("k_reshape:", k_reshape.shape)
# print("v_reshape:", v_reshape.shape)
# qk: (h_q, num_nodes, num_nodes)
qk = np.matmul(q_reshape, k_reshape) * attn_score_scaling_factor / math.sqrt(float(d))
# softmax(qk, axis=-1), numerical stability
qk[:, mask_i == 0] = -np.inf
qk_max = np.max(qk, axis=-1, keepdims=True)
qk = np.exp(qk - qk_max)
qk = qk / np.sum(qk, axis=-1, keepdims=True)
# attention
output_i = np.matmul(qk, v_reshape).transpose(1, 0, 2) # (num_nodes, h_q, d)
# print(output_i)
tvm.testing.assert_allclose(
output_i, output_tvm[q_base : q_base + num_nodes], rtol=1e-3, atol=1e-3
)
numpy_reference(
q,
q_indptr,
k,
v,
kv_indptr,
q_rope_position,
m_arr,
mn_indptr,
mask,
rotary_mode,
rotary_scale,
rotary_theta,
attn_score_scaling_factor,
output_tvm.numpy(),
)
if __name__ == "__main__":
tvm.testing.main()