Files
2026-07-13 12:24:33 +08:00

205 lines
5.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from typing import Any, Callable, Dict, Optional
# Third Party
from vllm.model_executor.layers.rotary_embedding import get_rope as vllm_get_rope
import torch
# First Party
from lmcache import torch_device_type
from lmcache.logging import init_logger
import lmcache.c_ops as lmc_ops
logger = init_logger(__name__)
# TODO(Jiayi): Add and test more types of rope
# (e.g., rope scaling, (non-)neox style, dtype, etc.)
class BasicReverseRope:
def __init__(self, rope, rotary_dim, is_neox_style):
self.rope = rope
self.rotary_dim = rotary_dim
self.is_neox_style = is_neox_style
def do_shuffle(self, t):
original_shape = t.shape
t = t.reshape(t.shape[0], -1, self.rotary_dim)
if self.is_neox_style:
o1, o2 = torch.chunk(t, 2, dim=-1)
else:
o1 = t[..., ::2]
o2 = t[..., 1::2]
if self.is_neox_style:
return torch.cat((o2, o1), dim=-1).reshape(original_shape)
else:
return torch.stack((o2, o1), dim=-1).reshape(original_shape)
def reverse_encode(self, positions, q, k):
sq = self.do_shuffle(q)
sk = self.do_shuffle(k)
nq, nk = self.rope(positions, sq, sk)
fq = self.do_shuffle(nq)
fk = self.do_shuffle(nk)
return fq, fk
def __call__(self, positions, q, k):
return self.reverse_encode(positions, q, k)
class FusedRope:
"""
Directly use the fused kernel to ratate K cache from
the old positions to the new positions.
"""
def __init__(self, rope, is_neox_style):
self.rope = rope
self.is_neox_style = is_neox_style
self.head_size = rope.head_size
self.cos_sin_cache = rope.cos_sin_cache
def fused_encode(self, old_positions, new_positions, k):
num_tokens = k.shape[0]
k = k.view(num_tokens, -1, self.head_size)
lmc_ops.rotary_embedding_k_fused(
old_positions,
new_positions,
k,
self.head_size,
self.cos_sin_cache.to(k.device),
self.is_neox_style,
)
k = k.view(num_tokens, -1)
return k
def __call__(self, old_positions, new_positions, k):
return self.fused_encode(old_positions, new_positions, k)
def validate_rope_params(
head_size: int,
rotary_dim: int,
max_position: int,
base: float,
is_neox_style: bool = True,
rope_scaling: Optional[Dict[str, Any]] = None,
dtype: Optional[torch.dtype] = None,
partial_rotary_factor: float = 1.0,
):
if rotary_dim != head_size:
logger.error("Currently KV blending only support rotary_dim == head_size.")
return False
if rope_scaling is not None:
logger.error("Currently KV blending do not support rope scaling.")
return False
if partial_rotary_factor != 1.0:
logger.error(
"Currently KV blending do not support rotary factor other than 1.0."
)
return False
return True
def validate_reverse_correctness(rope, reverse_rope, fused_rope, head_size) -> bool:
hidden_dim = head_size * 8
num_tokens = 10
dumb_q = torch.rand(
(num_tokens, hidden_dim), device=torch_device_type, dtype=torch.bfloat16
)
dumb_k = torch.rand(
(num_tokens, hidden_dim), device=torch_device_type, dtype=torch.bfloat16
)
positions = torch.arange(num_tokens, device=torch_device_type)
q1 = dumb_q.clone()
k1 = dumb_k.clone()
q1, k1 = rope(positions, q1, k1)
q1, k1 = reverse_rope(positions, q1, k1)
max_q_error = (dumb_q - q1).abs().max()
max_k_error = (dumb_k - k1).abs().max()
logger.info(f"Max Q error: {max_q_error.item()}")
logger.info(f"Max K error: {max_k_error.item()}")
q_no_pos = dumb_q.clone()
k_no_pos = dumb_k.clone()
positions2 = torch.arange(100, 100 + num_tokens, device=torch_device_type)
_, k_pos2 = rope(positions2, q_no_pos, k_no_pos)
k_no_pos = dumb_k.clone()
_, k_pos1 = rope(positions, q_no_pos, k_no_pos)
k_pos2_fused = fused_rope(positions, positions2, k_pos1)
max_k_error_fused = (k_pos2 - k_pos2_fused).abs().max()
logger.info(f"Max K error (fused): {max_k_error.item()}")
return max_q_error < 0.1 and max_k_error < 0.1 and max_k_error_fused < 0.1
# Main interface
def get_fused_rope(
head_size: int,
rotary_dim: int,
max_position: int,
base: float,
is_neox_style: bool = True,
rope_scaling: Optional[Dict[str, Any]] = None,
dtype: Optional[torch.dtype] = None,
partial_rotary_factor: float = 1.0,
) -> Optional[Callable[..., Any]]:
# Validate the ROPE parameters
if not validate_rope_params(
head_size,
rotary_dim,
max_position,
base,
is_neox_style,
rope_scaling,
dtype,
partial_rotary_factor,
):
logger.warning(
"The rope parameters is not supported! Cannot use cacheblend in this case"
)
return None
new_rope_params = {
"rope_theta": base,
"partial_rotary_factor": partial_rotary_factor,
}
if rope_scaling is not None:
new_rope_params.update(rope_scaling)
if "type" in rope_scaling:
new_rope_params["rope_type"] = rope_scaling["type"]
rope = vllm_get_rope(
head_size=head_size,
max_position=max_position,
is_neox_style=is_neox_style,
rope_parameters=new_rope_params,
dtype=dtype,
dual_chunk_attention_config=None,
)
reverse_rope = BasicReverseRope(rope, rotary_dim, is_neox_style)
fused_rope = FusedRope(rope, is_neox_style)
correct = validate_reverse_correctness(rope, reverse_rope, fused_rope, head_size)
if not correct:
logger.error(
"Fused/reverse rotary encoding is not correct! Will disable blending!"
)
return None
return fused_rope