Ddavidich's picture
Карточка, лицензия, конфиг, рантайм и QR-коды. Веса следом.
86b5732 verified
Raw
History Blame Contribute Delete
5.4 kB
#!/usr/bin/env python3
"""Decode side of the Lloyd-Max value codec.
Two shapes are needed, mirroring the two paths in vLLM's TurboQuant backend:
``unpack_values_rotated``
Reads the packed indices and the fp16 norm and returns value vectors in
*rotated* space. This is what the fused decode kernel accumulates against:
the attention weight carries the norm and the whole output is rotated back
once, so no cached vector is ever rotated individually.
``unpack_values_model_space``
The materialising form used by the continuation-prefill path, where the
inverse rotation is one batched GEMM over the whole dequantised tensor.
The unpack itself is the key path's MSE branch applied at the value offset:
gather centroids by index, optionally renormalise (norm correction), scale by
the stored vector norm.
"""
from __future__ import annotations
import torch
import triton
import triton.language as tl
@triton.jit
def _zenit_unpack_values(
KV_cache_ptr,
Slot_mapping_ptr,
Centroids_ptr,
Out_ptr,
stride_cache_block: tl.constexpr,
stride_cache_pos: tl.constexpr,
stride_cache_head: tl.constexpr,
D: tl.constexpr,
H: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
BLOCK_D: tl.constexpr,
KEY_PACKED: tl.constexpr,
VAL_BYTES: tl.constexpr,
VAL_BITS: tl.constexpr,
NORM_CORRECTION: tl.constexpr,
):
pid = tl.program_id(0)
token_idx = pid // H
head_idx = pid % H
slot = tl.load(Slot_mapping_ptr + token_idx)
if slot < 0:
return
blk = (slot // BLOCK_SIZE).to(tl.int64)
off = (slot % BLOCK_SIZE).to(tl.int64)
slot_base = (
blk * stride_cache_block
+ off * stride_cache_pos
+ tl.cast(head_idx, tl.int64) * stride_cache_head
)
val_base = slot_base + KEY_PACKED
d_offs = tl.arange(0, BLOCK_D)
d_mask = d_offs < D
if VAL_BITS == 4:
byte_idx = d_offs // 2
shift = (d_offs % 2) * 4
raw = tl.load(KV_cache_ptr + val_base + byte_idx, mask=d_mask, other=0).to(
tl.int32
)
idx = (raw >> shift) & 0xF
elif VAL_BITS == 3:
bit_off = d_offs * 3
byte_idx = bit_off // 8
shift = bit_off % 8
raw0 = tl.load(KV_cache_ptr + val_base + byte_idx, mask=d_mask, other=0).to(
tl.int32
)
raw1 = tl.load(KV_cache_ptr + val_base + byte_idx + 1, mask=d_mask, other=0).to(
tl.int32
)
idx = ((raw0 | (raw1 << 8)) >> shift) & 0x7
else: # VAL_BITS == 2
byte_idx = d_offs // 4
shift = (d_offs % 4) * 2
raw = tl.load(KV_cache_ptr + val_base + byte_idx, mask=d_mask, other=0).to(
tl.int32
)
idx = (raw >> shift) & 0x3
unit = tl.load(Centroids_ptr + idx, mask=d_mask, other=0.0)
if NORM_CORRECTION == 1:
energy = tl.sum(tl.where(d_mask, unit * unit, 0.0), axis=0)
unit = unit / tl.sqrt(tl.maximum(energy, 1e-24))
n_lo = tl.load(KV_cache_ptr + val_base + VAL_BYTES).to(tl.uint16)
n_hi = tl.load(KV_cache_ptr + val_base + VAL_BYTES + 1).to(tl.uint16)
vec_norm = (n_lo | (n_hi << 8)).to(tl.float16, bitcast=True).to(tl.float32)
tl.store(Out_ptr + pid * D + d_offs, (unit * vec_norm).to(tl.float16), mask=d_mask)
def unpack_values_rotated(
kv_cache: torch.Tensor,
slot_mapping: torch.Tensor,
centroids: torch.Tensor,
*,
num_tokens: int,
num_heads: int,
head_dim: int,
key_packed_size: int,
value_bits: int,
norm_correction: bool = True,
) -> torch.Tensor:
"""Value vectors in rotated space, [num_tokens * num_heads, head_dim]."""
import math
val_bytes = math.ceil(head_dim * value_bits / 8)
out = torch.empty(
num_tokens * num_heads, head_dim, dtype=torch.float16, device=kv_cache.device
)
_zenit_unpack_values[(num_tokens * num_heads,)](
kv_cache.view(-1),
slot_mapping,
centroids,
out,
stride_cache_block=kv_cache.stride(0),
stride_cache_pos=kv_cache.stride(1),
stride_cache_head=kv_cache.stride(2),
D=head_dim,
H=num_heads,
BLOCK_SIZE=kv_cache.shape[1],
BLOCK_D=triton.next_power_of_2(head_dim),
KEY_PACKED=key_packed_size,
VAL_BYTES=val_bytes,
VAL_BITS=value_bits,
NORM_CORRECTION=1 if norm_correction else 0,
num_warps=4,
num_stages=1,
)
return out
def unpack_values_model_space(
kv_cache: torch.Tensor,
slot_mapping: torch.Tensor,
centroids: torch.Tensor,
rotation: torch.Tensor,
**kwargs,
) -> torch.Tensor:
"""Materialising form: one batched GEMM undoes the rotation for everyone."""
rotated = unpack_values_rotated(kv_cache, slot_mapping, centroids, **kwargs)
return (rotated.float() @ rotation.T).to(torch.float16)
def rotate_attention_output(output: torch.Tensor, rotation: torch.Tensor) -> torch.Tensor:
"""Fused form: the only inverse rotation the decode path ever performs.
``output`` is whatever the split-KV reduction produced while accumulating
against rotated values, shaped [..., head_dim].
"""
shape = output.shape
flat = output.reshape(-1, shape[-1]).float()
return (flat @ rotation.T).reshape(shape).to(output.dtype)
__all__ = [
"rotate_attention_output",
"unpack_values_model_space",
"unpack_values_rotated",
]