Skip to content

Commit 28da35b

Browse files
LucasWilkinsoncodex
andcommitted
Simplify KV cache layout stride handling
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
1 parent 936abaf commit 28da35b

8 files changed

Lines changed: 91 additions & 64 deletions

File tree

tests/v1/attention/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
KVCacheLayout,
3636
KVCacheSpec,
3737
MambaSpec,
38+
compute_layout_strides,
3839
get_kv_quant_mode,
39-
layer_kv_cache_strides,
4040
reshape_kv_cache,
4141
)
4242

@@ -432,7 +432,7 @@ def dense_kv_cache_views(
432432
block_size: int | None = None,
433433
) -> list[torch.Tensor]:
434434
"""``reshape_kv_cache`` for a dense allocation of ``num_layers`` layers."""
435-
layer_stride, block_stride = layer_kv_cache_strides(
435+
layer_stride, block_stride, _, _, _ = compute_layout_strides(
436436
spec, num_blocks, num_layers, layout, block_size
437437
)
438438
return reshape_kv_cache(

vllm/config/cache.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,8 @@ class CacheConfig:
5757
kv_cache_layout: str | None = field(default=None, init=False)
5858
"""Resolved physical KV cache layout name (a ``KVCacheLayout`` member).
5959
60-
Written exactly once by attention backend selection (priority:
61-
test override > backend-required > VLLM_KV_CACHE_LAYOUT > connector
62-
preference > LBNHC); every consumer reads it from here so allocation,
63-
validation, and connectors can never disagree. Derived automatically."""
60+
Resolved by attention backend selection and synchronized across engine and
61+
worker processes before allocation. Derived automatically."""
6462
prefix_match_unit: int | None = Field(default=None, gt=0)
6563
"""The finest token boundary (in tokens) a prefix-cache hit can land on.
6664

vllm/distributed/kv_transfer/kv_connector/v1/offloading/canonical_mapping.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@
3434
def canonical_format_id() -> str:
3535
"""Identity of the canonical byte format, for namespacing persisted KV.
3636
Canonical pages keep the worker's KV layout family, so the id couples the
37-
format version with that family; consumers must match it exactly."""
37+
format version with that family; consumers must match it exactly.
38+
The family keeps its historical NHD/HND spelling so ids stay stable for
39+
KV persisted before the layout enum existed."""
3840
from vllm.v1.attention.backends.utils import get_kv_cache_layout
41+
from vllm.v1.kv_cache_interface import KVCacheLayout
3942

40-
return f"v{CANONICAL_FORMAT_VERSION}-{get_kv_cache_layout().name.lower()}"
43+
layout = get_kv_cache_layout()
44+
legacy = {KVCacheLayout.LBNHC: "nhd", KVCacheLayout.LBHNC: "hnd"}
45+
family = legacy.get(layout, layout.name.lower())
46+
return f"v{CANONICAL_FORMAT_VERSION}-{family}"
4147

4248

4349
@dataclass(frozen=True)

vllm/v1/attention/backends/utils.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,11 @@ def _validate_backend_supports_layout(
214214
def initialize_kv_cache_layout(
215215
backend: type[AttentionBackend], cache_config=None
216216
) -> None:
217-
"""Resolve the layout once at backend selection and publish it.
217+
"""Resolve the backend-compatible layout and publish it on the worker.
218218
219-
Single writer for the resolved layout: stores it on
220-
``cache_config.kv_cache_layout`` (serialized with the config) and in a
221-
process-local mirror for callers without a config handle. Priority is
222-
main-parity: a backend-required layout silently corrects the env var.
219+
Stores it on ``cache_config.kv_cache_layout`` and in a process-local mirror
220+
for callers without a config handle. A backend-required layout silently
221+
corrects the environment setting.
223222
"""
224223
global _RESOLVED_KV_CACHE_LAYOUT, _RESOLVED_LAYOUT_REQUIRED_BY
225224
if _KV_CACHE_LAYOUT_OVERRIDE is not None:
@@ -271,7 +270,7 @@ def require_block_outer_kv_cache_layout(cache_config=None) -> KVCacheLayout:
271270
honored and raises.
272271
"""
273272
global _RESOLVED_KV_CACHE_LAYOUT
274-
layout = get_kv_cache_layout()
273+
layout = get_kv_cache_layout(cache_config)
275274
if not layout.is_layer_compact:
276275
return layout
277276

@@ -290,18 +289,18 @@ def require_block_outer_kv_cache_layout(cache_config=None) -> KVCacheLayout:
290289
return layout
291290

292291

293-
def get_kv_cache_layout() -> KVCacheLayout:
292+
def get_kv_cache_layout(cache_config=None) -> KVCacheLayout:
294293
"""Return the resolved physical KV cache layout.
295294
296-
Read-only: prefers the test override, then the value published by
297-
``initialize_kv_cache_layout`` (via the process mirror or the current
298-
vllm config), then falls back to env > connector > LBNHC for processes
299-
where backend selection never runs (e.g. the engine core).
295+
Read-only: prefers the test override, then an explicit or current config,
296+
then the process-local value published by ``initialize_kv_cache_layout``.
297+
Processes where backend selection never runs fall back to
298+
env > connector > LBNHC.
300299
"""
301300
if _KV_CACHE_LAYOUT_OVERRIDE is not None:
302301
return _layout_from_name(_KV_CACHE_LAYOUT_OVERRIDE)
303-
if _RESOLVED_KV_CACHE_LAYOUT is not None:
304-
return _RESOLVED_KV_CACHE_LAYOUT
302+
if cache_config is not None and cache_config.kv_cache_layout is not None:
303+
return _layout_from_name(cache_config.kv_cache_layout)
305304

306305
from vllm.config import get_current_vllm_config_or_none
307306

@@ -312,6 +311,8 @@ def get_kv_cache_layout() -> KVCacheLayout:
312311
and vllm_config.cache_config.kv_cache_layout is not None
313312
):
314313
return _layout_from_name(vllm_config.cache_config.kv_cache_layout)
314+
if _RESOLVED_KV_CACHE_LAYOUT is not None:
315+
return _RESOLVED_KV_CACHE_LAYOUT
315316

316317
layout_name = envs.VLLM_KV_CACHE_LAYOUT
317318
if layout_name is None:

vllm/v1/core/kv_cache_utils.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
SlidingWindowMLASpec,
3939
SlidingWindowSpec,
4040
UniformTypeKVCacheSpecs,
41-
layer_kv_cache_strides,
41+
compute_layout_strides,
4242
replace_as,
4343
)
4444
from vllm.v1.kv_cache_spec_registry import KVCacheSpecRegistry
@@ -1270,7 +1270,7 @@ def _resolve_layout_for_groups(
12701270
cache_config=None,
12711271
) -> KVCacheLayout:
12721272
"""Resolve the layout this model's packing can be expressed in."""
1273-
layout = get_kv_cache_layout()
1273+
layout = get_kv_cache_layout(cache_config)
12741274
page_sizes = {
12751275
_get_per_layer_spec(group, layer_name).page_size_bytes
12761276
for group in kv_cache_groups
@@ -1324,28 +1324,27 @@ def get_kv_cache_config_from_groups(
13241324

13251325
layout = _resolve_layout_for_groups(kv_cache_groups, vllm_config.cache_config)
13261326
kv_cache_tensors = []
1327-
dense = len(runs) == 1
13281327
for byte_offset, layer_names, spec in runs:
1329-
page = spec.page_size_bytes
1330-
if dense:
1331-
layer_stride, block_stride = layer_kv_cache_strides(
1332-
spec, num_blocks, len(layer_names), layout
1333-
)
1334-
elif layout.is_layer_compact:
1335-
layer_stride, block_stride = page * num_blocks, page
1336-
else:
1337-
layer_stride, block_stride = page, packed_block_stride
1328+
strides = compute_layout_strides(
1329+
spec,
1330+
num_blocks,
1331+
len(layer_names),
1332+
layout,
1333+
packed_block_stride=packed_block_stride,
1334+
)
1335+
layer_stride, block_stride, _, _, _ = strides # L, B, H, N, C
1336+
offset = (
1337+
byte_offset
1338+
* max(layer_stride, spec.page_size_bytes)
1339+
// spec.page_size_bytes
1340+
)
13381341
kv_cache_tensors.append(
13391342
KVCacheTensor(
13401343
size=size,
13411344
layers=layer_names,
13421345
layer_stride=layer_stride,
13431346
block_stride=block_stride,
1344-
# Layer-outermost layouts give each layer its own region, so
1345-
# a run starts past every preceding run's whole region.
1346-
offset=byte_offset * num_blocks
1347-
if layout.is_layer_compact
1348-
else byte_offset,
1347+
offset=offset,
13491348
)
13501349
)
13511350

vllm/v1/executor/abstract.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,10 @@ def _init_executor(self) -> None:
117117

118118
def initialize_from_config(self, kv_cache_configs: list[KVCacheConfig]) -> None:
119119
"""Initialize the KV caches on the underlying workers."""
120-
self.collective_rpc("initialize_from_config", args=(kv_cache_configs,))
120+
self.collective_rpc(
121+
"initialize_from_config",
122+
args=(kv_cache_configs, self.cache_config.kv_cache_layout),
123+
)
121124

122125
def compile_or_warm_up_model(self) -> None:
123126
"""Compile/warm up the model and capture cudagraphs on workers."""
@@ -147,7 +150,14 @@ def determine_available_memory(self) -> list[int]: # in bytes
147150
return self.collective_rpc("determine_available_memory")
148151

149152
def get_kv_cache_specs(self) -> list[dict[str, KVCacheSpec]]:
150-
return self.collective_rpc("get_kv_cache_spec")
153+
specs: list[dict[str, KVCacheSpec]] = self.collective_rpc("get_kv_cache_spec")
154+
worker_layouts: list[str | None] = self.collective_rpc("get_kv_cache_layout")
155+
layouts = {layout for layout in worker_layouts if layout is not None}
156+
if len(layouts) > 1:
157+
raise ValueError(f"Workers disagree on KV cache layout: {layouts}")
158+
if layouts:
159+
self.cache_config.kv_cache_layout = next(iter(layouts))
160+
return specs
151161

152162
@overload
153163
def collective_rpc(

vllm/v1/kv_cache_interface.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -280,39 +280,41 @@ def compute_layer_kv_cache_shape_bytes(
280280
return (num_blocks, spec.num_heads, num_states, spec.state_content_size_bytes)
281281

282282

283-
def layer_kv_cache_strides(
283+
def compute_layout_strides(
284284
spec: KVCacheSpec,
285285
num_blocks: int,
286286
num_layers: int,
287287
layout: KVCacheLayout,
288288
block_size: int | None = None,
289-
) -> tuple[int, int]:
290-
"""Byte ``(layer_stride, block_stride)`` of a dense ``[L, B, H, N, C]``
291-
allocation in ``layout`` order."""
289+
packed_block_stride: int | None = None,
290+
) -> tuple[int, ...]:
291+
"""Byte strides in logical ``[L, B, H, N, C]`` axis order."""
292292
shape = (
293293
num_layers,
294294
*compute_layer_kv_cache_shape_bytes(spec, num_blocks, block_size),
295295
)
296296
stride_order = layout.stride_order
297297
physical_shape = tuple(shape[i] for i in stride_order)
298-
dense = torch.empty(physical_shape, device="meta").stride()
299298
inv_order = [stride_order.index(i) for i in range(5)]
300-
layer_stride = dense[inv_order[_DIM_L]]
301-
block_stride = dense[inv_order[_DIM_B]]
302299

303-
if padded := getattr(spec, "page_size_padded", None):
300+
padded = getattr(spec, "page_size_padded", None)
301+
if padded is not None:
304302
assert block_size is None or block_size == spec.block_size, (
305303
"Padded KV pages do not support kernel block splitting."
306304
)
307305
assert {inv_order[_DIM_L], inv_order[_DIM_B]} == {0, 1}, (
308306
f"Padded KV pages need L and B outermost, got {layout.name}."
309307
)
310-
# Padding widens every page, so the strides that step over whole
311-
# pages scale with it.
312-
page = prod(shape[2:])
313-
layer_stride = layer_stride // page * padded
314-
block_stride = block_stride // page * padded
315-
return layer_stride, block_stride
308+
309+
logical_tail = prod(physical_shape[2:])
310+
storage_tail = padded if padded is not None else logical_tail
311+
physical = torch.empty((*physical_shape[:2], storage_tail), device="meta")
312+
strides = list(
313+
physical[..., :logical_tail].view(physical_shape).permute(*inv_order).stride()
314+
)
315+
if packed_block_stride is not None and inv_order[_DIM_B] < inv_order[_DIM_L]:
316+
strides[_DIM_B] = packed_block_stride
317+
return tuple(strides)
316318

317319

318320
def reshape_kv_cache(
@@ -338,21 +340,21 @@ def reshape_kv_cache(
338340
# e.g. BHLNC's head stride spans the layers it interleaves); the caller's
339341
# strides place the layers and blocks themselves.
340342
logical_shape = (num_layers, *shape_bytes)
341-
stride_order = layout.stride_order
342-
physical_shape = tuple(logical_shape[i] for i in stride_order)
343-
inv_order = [stride_order.index(i) for i in range(5)]
344-
strides = list(torch.empty(physical_shape, device="meta").stride())
345-
strides[inv_order[_DIM_L]] = layer_stride
346-
strides[inv_order[_DIM_B]] = block_stride
343+
strides = list(
344+
compute_layout_strides(
345+
spec, num_blocks, num_layers, layout, block_size=block_size
346+
)
347+
)
348+
strides[_DIM_L] = layer_stride
349+
strides[_DIM_B] = block_stride
347350
dtype = getattr(spec, "dtype", None)
348351

349-
cache = torch.as_strided(
352+
cache_logical_5d = torch.as_strided(
350353
raw,
351-
size=physical_shape,
354+
size=logical_shape,
352355
stride=tuple(strides),
353356
storage_offset=raw.storage_offset() + offset,
354357
)
355-
cache_logical_5d = cache.permute(*inv_order)
356358

357359
views = []
358360
for layer_idx in range(num_layers):

vllm/v1/worker/worker_base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ def get_kv_cache_spec(self) -> dict[str, KVCacheSpec]:
9999
"""Get specifications for KV cache implementation."""
100100
raise NotImplementedError
101101

102+
def get_kv_cache_layout(self) -> str | None:
103+
"""Return the worker's resolved KV cache layout."""
104+
cache_config = getattr(self.vllm_config, "cache_config", None)
105+
return getattr(cache_config, "kv_cache_layout", None)
106+
102107
def compile_or_warm_up_model(self) -> CompilationTimes:
103108
"""Prepare model for execution through compilation/warmup.
104109
@@ -322,9 +327,15 @@ def init_worker(self, all_kwargs: list[dict[str, Any]]) -> None:
322327
# To make vLLM config available during worker initialization
323328
self.worker = worker_class(**kwargs)
324329

325-
def initialize_from_config(self, kv_cache_configs: list[Any]) -> None:
330+
def initialize_from_config(
331+
self,
332+
kv_cache_configs: list[Any],
333+
kv_cache_layout: str | None = None,
334+
) -> None:
326335
kv_cache_config = kv_cache_configs[self.global_rank]
327336
assert self.vllm_config is not None
337+
if kv_cache_layout is not None:
338+
self.vllm_config.cache_config.kv_cache_layout = kv_cache_layout
328339
with set_current_vllm_config(self.vllm_config):
329340
self.worker.initialize_from_config(kv_cache_config) # type: ignore
330341

0 commit comments

Comments
 (0)