241 lines
7.5 KiB
Python
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
|