768 lines
29 KiB
Python
768 lines
29 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Format-dispatched geometry now lives in the ``kv_format`` package: each
|
|
# ``EngineKVFormat`` has a :class:`KVFormatSpec` (geometry accessors) and
|
|
# detection is split per engine in ``kv_format.detection``. The public
|
|
# functions below are a thin, backwards-compatible facade that delegates
|
|
# to ``get_spec`` / ``get_spec_class`` / ``detect_format`` so existing
|
|
# callers keep working unchanged.
|
|
# mypy: disable-error-code="union-attr,call-overload"
|
|
# Standard
|
|
from collections.abc import Hashable, Sequence
|
|
from typing import TYPE_CHECKING, Optional, Union
|
|
|
|
# Third Party
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.logging import init_logger
|
|
from lmcache.python_ops_fallback import set_shape_desc_dtype
|
|
from lmcache.utils import EngineType, lmcache_deprecate
|
|
from lmcache.v1.config import LMCacheEngineConfig
|
|
from lmcache.v1.gpu_connector.kv_format import (
|
|
concrete_shape,
|
|
describe_shape,
|
|
detect_format,
|
|
get_spec,
|
|
get_spec_class,
|
|
)
|
|
from lmcache.v1.gpu_connector.kv_format.types import DiscoverableKVCache, LayoutHints
|
|
|
|
if TYPE_CHECKING:
|
|
# First Party
|
|
from lmcache.v1.gpu_connector.gpu_connectors import GPUConnectorInterface
|
|
|
|
# First Party
|
|
import lmcache.c_ops as lmc_ops
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
|
|
def assert_contiguous(tensor: torch.Tensor) -> None:
|
|
"""Assert that *tensor* has a contiguous physical layout with zero offset.
|
|
|
|
LMCache transfer kernels assume logical and physical views match for
|
|
coalesced memory accesses. Used at boundaries where we receive a
|
|
tensor we can't or shouldn't permute (e.g. raw CUDA-IPC reconstruction
|
|
in :class:`~lmcache.v1.platform.cuda.ipc_wrapper.RawCudaIPCWrapper`).
|
|
|
|
Raises:
|
|
ValueError: If *tensor* has a nonzero storage offset, or is
|
|
non-contiguous.
|
|
"""
|
|
if tensor.storage_offset() != 0:
|
|
raise ValueError(f"expected storage_offset 0, got {tensor.storage_offset()}")
|
|
if not tensor.is_contiguous():
|
|
raise ValueError("tensor is not contiguous")
|
|
|
|
|
|
def need_gpu_interm_buffer(lmcache_config: LMCacheEngineConfig):
|
|
"""
|
|
Check if the GPU Connector needs to create an intermediate
|
|
buffer on the GPU
|
|
"""
|
|
if lmcache_config.enable_pd:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def assert_layerwise_gpu_connector(gpu_connector: "GPUConnectorInterface"):
|
|
"""
|
|
Assert that a GPU Connector is a layerwise connector.
|
|
"""
|
|
# Import at runtime to avoid circular dependency
|
|
# First Party
|
|
from lmcache.v1.gpu_connector import gpu_connectors, xpu_connectors
|
|
|
|
valid_connectors = (
|
|
gpu_connectors.VLLMPagedMemLayerwiseGPUConnector,
|
|
gpu_connectors.VLLMBufferLayerwiseGPUConnector,
|
|
gpu_connectors.SGLangLayerwiseGPUConnector,
|
|
xpu_connectors.VLLMPagedMemLayerwiseXPUConnector,
|
|
xpu_connectors.VLLMBufferLayerwiseXPUConnector,
|
|
xpu_connectors.SGLangLayerwiseXPUConnector,
|
|
)
|
|
|
|
assert isinstance(gpu_connector, valid_connectors)
|
|
|
|
|
|
def is_mla(engine_kv_format: "lmc_ops.EngineKVFormat") -> bool:
|
|
"""Return ``True`` for a Multi-head Latent Attention (MLA) layout."""
|
|
return lmc_ops.is_mla(engine_kv_format)
|
|
|
|
|
|
def get_engine_kv_shape_description(engine_kv_format: "lmc_ops.EngineKVFormat") -> str:
|
|
"""Return a human-readable symbolic shape legend for the Engine KV format.
|
|
|
|
Uses short names matching the ``EngineKVFormat`` enum convention:
|
|
NB=num_blocks, NL=num_layers, BS=block_size, NH=num_heads,
|
|
HS=head_size, PBS=page_buffer_size (NB*BS).
|
|
"""
|
|
try:
|
|
return describe_shape(engine_kv_format)
|
|
except KeyError:
|
|
return f"Unknown ({engine_kv_format})"
|
|
|
|
|
|
def get_attention_backend(engine_kv_format: "lmc_ops.EngineKVFormat") -> str:
|
|
"""Return a representative attention-backend label for the format.
|
|
|
|
Diagnostic only. A format may be produced by several (engine,
|
|
attention-backend) combinations; the spec lists them in
|
|
``attention_backends`` and this returns the first (canonical) one.
|
|
"""
|
|
try:
|
|
backends = get_spec_class(engine_kv_format).attention_backends
|
|
except ValueError:
|
|
return f"Unknown ({engine_kv_format})"
|
|
return backends[0] if backends else f"Unknown ({engine_kv_format})"
|
|
|
|
|
|
def get_concrete_engine_kv_shape(
|
|
kv_caches: DiscoverableKVCache, engine_kv_format: "lmc_ops.EngineKVFormat"
|
|
) -> str:
|
|
"""Return the shape with actual numeric values substituted.
|
|
|
|
For example, instead of ``NL x [2, NB, BS, NH, HS]``
|
|
this returns ``80 x [2, 2048, 128, 8, 128]``.
|
|
"""
|
|
try:
|
|
return get_spec(kv_caches, engine_kv_format).concrete_shape_str()
|
|
except ValueError:
|
|
return f"Unknown ({engine_kv_format})"
|
|
|
|
|
|
def get_concrete_engine_kv_shape_from_shape_desc(
|
|
shape_desc: "lmc_ops.PageBufferShapeDesc",
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
) -> str:
|
|
"""Return the concrete shape for a single kernel group's ``shape_desc``.
|
|
|
|
Like :func:`get_concrete_engine_kv_shape`, but the numeric values are
|
|
read from a per-group :class:`PageBufferShapeDesc` rather than from the
|
|
whole ``kv_caches`` structure. This makes the result *group-accurate*:
|
|
``shape_desc.nl`` is the layer count of the group (not the model total),
|
|
so each kernel group of a hybrid model reports its own shape.
|
|
|
|
For example, instead of ``NL x [2, NB, BS, NH, HS]`` this returns
|
|
``80 x [2, 2048, 128, 8, 128]``.
|
|
|
|
Args:
|
|
shape_desc: The kernel group's shape descriptor. Sizes are read
|
|
from its ``nl``/``nb``/``bs``/``nh``/``hs`` fields; fused
|
|
page-buffer (``NBBS``) formats use ``nb * bs``.
|
|
engine_kv_format: The format whose member name is the shape template.
|
|
|
|
Returns:
|
|
The shape string with numeric values substituted, or
|
|
``"Unknown (<format>)"`` for an unrecognised format.
|
|
"""
|
|
sizes = {
|
|
"NB": shape_desc.nb,
|
|
"NL": shape_desc.nl,
|
|
"BS": shape_desc.bs,
|
|
"NH": shape_desc.nh,
|
|
"HS": shape_desc.hs,
|
|
"PBS": shape_desc.nb * shape_desc.bs,
|
|
}
|
|
try:
|
|
return concrete_shape(engine_kv_format, lambda label: sizes[label])
|
|
except KeyError:
|
|
return f"Unknown ({engine_kv_format})"
|
|
|
|
|
|
def legible_print_engine_kv_format(engine_kv_format: "lmc_ops.EngineKVFormat"):
|
|
"""
|
|
Print the Engine KV Format in a legible way
|
|
"""
|
|
shape = get_engine_kv_shape_description(engine_kv_format)
|
|
backend = get_attention_backend(engine_kv_format)
|
|
if shape.startswith("Unknown"):
|
|
logger.warning(f"Unknown Engine KV Format: {engine_kv_format}")
|
|
else:
|
|
logger.info("Engine KV Format: %s", shape)
|
|
logger.info("Currently used by:\n - %s", backend)
|
|
|
|
|
|
def normalize_kv_and_discover_format(
|
|
kv_caches: DiscoverableKVCache,
|
|
serving_engine: EngineType,
|
|
layout_hints: "LayoutHints | None" = None,
|
|
) -> tuple["lmc_ops.EngineKVFormat", DiscoverableKVCache]:
|
|
"""Normalize ``kv_caches`` into canonical form and discover its Engine KV format.
|
|
|
|
Thin wrapper over
|
|
:func:`lmcache.v1.gpu_connector.kv_format.detect_format`; see that
|
|
function for the full contract.
|
|
|
|
Args:
|
|
kv_caches: The KV cache tensors (possibly nested lists of tensors).
|
|
serving_engine: Which serving engine produced the caches.
|
|
layout_hints: See :class:`LayoutHints`.
|
|
|
|
Returns:
|
|
``(engine_kv_format, normalized_kv_caches)``. Callers must use the
|
|
returned tensor structure for subsequent operations -- it shares
|
|
storage with the input but may be a permuted view.
|
|
"""
|
|
return detect_format(kv_caches, serving_engine, layout_hints)
|
|
|
|
|
|
def normalize_and_discover_per_layer_formats(
|
|
kv_caches: "DiscoverableKVCache",
|
|
layer_index_groups: "Sequence[Sequence[int]]",
|
|
serving_engine: EngineType,
|
|
layout_hints: "LayoutHints | None" = None,
|
|
) -> "tuple[DiscoverableKVCache, list[lmc_ops.EngineKVFormat]]":
|
|
"""Normalize the KV caches and return one Engine KV format per layer.
|
|
|
|
Reports each layer's own format, so models whose layers do not all share one
|
|
format -- e.g. a K+V main cache (``kv_size=2``) alongside a key-only MLA index
|
|
cache (``kv_size=1``) -- get a correct per-layer format rather than a single
|
|
model-wide one.
|
|
|
|
Args:
|
|
kv_caches: The registered KV caches: a per-layer list, or a single fused
|
|
tensor for cross-layer formats.
|
|
layer_index_groups: Layer indices of each engine group (one inner
|
|
sequence per group). Empty means a single non-hybrid group.
|
|
serving_engine: Which serving engine produced the caches.
|
|
layout_hints: See :class:`LayoutHints`.
|
|
|
|
Returns:
|
|
``(normalized_kv_caches, engine_kv_formats)``: the canonical KV cache
|
|
structure and one format per layer (length equals the layer count),
|
|
ready for :func:`lmcache.v1.kv_layer_groups.group_layers_by_identity`.
|
|
"""
|
|
|
|
# Detect the whole structure once. A format that isn't a per-layer list (a
|
|
# cross-layer tensor, or a K/V-split) is single-format -- return it whole.
|
|
whole_format, whole_normalized = detect_format(
|
|
kv_caches, serving_engine, layout_hints
|
|
)
|
|
if not lmc_ops.is_layer_list(whole_format):
|
|
return whole_normalized, [whole_format] * get_num_layers(
|
|
whole_normalized, whole_format
|
|
)
|
|
|
|
# Per-layer list: re-detect per engine group, split by tensor shape so a group
|
|
# that mixes layouts gets the right format per layer.
|
|
groups = layer_index_groups or [range(len(kv_caches))]
|
|
detected: dict[int, tuple[DiscoverableKVCache, "lmc_ops.EngineKVFormat"]] = {}
|
|
for indices in groups:
|
|
layers_by_shape: dict[Hashable, list[int]] = {}
|
|
for i in indices:
|
|
shape = getattr(kv_caches[i], "shape", None)
|
|
key = tuple(shape) if shape is not None else None
|
|
layers_by_shape.setdefault(key, []).append(i)
|
|
for same_shape_indices in layers_by_shape.values():
|
|
fmt, normalized = detect_format(
|
|
[kv_caches[i] for i in same_shape_indices],
|
|
serving_engine,
|
|
layout_hints,
|
|
)
|
|
for sub_idx, layer_idx in enumerate(same_shape_indices):
|
|
detected[layer_idx] = (normalized[sub_idx], fmt)
|
|
|
|
# A layer in no group (cross-layer KV sharing) keeps its own tensor and is
|
|
# skipped downstream; give it any detected format so every layer has one.
|
|
fallback_format = next(fmt for _, fmt in detected.values())
|
|
normalized_per_layer = [
|
|
detected[i][0] if i in detected else kv_caches[i] for i in range(len(kv_caches))
|
|
]
|
|
engine_kv_formats = [
|
|
detected[i][1] if i in detected else fallback_format
|
|
for i in range(len(kv_caches))
|
|
]
|
|
return normalized_per_layer, engine_kv_formats
|
|
|
|
|
|
def get_num_layers(
|
|
kv_caches: DiscoverableKVCache, engine_kv_format: "lmc_ops.EngineKVFormat"
|
|
) -> int:
|
|
"""Return the number of layers from ``kv_caches``."""
|
|
return get_spec(kv_caches, engine_kv_format).num_layers()
|
|
|
|
|
|
def get_num_blocks(
|
|
kv_caches: DiscoverableKVCache, engine_kv_format: "lmc_ops.EngineKVFormat"
|
|
) -> int:
|
|
"""Return the number of blocks from ``kv_caches``.
|
|
|
|
Raises:
|
|
ValueError: For NBBS-fused formats with no separate block axis.
|
|
"""
|
|
return get_spec(kv_caches, engine_kv_format).num_blocks()
|
|
|
|
|
|
def get_block_size(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int = 0,
|
|
) -> int:
|
|
"""Return the block size (tokens per block) for layer ``layer_idx``.
|
|
|
|
``layer_idx`` is honoured only for per-layer formats where BS may
|
|
differ across layers (e.g. mixed-compression MLA pools). For
|
|
cross-layer formats BS is shared across layers and ``layer_idx``
|
|
is ignored.
|
|
|
|
Raises:
|
|
ValueError: For NBBS-fused formats with no separate block axis.
|
|
"""
|
|
return get_spec(kv_caches, engine_kv_format).block_size(layer_idx)
|
|
|
|
|
|
@lmcache_deprecate(
|
|
"page_buffer_size is only used by the legacy non-MP (in-process) connectors; "
|
|
"the MP transfer path reads geometry from a per-group PageBufferShapeDesc instead"
|
|
)
|
|
def get_page_buffer_size(
|
|
kv_caches: DiscoverableKVCache, engine_kv_format: "lmc_ops.EngineKVFormat"
|
|
) -> int:
|
|
"""Return the page buffer size (num_blocks * block_size) from ``kv_caches``."""
|
|
return get_spec(kv_caches, engine_kv_format).page_buffer_size()
|
|
|
|
|
|
def get_num_heads(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int = 0,
|
|
) -> int:
|
|
"""Return the number of heads for a layer (defaults to layer 0)."""
|
|
return get_spec(kv_caches, engine_kv_format).num_heads(layer_idx)
|
|
|
|
|
|
def get_hidden_dim_size(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int = 0,
|
|
) -> int:
|
|
"""Return the hidden dimension for a layer (defaults to layer 0)."""
|
|
return get_spec(kv_caches, engine_kv_format).hidden_dim(layer_idx)
|
|
|
|
|
|
def get_head_size(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int = 0,
|
|
) -> int:
|
|
"""Return the head size for a layer (defaults to layer 0)."""
|
|
return get_spec(kv_caches, engine_kv_format).head_size(layer_idx)
|
|
|
|
|
|
def get_tokens_per_layer(
|
|
kv_caches: DiscoverableKVCache, engine_kv_format: "lmc_ops.EngineKVFormat"
|
|
) -> int:
|
|
"""Return the number of tokens per layer (num_blocks * block_size)."""
|
|
return get_spec(kv_caches, engine_kv_format).tokens_per_layer()
|
|
|
|
|
|
def get_elements_per_layer(
|
|
kv_caches: DiscoverableKVCache, engine_kv_format: "lmc_ops.EngineKVFormat"
|
|
) -> int:
|
|
"""Return the number of elements per layer (both K and V for non-MLA)."""
|
|
return get_spec(kv_caches, engine_kv_format).elements_per_layer()
|
|
|
|
|
|
def get_dtype(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int = 0,
|
|
) -> torch.dtype:
|
|
"""Return the dtype for a layer (defaults to layer 0)."""
|
|
return get_spec(kv_caches, engine_kv_format).dtype(layer_idx)
|
|
|
|
|
|
def get_group_data_ptrs(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_indices: list[int],
|
|
) -> list[int]:
|
|
"""Return device pointers for a group of layers in kernel-expected order.
|
|
|
|
See :meth:`KVFormatSpec.data_ptrs` for the per-format pointer-array
|
|
shape (per-layer list, SGLang K-then-V, or single cross-layer base).
|
|
|
|
Args:
|
|
kv_caches: Full kv_caches structure.
|
|
engine_kv_format: Format returned by :func:`normalize_kv_and_discover_format`.
|
|
layer_indices: 0-based layer indices in the group, in kernel order.
|
|
|
|
Returns:
|
|
Device pointers (int), in kernel-expected order.
|
|
|
|
Raises:
|
|
ValueError: If *engine_kv_format* is not recognized.
|
|
"""
|
|
return get_spec(kv_caches, engine_kv_format).data_ptrs(layer_indices)
|
|
|
|
|
|
def assert_is_vllm_flash_attn_or_flash_infer(
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
):
|
|
"""
|
|
Ensure that we have an Engine KV Cache Format
|
|
that is either vLLM's flash attention or flash infer.
|
|
"""
|
|
assert engine_kv_format in (
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_NH_BS_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_NH_BS_HS,
|
|
# Blocks-first fused K/V (vLLM CPU): a per-layer non-MLA layout that
|
|
# shares this transfer path even though it is not literally flash-*.
|
|
lmc_ops.EngineKVFormat.NL_X_NB_NH_BS_TWO_HS,
|
|
)
|
|
|
|
|
|
def assert_is_vllm_mla_or_flash_attn_or_flash_infer(
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
) -> None:
|
|
"""
|
|
Ensure that we have an Engine KV Cache Format that is either
|
|
vLLM's MLA, flash attention, or flash infer.
|
|
|
|
Accepted formats:
|
|
- ``NL_X_TWO_NB_BS_NH_HS`` (flash attention, NHD)
|
|
- ``NL_X_NB_TWO_BS_NH_HS`` (flash infer, NHD)
|
|
- ``NL_X_TWO_NB_NH_BS_HS`` (flash attention, HND)
|
|
- ``NL_X_NB_TWO_NH_BS_HS`` (flash infer, HND)
|
|
- ``NL_X_NB_BS_HS`` (MLA)
|
|
|
|
Raises:
|
|
AssertionError: If *engine_kv_format* is not one of the accepted formats.
|
|
"""
|
|
assert engine_kv_format in (
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_TWO_NB_NH_BS_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_NB_TWO_NH_BS_HS,
|
|
lmc_ops.EngineKVFormat.NL_X_NB_BS_HS,
|
|
)
|
|
|
|
|
|
def get_device(kv_caches: DiscoverableKVCache) -> torch.device:
|
|
"""Return the device of the KV cache tensors.
|
|
|
|
Descends into any list nesting until a tensor is found; assumes all
|
|
tensors in *kv_caches* live on the same device (true for every
|
|
current :class:`EngineKVFormat`).
|
|
"""
|
|
probe: DiscoverableKVCache = kv_caches
|
|
while isinstance(probe, list):
|
|
probe = probe[0]
|
|
return probe.device
|
|
|
|
|
|
# Formats whose per-layer tensor dim-0 is the *block* axis AND for
|
|
# which we currently support dim-0 padding (e.g. DeepSeek V4
|
|
# compressor / indexer caches sharing a KV pool with larger attn
|
|
# groups). Today only the MLA layout (``NL_X_NB_BS_HS``, kv_size==1)
|
|
# is exercised by real mixed-compression workloads.
|
|
#
|
|
# ``NL_X_NB_TWO_BS_NH_HS`` *could* in principle also be the block
|
|
# axis on dim-0, but no real serving engine emits a padded layout of
|
|
# that format yet, and supporting it would require: (a) deciding
|
|
# (without a ground-truth example) which axis carries the padding
|
|
# -- NB boundary vs K<->V offset -- and (b) a coordinated change in
|
|
# ``attempt_permute_to_contiguous_view`` to let interior-dim padding
|
|
# through for that one format. Rather than ship an unverifiable code
|
|
# path, we keep ``NL_X_NB_TWO_BS_NH_HS`` out of this set, which means
|
|
# any padded tensor of that format will fail loudly via the
|
|
# non-block-axis dim-0-padding check below. Revisit and add a
|
|
# properly-tested branch when a concrete use case lands.
|
|
_BLOCK_AXIS_FORMATS: frozenset = frozenset(
|
|
{
|
|
lmc_ops.EngineKVFormat.NL_X_NB_BS_HS,
|
|
}
|
|
)
|
|
|
|
|
|
def resolve_block_stride_and_log_layout(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int,
|
|
group_idx: int,
|
|
) -> Optional[int]:
|
|
"""Resolve the per-block stride for a KV layer group and log its layout.
|
|
|
|
Single entry point for :class:`KVLayerGroupsManager` to obtain the
|
|
``block_stride_elems`` value for :class:`PageBufferShapeDesc` and emit
|
|
a one-shot layout audit line. All ``EngineKVFormat``-aware reasoning is
|
|
kept here so callers never touch a "representative KV cache" tensor.
|
|
|
|
* Block-axis formats (:data:`_BLOCK_AXIS_FORMATS`): ``stride(0)`` is
|
|
the per-block step and is returned as-is. A value larger than the
|
|
tight stride indicates dim-0 padding (e.g. DeepSeek V4 compressor
|
|
caches sharing a KV pool with larger attn groups).
|
|
* Other formats: dim-0 is not the block axis, so ``None`` is
|
|
returned and ``shape_desc`` falls back to the tight stride. Any
|
|
dim-0 padding in such formats is rejected with ``ValueError``
|
|
since downstream kernels cannot honour it.
|
|
|
|
Args:
|
|
kv_caches: Full KV cache structure (already normalised).
|
|
engine_kv_format: Format of ``kv_caches``.
|
|
layer_idx: 0-based layer index used as the layout probe.
|
|
group_idx: 0-based group index, used only for logging.
|
|
|
|
Returns:
|
|
``stride(0)`` for block-axis formats; ``None`` otherwise.
|
|
|
|
Raises:
|
|
ValueError: Non-block-axis format carries dim-0 padding.
|
|
"""
|
|
|
|
def _pick_layout_probe_tensor() -> torch.Tensor:
|
|
# Layout probe only (shape/stride/storage_offset/dtype); not
|
|
# the K/V slice fed to the transfer kernel.
|
|
# - Cross-layer formats: ``kv_caches`` is the single backing
|
|
# tensor packing all layers along dim-1; indexing dim-0
|
|
# (= NB) would yield a per-block slice, so return the whole
|
|
# tensor (its ``stride(0)`` is the authoritative per-NB step).
|
|
# - SGL MHA: outer list is K/V (length 2), inner list is
|
|
# per-layer; K & V share shape/stride by construction.
|
|
# - Other formats: ``kv_caches`` is already a per-layer list.
|
|
if lmc_ops.is_cross_layer(engine_kv_format):
|
|
if not isinstance(kv_caches, torch.Tensor):
|
|
raise TypeError(
|
|
"Cross-layer EngineKVFormat expects a single backing "
|
|
f"torch.Tensor, got {type(kv_caches).__name__}."
|
|
)
|
|
return kv_caches
|
|
if lmc_ops.is_kv_list(engine_kv_format):
|
|
return kv_caches[0][layer_idx] # type: ignore[index,return-value]
|
|
return kv_caches[layer_idx] # type: ignore[index,return-value]
|
|
|
|
rep = _pick_layout_probe_tensor()
|
|
|
|
block_stride_elems: Optional[int]
|
|
if engine_kv_format in _BLOCK_AXIS_FORMATS and rep.ndim > 0:
|
|
block_stride_elems = int(rep.stride(0))
|
|
else:
|
|
# Non-block-axis format: detect forbidden dim-0 padding.
|
|
if rep.ndim >= 2:
|
|
tight_dim0 = 1
|
|
for d in range(1, rep.ndim):
|
|
tight_dim0 *= int(rep.shape[d])
|
|
padding = int(rep.stride(0)) - tight_dim0
|
|
if padding > 0:
|
|
raise ValueError(
|
|
"resolve_block_stride_and_log_layout: group's probe "
|
|
f"tensor has dim-0 padding ({padding} elements per "
|
|
f"block) but engine_kv_format={engine_kv_format!r} is not "
|
|
"a supported dim-0-padded format (only "
|
|
"NL_X_NB_BS_HS is); downstream transfer kernels "
|
|
"cannot honour this padding and would read/write "
|
|
"wrong bytes. "
|
|
f"layer_idx={layer_idx}, shape={tuple(rep.shape)}, "
|
|
f"stride={tuple(rep.stride())}, "
|
|
f"tight_stride0={tight_dim0}, "
|
|
f"storage_offset={int(rep.storage_offset())}, "
|
|
f"dtype={rep.dtype}."
|
|
)
|
|
block_stride_elems = None
|
|
|
|
# Best-effort layout audit log; the log line itself must not raise.
|
|
shape = tuple(rep.shape)
|
|
stride = tuple(rep.stride())
|
|
try:
|
|
inner = 1
|
|
for s in shape[1:]:
|
|
inner *= int(s)
|
|
padding_per_block = stride[0] - inner if stride else 0
|
|
except Exception:
|
|
padding_per_block = -1
|
|
try:
|
|
storage_nbytes = rep.untyped_storage().nbytes()
|
|
except Exception:
|
|
storage_nbytes = -1
|
|
logger.info(
|
|
"Group %d first-layer tensor: layer_idx=%d shape=%s "
|
|
"stride=%s is_contiguous=%s dtype=%s device=%s "
|
|
"storage_offset=%d numel=%d storage_nbytes=%d "
|
|
"padding_per_block=%d",
|
|
group_idx,
|
|
layer_idx,
|
|
shape,
|
|
stride,
|
|
rep.is_contiguous(),
|
|
rep.dtype,
|
|
rep.device,
|
|
rep.storage_offset(),
|
|
rep.numel(),
|
|
storage_nbytes,
|
|
padding_per_block,
|
|
)
|
|
|
|
return block_stride_elems
|
|
|
|
|
|
def make_page_buffer_shape_desc(
|
|
kv_caches: DiscoverableKVCache,
|
|
engine_kv_format: "lmc_ops.EngineKVFormat",
|
|
layer_idx: int,
|
|
num_layers_in_group: int,
|
|
num_blocks: int,
|
|
block_size: int,
|
|
block_stride_elems: Optional[int] = None,
|
|
) -> "lmc_ops.PageBufferShapeDesc":
|
|
"""Build a :class:`PageBufferShapeDesc` from a representative layer.
|
|
|
|
Args:
|
|
kv_caches: Full kv_caches structure.
|
|
engine_kv_format: Format returned by :func:`normalize_kv_and_discover_format`.
|
|
layer_idx: 0-based index of the representative layer.
|
|
num_layers_in_group: Number of layers in the group (``nl``).
|
|
num_blocks: Number of paged blocks (``nb``).
|
|
block_size: Tokens per block (``bs``).
|
|
block_stride_elems: Physical per-block stride in *elements*
|
|
(= ``tensor.stride(0)`` of the representative layer). Pass
|
|
the real value whenever the group's KV pool may be
|
|
dim-0-padded (e.g. DeepSeek V4 compressor/indexer caches
|
|
sharing a row width with a larger group in the same pool);
|
|
otherwise downstream transfer kernels will skip into
|
|
padding and corrupt data. Leave as ``None`` for unpadded
|
|
pools -- the kernel's ``per_block_stride()`` fallback
|
|
(block_stride_elems <= 0) will reconstruct the tight
|
|
stride from ``kv_size`` and ``scalars_per_block`` itself,
|
|
so we don't duplicate that arithmetic on the Python side.
|
|
|
|
Returns:
|
|
A populated ``PageBufferShapeDesc``.
|
|
"""
|
|
desc = lmc_ops.PageBufferShapeDesc()
|
|
desc.kv_size = 1 if is_mla(engine_kv_format) else 2
|
|
desc.nl = num_layers_in_group
|
|
desc.nb = num_blocks
|
|
desc.bs = block_size
|
|
desc.nh = (
|
|
1
|
|
if is_mla(engine_kv_format)
|
|
else get_num_heads(kv_caches, engine_kv_format, layer_idx)
|
|
)
|
|
desc.hs = get_head_size(kv_caches, engine_kv_format, layer_idx)
|
|
dtype = get_dtype(kv_caches, engine_kv_format, layer_idx)
|
|
desc.element_size = dtype.itemsize
|
|
# The C++ PageBufferShapeDesc has no ``dtype`` field, but the pure-Python
|
|
# CPU fallback does -- and needs it to disambiguate float16 vs bfloat16
|
|
# (both have itemsize 2, so element_size alone is not enough). Best-effort.
|
|
set_shape_desc_dtype(desc, dtype)
|
|
|
|
resolved_stride = int(block_stride_elems) if block_stride_elems else 0
|
|
desc.block_stride_elems = resolved_stride
|
|
return desc
|
|
|
|
|
|
def _split_token2d_kv(token2d: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""
|
|
Accepts either:
|
|
- [2, T, D]
|
|
- [T, 2, D]
|
|
Returns:
|
|
- k_tok: [T, D]
|
|
- v_tok: [T, D]
|
|
"""
|
|
if token2d.dim() != 3:
|
|
raise ValueError(f"Expected token2d dim=3, got {token2d.shape}")
|
|
if token2d.shape[0] == 2: # [2, T, D]
|
|
return token2d[0], token2d[1]
|
|
if token2d.shape[1] == 2: # [T, 2, D]
|
|
return token2d[:, 0, :], token2d[:, 1, :]
|
|
raise ValueError(f"Unrecognized token2d layout: {token2d.shape}")
|
|
|
|
|
|
def _get_head_size_view(
|
|
kv_cache_layer: Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]],
|
|
*,
|
|
use_mla: bool,
|
|
engine_kv_format: Optional["lmc_ops.EngineKVFormat"] = None,
|
|
) -> Union[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
|
|
"""
|
|
Returns flattened views for index_copy/index_select.
|
|
|
|
If engine_kv_format is provided, use it to interpret tensor layout explicitly.
|
|
If not provided, fall back to current structural behavior:
|
|
- MLA: expects Tensor [P, B, HS]
|
|
- Non-MLA: expects either
|
|
* Tensor [2, P, B, NH, HS] OR
|
|
* (k, v) tuple each [P, B, NH, HS]
|
|
(and also supports [P, 2, B, NH, HS] as a safe extension)
|
|
"""
|
|
# -------------------------
|
|
# MLA
|
|
# -------------------------
|
|
if use_mla:
|
|
if not isinstance(kv_cache_layer, torch.Tensor):
|
|
raise ValueError("MLA expects kv_cache_layer as Tensor")
|
|
if kv_cache_layer.dim() != 3:
|
|
raise ValueError(f"MLA expects 3D [P,B,HS], got {kv_cache_layer.shape}")
|
|
p, b, hs = kv_cache_layer.shape
|
|
return kv_cache_layer.view(p * b, hs)
|
|
|
|
# -------------------------
|
|
# non-MLA (K/V)
|
|
# -------------------------
|
|
# If already provided (k, v) in canonical per-layer form, no format needed.
|
|
if not isinstance(kv_cache_layer, torch.Tensor):
|
|
k, v = kv_cache_layer
|
|
if k.dim() != 4 or v.dim() != 4:
|
|
raise ValueError(f"Expected (k,v) 4D [P,B,NH,HS], got {k.shape}, {v.shape}")
|
|
p, b, nh, hs = k.shape
|
|
if v.shape != (p, b, nh, hs):
|
|
raise ValueError(f"k/v shape mismatch: {k.shape} vs {v.shape}")
|
|
return k.view(p * b, nh * hs), v.view(p * b, nh * hs)
|
|
|
|
t = kv_cache_layer
|
|
if t.dim() != 5:
|
|
raise ValueError(f"Expected 5D tensor for non-MLA, got {t.shape}")
|
|
|
|
# If we have the format enum, decode explicitly.
|
|
if engine_kv_format is not None:
|
|
if engine_kv_format == lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS:
|
|
# per-layer: [2, NB, BS, NH, HS]
|
|
if t.shape[0] != 2:
|
|
raise ValueError(
|
|
f"{engine_kv_format} expects [2,NB,BS,NH,HS], got {t.shape}"
|
|
)
|
|
k, v = t[0], t[1] # [NB,BS,NH,HS]
|
|
|
|
elif engine_kv_format == lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS:
|
|
# per-layer: [NB, 2, BS, NH, HS]
|
|
if t.shape[1] != 2:
|
|
raise ValueError(
|
|
f"{engine_kv_format} expects [NB,2,BS,NH,HS], got {t.shape}"
|
|
)
|
|
k, v = t[:, 0], t[:, 1] # [NB,BS,NH,HS]
|
|
|
|
else:
|
|
# Other formats are either MLA-only or require upstream normalization.
|
|
raise NotImplementedError(
|
|
f"engine_kv_format={engine_kv_format} not supported in non-MLA "
|
|
"path here. Normalize to (k,v) tuple [NB,BS,NH,HS] per-layer "
|
|
"before calling."
|
|
)
|
|
|
|
else:
|
|
# No enum available: Assumed [2,P,B,H,D] (or [2,NB,BS,NH,HS] per-layer).
|
|
# Also accept [P,2,B,H,D] (or [NB,2,BS,NH,HS]) to be more robust.
|
|
if t.shape[0] == 2:
|
|
k, v = t[0], t[1]
|
|
elif t.shape[1] == 2:
|
|
k, v = t[:, 0], t[:, 1]
|
|
else:
|
|
raise ValueError(
|
|
f"engine_kv_format is None and tensor does not look like stacked KV. "
|
|
f"Expected axis0==2 or axis1==2, got {t.shape}"
|
|
)
|
|
|
|
if k.dim() != 4 or v.dim() != 4:
|
|
raise ValueError(f"Expected k/v 4D [NB,BS,NH,HS], got {k.shape}, {v.shape}")
|
|
|
|
nb, bs, nh, hs = k.shape
|
|
if v.shape != (nb, bs, nh, hs):
|
|
raise ValueError(f"k/v shape mismatch after decode: {k.shape} vs {v.shape}")
|
|
|
|
return k.view(nb * bs, nh * hs), v.view(nb * bs, nh * hs)
|