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

241 lines
7.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Helpers for the token dropping example."""
# Standard
import json
# Third Party
from transformers import AutoConfig
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
import httpx
import torch
# First Party
import lmcache.sdk.stream as lmc_stream
def _rotate_half_neox(x: torch.Tensor) -> torch.Tensor:
"""Rotate pairs using GPT-NeoX/Llama-style half layout."""
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
def _rotate_half_interleaved(x: torch.Tensor) -> torch.Tensor:
"""Rotate pairs using interleaved layout."""
x_even = x[..., ::2]
x_odd = x[..., 1::2]
return torch.stack((-x_odd, x_even), dim=-1).flatten(-2)
def _rope_cos_sin(
*,
config: AutoConfig,
positions: torch.Tensor,
rotary_dim: int,
device: torch.device,
dtype: torch.dtype,
) -> tuple[torch.Tensor, torch.Tensor, float]:
"""Build RoPE cos/sin for the model config, including HF rope_scaling."""
rope_scaling = getattr(config, "rope_scaling", None)
rope_type = None
if rope_scaling is not None:
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type"))
if rope_scaling is None or rope_type == "default":
rope_theta = getattr(config, "rope_theta", 10000.0)
inv_freq = 1.0 / (
rope_theta
** (
torch.arange(0, rotary_dim, 2, device=device, dtype=torch.float32)
/ rotary_dim
)
)
attention_scaling = 1.0
else:
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type"))
if rope_type is None:
raise ValueError(f"rope_scaling is missing rope_type/type: {rope_scaling}")
if rope_type == "default":
rope_theta = getattr(config, "rope_theta", 1000000) # Qwen3
inv_freq = 1.0 / (
rope_theta
** (
torch.arange(0, rotary_dim, 2, device=device, dtype=torch.float32)
/ rotary_dim
)
)
attention_scaling = 1.0
else:
inv_freq, attention_scaling = ROPE_INIT_FUNCTIONS[rope_type](
config=config,
device=device,
seq_len=int(positions.max().item()) + 1,
)
inv_freq = inv_freq[: rotary_dim // 2].to(device=device, dtype=torch.float32)
positions = positions.to(device=device, dtype=torch.float32)
freqs = torch.outer(positions, inv_freq)
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos().to(dtype=dtype)
sin = emb.sin().to(dtype=dtype)
return cos[:, None, :], sin[:, None, :], float(attention_scaling)
def _rerotate_k_cache(
*,
k_flat: torch.Tensor,
old_positions: torch.Tensor,
new_positions: torch.Tensor,
config: AutoConfig,
head_size: int,
is_neox_style: bool,
) -> torch.Tensor:
"""Move key cache RoPE from old positions to new positions."""
num_tokens = k_flat.shape[0]
k = k_flat.view(num_tokens, -1, head_size)
partial_factor = getattr(config, "partial_rotary_factor", 1.0)
rotary_dim = int(head_size * partial_factor)
if rotary_dim % 2 != 0:
raise ValueError(f"rotary_dim must be even, got {rotary_dim}")
k_rot = k[..., :rotary_dim]
k_pass = k[..., rotary_dim:]
old_cos, old_sin, old_scale = _rope_cos_sin(
config=config,
positions=old_positions,
rotary_dim=rotary_dim,
device=k.device,
dtype=k.dtype,
)
new_cos, new_sin, new_scale = _rope_cos_sin(
config=config,
positions=new_positions,
rotary_dim=rotary_dim,
device=k.device,
dtype=k.dtype,
)
rotate_half = _rotate_half_neox if is_neox_style else _rotate_half_interleaved
# Detach old RoPE: inverse rotation
k_unscaled = k_rot / old_scale
k_plain = (k_unscaled * old_cos) - (rotate_half(k_unscaled) * old_sin)
# Attach new RoPE.
k_new = new_scale * ((k_plain * new_cos) + (rotate_half(k_plain) * new_sin))
return torch.cat((k_new, k_pass), dim=-1).reshape_as(k_flat)
def rerotate_k_cache(
kv_tensor: torch.Tensor,
old_positions: torch.Tensor,
new_positions: torch.Tensor,
model_config: AutoConfig,
) -> torch.Tensor:
"""Move key cache RoPE from old positions to new positions.
Args:
kv_tensor: The KV cache tensor of shape [2, L, T, D].
old_positions: The old token positions.
new_positions: The new token positions.
model_config: The model configuration.
Returns:
The rerotated KV cache tensor.
Raises:
ValueError: If the input tensor shape or dimensions are invalid.
"""
if kv_tensor.ndim != 4:
raise ValueError(f"kv_tensor must be 4D [2, L, T, D], got {kv_tensor.ndim}D")
head_size = getattr(
model_config,
"head_dim",
model_config.hidden_size // model_config.num_attention_heads,
)
if kv_tensor.shape[3] % head_size != 0:
raise ValueError(
f"hidden dim {kv_tensor.shape[3]} not divisible by head_size {head_size}"
)
is_neox_style = True
for layer in range(kv_tensor.shape[1]):
kv_tensor[0, layer] = _rerotate_k_cache(
k_flat=kv_tensor[0, layer],
old_positions=old_positions,
new_positions=new_positions,
config=model_config,
head_size=head_size,
is_neox_style=is_neox_style,
)
return kv_tensor
def _extract_token_id(choice: dict) -> int:
"""Pull the generated token id from a streaming choice.
Requires the vLLM server to be started with --return-tokens-as-token-ids,
so logprobs token strings look like "token_id:123".
"""
logprobs = choice.get("logprobs") or {}
toks = logprobs.get("tokens") or []
if not toks:
return -1
last = toks[-1]
return int(last.split(":")[-1]) if ":" in last else -1
def make_post_completion(
vllm_url: str, model_name: str, timeout: float
) -> lmc_stream.PostCompletion:
"""Build a streaming post_completion callable for lmcache.sdk.stream.
Args:
vllm_url: The URL of the vLLM server.
model_name: The name of the model.
timeout: Timeout in seconds for HTTP requests.
Returns:
A streaming post_completion callable.
"""
def post_completion(prompt_token_ids, sampling_params, cache_salt):
payload = {
**sampling_params,
"model": model_name,
"prompt": list(prompt_token_ids),
"stream": True,
"logprobs": 0,
}
if cache_salt:
payload["cache_salt"] = cache_salt
timeout_http = httpx.Timeout(
connect=timeout, read=None, write=timeout, pool=timeout
)
with httpx.stream(
"POST",
f"{vllm_url.rstrip('/')}/v1/completions",
json=payload,
timeout=timeout_http,
) as response:
response.raise_for_status()
for line in response.iter_lines():
if not line or not line.startswith("data: "):
continue
if line == "data: [DONE]":
break
body = json.loads(line.removeprefix("data: "))
choices = body.get("choices") or []
if not choices:
continue
choice = choices[0]
yield lmc_stream.TokenEvent(
token_id=_extract_token_id(choice),
text=choice.get("text", ""),
)
return post_completion