kvbm_physical/layout/kv_block_layout.rs
1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! KV Block layout types for describing dimension permutations within blocks.
5//!
6//! This module provides types for describing how dimensions are ordered within
7//! a fully contiguous KV cache block, enabling type-driven kernel selection
8//! for transfers between different layout formats.
9
10use serde::{Deserialize, Serialize};
11
12/// Symbolic dimensions that can be permuted within a block.
13///
14/// The head dimension (hd) is always innermost and not included here.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub enum BlockDim {
17 /// Number of layers (nl)
18 Layer,
19 /// Outer dimension - typically 2 for K/V, 1 for MLA (no)
20 Outer,
21 /// Page size / tokens per block (nt)
22 Page,
23 /// Number of attention heads (nh)
24 Head,
25}
26
27/// Block layout defined by dimension ordering.
28///
29/// Describes how the 4 permutable dimensions (layer, outer, page, head) are
30/// ordered within a fully contiguous block. The head dimension (hd) is always
31/// innermost and implicit.
32///
33/// The order specifies outer-to-inner dimensions, with head_dim always last.
34///
35/// # Examples
36///
37/// - `UniversalTP`: `[nh, nl, no, nt, hd]` - heads outermost for TP resharding
38/// - `OperationalNHD`: `[nl, no, nt, nh, hd]` - inner is `[nt, nh, hd]`
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
40pub enum KvBlockLayout {
41 /// Universal format: `[nh, nl, no, nt, hd]`
42 ///
43 /// Heads are outermost to enable tensor parallelism (TP) resharding.
44 /// Cache saved from one TP configuration can be loaded into another
45 /// by simply slicing the head dimension differently.
46 UniversalTP,
47
48 /// Pipeline parallelism format: `[nl, nh, no, nt, hd]`
49 ///
50 /// Layers are outermost for pipeline parallelism scenarios.
51 UniversalPP,
52
53 /// Operational HND format: `[nl, no, nh, nt, hd]`
54 ///
55 /// Inner tensor shape is `[nh, nt, hd]` (heads, tokens, head_dim).
56 OperationalHND,
57
58 /// Operational NHD format: `[nl, no, nt, nh, hd]`
59 ///
60 /// Inner tensor shape is `[nt, nh, hd]` (tokens, heads, head_dim).
61 /// This is the most common format used by vLLM and other frameworks.
62 OperationalNHD,
63
64 /// Custom ordering with explicit dimension list.
65 ///
66 /// The array specifies dimensions from outermost to innermost,
67 /// with head_dim always implicitly last.
68 Custom([BlockDim; 4]),
69
70 /// Unknown layout - fallback when format cannot be determined.
71 ///
72 /// Operations involving Unknown layouts may fail or require explicit
73 /// configuration.
74 #[default]
75 Unknown,
76}
77
78impl KvBlockLayout {
79 /// Get the dimension ordering as an array.
80 ///
81 /// Returns the 4 dimensions from outermost to innermost.
82 /// Head dimension (hd) is implicit as the innermost dimension.
83 ///
84 /// # Returns
85 /// `None` for `Unknown` layout, `Some([BlockDim; 4])` otherwise.
86 pub fn dim_order(&self) -> Option<[BlockDim; 4]> {
87 use BlockDim::*;
88 match self {
89 Self::UniversalTP => Some([Head, Layer, Outer, Page]),
90 Self::UniversalPP => Some([Layer, Head, Outer, Page]),
91 Self::OperationalHND => Some([Layer, Outer, Head, Page]),
92 Self::OperationalNHD => Some([Layer, Outer, Page, Head]),
93 Self::Custom(order) => Some(*order),
94 Self::Unknown => None,
95 }
96 }
97
98 /// Check if two layouts require transformation (not just copy).
99 ///
100 /// Returns `true` if the layouts have different dimension orderings,
101 /// meaning a transformation kernel is needed rather than a simple copy.
102 ///
103 /// For Unknown→Unknown comparisons, returns `false` (compatible) but emits
104 /// a warning so these cases can be tracked and fixed.
105 ///
106 /// Returns `true` if one is Unknown and the other is Known (conservative).
107 pub fn requires_transform(&self, other: &Self) -> bool {
108 match (self.dim_order(), other.dim_order()) {
109 (Some(a), Some(b)) => a != b,
110 (None, None) => {
111 // Unknown→Unknown is compatible, but warn so we can fix these
112 tracing::warn!("Unknown→Unknown KvBlockLayout comparison - this should be fixed");
113 false
114 }
115 // Unknown→Known requires transform (conservative)
116 _ => true,
117 }
118 }
119
120 /// Check if this is an operational layout (NHD or HND).
121 ///
122 /// Operational layouts are used for direct computation and have
123 /// layer/outer as the outermost dimensions.
124 pub fn is_operational(&self) -> bool {
125 matches!(self, Self::OperationalNHD | Self::OperationalHND)
126 }
127
128 /// Check if this is a universal layout (TP or PP).
129 ///
130 /// Universal layouts are optimized for storage and transfer,
131 /// with different parallelism-friendly orderings.
132 pub fn is_universal(&self) -> bool {
133 matches!(self, Self::UniversalTP | Self::UniversalPP)
134 }
135
136 /// Get the layout name as a string identifier.
137 pub fn name(&self) -> &'static str {
138 match self {
139 Self::UniversalTP => "universal_tp",
140 Self::UniversalPP => "universal_pp",
141 Self::OperationalHND => "operational_hnd",
142 Self::OperationalNHD => "operational_nhd",
143 Self::Custom(_) => "custom",
144 Self::Unknown => "unknown",
145 }
146 }
147
148 /// Try to create a KvBlockLayout from an InnerShape.
149 ///
150 /// This provides compatibility with the existing InnerShape enum.
151 pub(crate) fn from_inner_shape(inner_shape: super::InnerShape) -> Self {
152 match inner_shape {
153 super::InnerShape::NHD => Self::OperationalNHD,
154 super::InnerShape::HND => Self::OperationalHND,
155 super::InnerShape::Unknown => Self::Unknown,
156 }
157 }
158
159 /// Convert to InnerShape if this is an operational layout.
160 ///
161 /// Returns `None` for universal or custom layouts.
162 pub(crate) fn to_inner_shape(self) -> Option<super::InnerShape> {
163 match self {
164 Self::OperationalNHD => Some(super::InnerShape::NHD),
165 Self::OperationalHND => Some(super::InnerShape::HND),
166 Self::Unknown => Some(super::InnerShape::Unknown),
167 _ => None,
168 }
169 }
170}
171
172impl std::fmt::Display for KvBlockLayout {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 match self {
175 Self::UniversalTP => write!(f, "Universal TP [nh, nl, no, nt, hd]"),
176 Self::UniversalPP => write!(f, "Universal PP [nl, nh, no, nt, hd]"),
177 Self::OperationalHND => write!(f, "Operational HND [nl, no, nh, nt, hd]"),
178 Self::OperationalNHD => write!(f, "Operational NHD [nl, no, nt, nh, hd]"),
179 Self::Custom(order) => write!(f, "Custom {:?}", order),
180 Self::Unknown => write!(f, "Unknown"),
181 }
182 }
183}
184
185impl std::fmt::Display for BlockDim {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 match self {
188 Self::Layer => write!(f, "nl"),
189 Self::Outer => write!(f, "no"),
190 Self::Page => write!(f, "nt"),
191 Self::Head => write!(f, "nh"),
192 }
193 }
194}
195
196// ============================================================================
197// KvBlocks - Collection wrapper for blocks with shared layout
198// ============================================================================
199
200use crate::BlockId;
201use crate::layout::PhysicalLayout;
202use std::sync::Arc;
203
204/// A collection of blocks with a shared layout configuration and block layout type.
205///
206/// `KvBlocks` provides a convenient way to group blocks that should be treated
207/// uniformly in transfer operations. All blocks in the collection share:
208/// - The same [`PhysicalLayout`] (memory organization)
209/// - The same [`KvBlockLayout`] interpretation (dimension ordering)
210///
211/// This enables efficient batch transfers with optional layout override.
212///
213/// # Example
214///
215/// ```ignore
216/// // Create blocks with universal layout override
217/// let blocks = KvBlocks::new(
218/// physical_layout.clone(),
219/// vec![0, 1, 2, 3], // block IDs
220/// Some(KvBlockLayout::UniversalTP),
221/// )?;
222///
223/// // Use in transfers - the override tells the transfer system
224/// // to interpret these blocks as universal format
225/// ```
226#[derive(Debug, Clone)]
227pub struct KvBlocks {
228 /// The physical layout containing these blocks
229 layout: Arc<PhysicalLayout>,
230 /// Block IDs within the layout
231 block_ids: Vec<BlockId>,
232 /// Optional layout override (None = use layout's native block_layout)
233 kv_layout_override: Option<KvBlockLayout>,
234}
235
236impl KvBlocks {
237 /// Create a new KvBlocks collection.
238 ///
239 /// # Arguments
240 /// * `layout` - The physical layout containing the blocks
241 /// * `block_ids` - Block IDs to include in this collection
242 /// * `kv_layout_override` - Optional override for the block layout interpretation.
243 /// If `None`, uses the layout's native `block_layout()`.
244 /// If `Some`, overrides the interpretation for transfers.
245 ///
246 /// # Validation
247 /// - For layer-separate layouts, only operational layouts (NHD/HND) are valid overrides
248 /// - For fully contiguous layouts, any layout is valid
249 /// - If the override matches the native layout, it is normalized to None
250 pub fn new(
251 layout: Arc<PhysicalLayout>,
252 block_ids: Vec<BlockId>,
253 kv_layout_override: Option<KvBlockLayout>,
254 ) -> anyhow::Result<Self> {
255 // Validate block IDs are in range
256 let num_blocks = layout.layout().num_blocks();
257 for &id in &block_ids {
258 if id >= num_blocks {
259 return Err(anyhow::anyhow!(
260 "Block ID {} out of range (layout has {} blocks)",
261 id,
262 num_blocks
263 ));
264 }
265 }
266
267 // Validate layout override compatibility
268 if let Some(ref override_layout) = kv_layout_override {
269 // Layer-separate layouts can only use operational formats
270 if !layout.layout().is_fully_contiguous() && !override_layout.is_operational() {
271 return Err(anyhow::anyhow!(
272 "Layer-separate layouts only support operational block layouts (NHD/HND), got {:?}",
273 override_layout
274 ));
275 }
276 }
277
278 // Normalize: if override matches native layout, set to None
279 let normalized_override = kv_layout_override.and_then(|override_layout| {
280 if override_layout == layout.layout().block_layout() {
281 None
282 } else {
283 Some(override_layout)
284 }
285 });
286
287 Ok(Self {
288 layout,
289 block_ids,
290 kv_layout_override: normalized_override,
291 })
292 }
293
294 /// Create a KvBlocks collection without layout override.
295 #[expect(dead_code)]
296 pub fn from_layout(
297 layout: Arc<PhysicalLayout>,
298 block_ids: Vec<BlockId>,
299 ) -> anyhow::Result<Self> {
300 Self::new(layout, block_ids, None)
301 }
302
303 /// Get the physical layout.
304 #[expect(dead_code)]
305 pub fn layout(&self) -> &Arc<PhysicalLayout> {
306 &self.layout
307 }
308
309 /// Get the block IDs.
310 #[expect(dead_code)]
311 pub fn block_ids(&self) -> &[BlockId] {
312 &self.block_ids
313 }
314
315 /// Get the effective block layout (override or native).
316 pub fn effective_block_layout(&self) -> KvBlockLayout {
317 self.kv_layout_override
318 .unwrap_or_else(|| self.layout.layout().block_layout())
319 }
320
321 /// Get the layout override if set.
322 #[expect(dead_code)]
323 pub fn layout_override(&self) -> Option<KvBlockLayout> {
324 self.kv_layout_override
325 }
326
327 /// Check if this collection has a layout override.
328 #[expect(dead_code)]
329 pub fn has_override(&self) -> bool {
330 self.kv_layout_override.is_some()
331 }
332
333 /// Get the number of blocks in this collection.
334 #[expect(dead_code)]
335 pub fn len(&self) -> usize {
336 self.block_ids.len()
337 }
338
339 /// Check if the collection is empty.
340 #[expect(dead_code)]
341 pub fn is_empty(&self) -> bool {
342 self.block_ids.is_empty()
343 }
344
345 /// Check if a transfer between two KvBlocks collections requires transformation.
346 ///
347 /// Returns `true` if the effective layouts differ and a transformation kernel
348 /// is needed rather than a simple copy.
349 #[expect(dead_code)]
350 pub fn requires_transform_to(&self, dst: &KvBlocks) -> bool {
351 self.effective_block_layout()
352 .requires_transform(&dst.effective_block_layout())
353 }
354}
355
356#[cfg(all(test, feature = "testing-kvbm"))]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_dim_order() {
362 use BlockDim::*;
363
364 assert_eq!(
365 KvBlockLayout::UniversalTP.dim_order(),
366 Some([Head, Layer, Outer, Page])
367 );
368 assert_eq!(
369 KvBlockLayout::OperationalNHD.dim_order(),
370 Some([Layer, Outer, Page, Head])
371 );
372 assert_eq!(KvBlockLayout::Unknown.dim_order(), None);
373 }
374
375 #[test]
376 fn test_requires_transform() {
377 // Same layout - no transform
378 assert!(!KvBlockLayout::OperationalNHD.requires_transform(&KvBlockLayout::OperationalNHD));
379
380 // Different layouts - transform required
381 assert!(KvBlockLayout::OperationalNHD.requires_transform(&KvBlockLayout::UniversalTP));
382 assert!(KvBlockLayout::OperationalHND.requires_transform(&KvBlockLayout::OperationalNHD));
383
384 // Unknown→Known requires transform (conservative)
385 assert!(KvBlockLayout::Unknown.requires_transform(&KvBlockLayout::OperationalNHD));
386 assert!(KvBlockLayout::OperationalNHD.requires_transform(&KvBlockLayout::Unknown));
387
388 // Unknown→Unknown is compatible (but emits warning)
389 assert!(!KvBlockLayout::Unknown.requires_transform(&KvBlockLayout::Unknown));
390 }
391
392 #[test]
393 fn test_is_operational() {
394 assert!(KvBlockLayout::OperationalNHD.is_operational());
395 assert!(KvBlockLayout::OperationalHND.is_operational());
396 assert!(!KvBlockLayout::UniversalTP.is_operational());
397 assert!(!KvBlockLayout::Unknown.is_operational());
398 }
399
400 #[test]
401 fn test_is_universal() {
402 assert!(KvBlockLayout::UniversalTP.is_universal());
403 assert!(KvBlockLayout::UniversalPP.is_universal());
404 assert!(!KvBlockLayout::OperationalNHD.is_universal());
405 }
406
407 #[test]
408 fn test_default() {
409 assert_eq!(KvBlockLayout::default(), KvBlockLayout::Unknown);
410 }
411
412 #[test]
413 fn test_serialization() {
414 let layout = KvBlockLayout::UniversalTP;
415 let json = serde_json::to_string(&layout).unwrap();
416 let deserialized: KvBlockLayout = serde_json::from_str(&json).unwrap();
417 assert_eq!(layout, deserialized);
418
419 // Test custom layout
420 let custom = KvBlockLayout::Custom([
421 BlockDim::Head,
422 BlockDim::Page,
423 BlockDim::Layer,
424 BlockDim::Outer,
425 ]);
426 let json = serde_json::to_string(&custom).unwrap();
427 let deserialized: KvBlockLayout = serde_json::from_str(&json).unwrap();
428 assert_eq!(custom, deserialized);
429 }
430
431 #[test]
432 fn test_inner_shape_conversion() {
433 use super::super::InnerShape;
434
435 assert_eq!(
436 KvBlockLayout::from_inner_shape(InnerShape::NHD),
437 KvBlockLayout::OperationalNHD
438 );
439 assert_eq!(
440 KvBlockLayout::from_inner_shape(InnerShape::HND),
441 KvBlockLayout::OperationalHND
442 );
443
444 assert_eq!(
445 KvBlockLayout::OperationalNHD.to_inner_shape(),
446 Some(InnerShape::NHD)
447 );
448 assert_eq!(KvBlockLayout::UniversalTP.to_inner_shape(), None);
449 }
450}