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

252 lines
9.3 KiB
Python

import numpy as np
import pytest
tvm = pytest.importorskip("tvm")
from tvm import relax # noqa: E402
from tvm.relax.frontend import nn # noqa: E402
from tvm.relax.frontend.nn import spec # noqa: E402
from tvm.runtime import tensor as tvm_tensor # noqa: E402
from mlc_llm.op import ( # noqa: E402
MultimodalRotaryEmbedding,
VisionPositionMetadata,
apply_multimodal_rotary_pos_emb,
get_mrope_position_ids,
)
def _numpy_rotate_half(x: np.ndarray) -> np.ndarray:
x1, x2 = np.split(x, 2, axis=-1)
return np.concatenate([-x2, x1], axis=-1)
def _numpy_apply_mrope(
q: np.ndarray,
k: np.ndarray,
position_ids: np.ndarray,
theta: float,
mrope_section: tuple[int, ...],
) -> tuple[np.ndarray, np.ndarray]:
if position_ids.ndim != 3:
raise ValueError(f"position_ids must be rank-3, got shape {position_ids.shape}")
if position_ids.shape[0] == 3:
position_ids = np.transpose(position_ids, (1, 2, 0))
elif position_ids.shape[-1] != 3:
raise ValueError(
"position_ids must have shape (batch, seq, 3) or (3, batch, seq), "
f"got {position_ids.shape}"
)
head_dim = q.shape[-1]
inv_freq = 1.0 / (theta ** (np.arange(0, head_dim, 2, dtype=np.float32) / float(head_dim)))
pos = np.transpose(position_ids, (2, 0, 1))
inv = inv_freq.reshape(1, 1, -1, 1).astype(np.float32)
inv = np.broadcast_to(inv, (3, pos.shape[1], inv_freq.size, 1))
pos = pos.reshape(3, pos.shape[1], 1, pos.shape[2]).astype(np.float32)
freqs = np.matmul(inv, pos)
freqs = np.transpose(freqs, (0, 1, 3, 2))
emb = np.concatenate([freqs, freqs], axis=-1)
cos = np.cos(emb)
sin = np.sin(emb)
split_sizes = list(mrope_section) * 2
split_points = np.cumsum(split_sizes)[:-1]
cos_chunks = np.split(cos, split_points, axis=-1)
sin_chunks = np.split(sin, split_points, axis=-1)
cos = np.concatenate([chunk[idx % 3] for idx, chunk in enumerate(cos_chunks)], axis=-1)
sin = np.concatenate([chunk[idx % 3] for idx, chunk in enumerate(sin_chunks)], axis=-1)
cos = np.expand_dims(cos, axis=2)
sin = np.expand_dims(sin, axis=2)
q_out = q * cos + _numpy_rotate_half(q) * sin
k_out = k * cos + _numpy_rotate_half(k) * sin
return q_out, k_out
def _evaluate_tensor(expr):
mod = tvm.IRModule.from_expr(expr)
target = tvm.target.Target("llvm")
ex = tvm.relax.build(mod, target)
vm = tvm.relax.VirtualMachine(ex, tvm.cpu())
return vm["main"]().numpy()
def _run_mlc_mrope(
q_np: np.ndarray,
k_np: np.ndarray,
position_ids_np: np.ndarray,
theta: float,
mrope_section: tuple[int, ...],
) -> tuple[np.ndarray, np.ndarray]:
class RopeModule(nn.Module):
def __init__(self):
super().__init__()
self.rotary = MultimodalRotaryEmbedding(q_np.shape[-1], theta, mrope_section)
def forward(
self,
q: nn.Tensor,
k: nn.Tensor,
pos: nn.Tensor,
):
"""Run MRoPE on test tensors and return rotated query/key outputs."""
cos, sin = self.rotary(q, pos)
return apply_multimodal_rotary_pos_emb(q, k, cos, sin, mrope_section)
module = RopeModule()
mod, _, _ = module.export_tvm(
spec={
"forward": {
"q": spec.Tensor(q_np.shape, "float32"),
"k": spec.Tensor(k_np.shape, "float32"),
"pos": spec.Tensor(position_ids_np.shape, "int64"),
}
},
allow_extern=True,
)
target = tvm.target.Target("llvm")
exec_mod = relax.build(mod, target=target)
vm = relax.VirtualMachine(exec_mod, tvm.cpu())
device = tvm.cpu()
q_nd = tvm_tensor(q_np.astype("float32"), device=device)
k_nd = tvm_tensor(k_np.astype("float32"), device=device)
pos_nd = tvm_tensor(position_ids_np.astype("int64"), device=device)
out_q, out_k = vm["forward"](q_nd, k_nd, pos_nd)
return out_q.numpy(), out_k.numpy()
def test_apply_mrope_matches_numpy_reference():
theta = 10000.0
mrope_section = (2, 2, 2)
batch, seq_len, heads, head_dim = 1, 4, 2, 12
rng = np.random.default_rng(0)
q_np = rng.standard_normal((batch, seq_len, heads, head_dim), dtype=np.float32)
k_np = rng.standard_normal((batch, seq_len, heads, head_dim), dtype=np.float32)
position_ids = np.zeros((batch, seq_len, 3), dtype=np.int64)
position_ids[0, :, 0] = np.arange(seq_len)
position_ids[0, :, 1] = np.arange(seq_len) * 2
position_ids[0, :, 2] = np.arange(seq_len) * 3
mlc_q, mlc_k = _run_mlc_mrope(q_np, k_np, position_ids, theta, mrope_section)
ref_q, ref_k = _numpy_apply_mrope(q_np, k_np, position_ids, theta, mrope_section)
np.testing.assert_allclose(mlc_q, ref_q, rtol=1e-5, atol=1e-5)
np.testing.assert_allclose(mlc_k, ref_k, rtol=1e-5, atol=1e-5)
def test_get_mrope_position_ids_text_only():
input_ids = np.array([[1, 2, 3, 0, 0]], dtype=np.int64)
attention_mask = np.array([[1, 1, 1, 0, 0]], dtype=np.int64)
meta = VisionPositionMetadata(
vision_start_token_id=1000,
image_token_id=1001,
video_token_id=1002,
spatial_merge_size=2,
tokens_per_second=4.0,
)
position_ids, deltas = get_mrope_position_ids(
input_ids,
meta,
attention_mask=attention_mask,
image_grid_thw=None,
video_grid_thw=None,
second_per_grid_ts=None,
)
expected = attention_mask.cumsum(axis=-1) - 1
expected = np.where(attention_mask == 0, 1, expected)
expected = np.expand_dims(expected, axis=0).repeat(3, axis=0)
np.testing.assert_array_equal(position_ids, expected)
np.testing.assert_array_equal(deltas, np.array([[-2]], dtype=np.int64))
def test_get_mrope_position_ids_single_image_block():
meta = VisionPositionMetadata(
vision_start_token_id=5000,
image_token_id=5001,
video_token_id=6000,
spatial_merge_size=2,
tokens_per_second=4.0,
)
input_ids = np.array(
[[11, 12, 5000, 5001, 21, 22, 23, 24, 31, 32]],
dtype=np.int64,
)
attention_mask = np.ones_like(input_ids, dtype=np.int64)
image_grid_thw = np.array([[1, 4, 4]], dtype=np.int64)
position_ids, deltas = get_mrope_position_ids(
input_ids,
meta,
attention_mask=attention_mask,
image_grid_thw=image_grid_thw,
video_grid_thw=None,
second_per_grid_ts=None,
)
expected = np.array(
[
[0, 1, 2, 3, 3, 3, 3, 5, 6, 7],
[0, 1, 2, 3, 3, 4, 4, 5, 6, 7],
[0, 1, 2, 3, 4, 3, 4, 5, 6, 7],
],
dtype=np.int64,
).reshape(3, 1, -1)
np.testing.assert_array_equal(position_ids, expected)
np.testing.assert_array_equal(deltas, np.array([[-2]], dtype=np.int64))
def test_apply_mrope_accepts_3_batch_seq_layout():
theta = 10000.0
mrope_section = (2, 2, 2)
batch, seq_len, heads, head_dim = 1, 4, 2, 12
rng = np.random.default_rng(1)
q_np = rng.standard_normal((batch, seq_len, heads, head_dim), dtype=np.float32)
k_np = rng.standard_normal((batch, seq_len, heads, head_dim), dtype=np.float32)
position_ids_bsc = np.zeros((batch, seq_len, 3), dtype=np.int64)
position_ids_bsc[0, :, 0] = np.arange(seq_len)
position_ids_bsc[0, :, 1] = np.arange(seq_len) * 2
position_ids_bsc[0, :, 2] = np.arange(seq_len) * 3
position_ids_3bs = np.transpose(position_ids_bsc, (2, 0, 1))
mlc_q_bsc, mlc_k_bsc = _run_mlc_mrope(q_np, k_np, position_ids_bsc, theta, mrope_section)
mlc_q_3bs, mlc_k_3bs = _run_mlc_mrope(q_np, k_np, position_ids_3bs, theta, mrope_section)
ref_q, ref_k = _numpy_apply_mrope(q_np, k_np, position_ids_bsc, theta, mrope_section)
np.testing.assert_allclose(mlc_q_bsc, ref_q, rtol=1e-5, atol=1e-5)
np.testing.assert_allclose(mlc_k_bsc, ref_k, rtol=1e-5, atol=1e-5)
np.testing.assert_allclose(mlc_q_3bs, ref_q, rtol=1e-5, atol=1e-5)
np.testing.assert_allclose(mlc_k_3bs, ref_k, rtol=1e-5, atol=1e-5)
def test_get_mrope_position_ids_output_is_directly_usable():
theta = 10000.0
mrope_section = (2, 2, 2)
meta = VisionPositionMetadata(
vision_start_token_id=7000,
image_token_id=7001,
video_token_id=7002,
spatial_merge_size=2,
tokens_per_second=4.0,
)
input_ids = np.array([[11, 12, 7000, 7001, 21, 22, 23, 24, 31, 32]], dtype=np.int64)
attention_mask = np.ones_like(input_ids, dtype=np.int64)
image_grid_thw = np.array([[1, 4, 4]], dtype=np.int64)
position_ids_3bs, _ = get_mrope_position_ids(
input_ids,
meta,
attention_mask=attention_mask,
image_grid_thw=image_grid_thw,
video_grid_thw=None,
second_per_grid_ts=None,
)
position_ids_bsc = np.transpose(position_ids_3bs, (1, 2, 0))
batch, seq_len = input_ids.shape
heads, head_dim = 2, 12
rng = np.random.default_rng(2)
q_np = rng.standard_normal((batch, seq_len, heads, head_dim), dtype=np.float32)
k_np = rng.standard_normal((batch, seq_len, heads, head_dim), dtype=np.float32)
mlc_q_3bs, mlc_k_3bs = _run_mlc_mrope(q_np, k_np, position_ids_3bs, theta, mrope_section)
mlc_q_bsc, mlc_k_bsc = _run_mlc_mrope(q_np, k_np, position_ids_bsc, theta, mrope_section)
np.testing.assert_allclose(mlc_q_3bs, mlc_q_bsc, rtol=1e-5, atol=1e-5)
np.testing.assert_allclose(mlc_k_3bs, mlc_k_bsc, rtol=1e-5, atol=1e-5)