hurray_core/layout/block_paged.rs
1//! Block-paged layout descriptor.
2//!
3//! Tag `0x0A`. Rank MUST be 3. Buffer count = 3 (page_pool + block_table + seq_ptr).
4//! See `docs/spec/layouts/block-paged.md`.
5
6/// KV-cache role for a block-paged tensor.
7///
8/// The wire value is a `uint8` in the additional descriptor fields of a
9/// block-paged layout (see `docs/spec/layouts/block-paged.md § Additional Descriptor Fields`).
10///
11/// # Examples
12///
13/// ```
14/// use hurray_core::layout::KvRole;
15///
16/// assert_eq!(KvRole::Key.wire_byte(), 0x00);
17/// assert_eq!(KvRole::Value.wire_byte(), 0x01);
18/// assert_eq!(KvRole::Fused.wire_byte(), 0x02);
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum KvRole {
23 /// Key tensor (`kv_role = 0x00`).
24 Key,
25 /// Value tensor (`kv_role = 0x01`).
26 Value,
27 /// Fused or non-KV generic tensor (`kv_role = 0x02`).
28 Fused,
29}
30
31impl KvRole {
32 /// Returns the wire byte for this role.
33 ///
34 /// # Examples
35 ///
36 /// ```
37 /// use hurray_core::layout::KvRole;
38 ///
39 /// assert_eq!(KvRole::Key.wire_byte(), 0x00);
40 /// assert_eq!(KvRole::Value.wire_byte(), 0x01);
41 /// assert_eq!(KvRole::Fused.wire_byte(), 0x02);
42 /// ```
43 #[inline]
44 pub fn wire_byte(self) -> u8 {
45 match self {
46 Self::Key => 0x00,
47 Self::Value => 0x01,
48 Self::Fused => 0x02,
49 }
50 }
51
52 /// Constructs a [`KvRole`] from its wire byte.
53 ///
54 /// Returns `None` for unrecognized values (future spec extensions).
55 ///
56 /// # Examples
57 ///
58 /// ```
59 /// use hurray_core::layout::KvRole;
60 ///
61 /// assert_eq!(KvRole::from_wire(0x00), Some(KvRole::Key));
62 /// assert_eq!(KvRole::from_wire(0x03), None);
63 /// ```
64 #[inline]
65 pub fn from_wire(byte: u8) -> Option<Self> {
66 match byte {
67 0x00 => Some(Self::Key),
68 0x01 => Some(Self::Value),
69 0x02 => Some(Self::Fused),
70 _ => None,
71 }
72 }
73}
74
75/// Element type for the `block_table` and `seq_ptr` index buffers.
76///
77/// Both buffers share the same index type, selected by `block_table_index_type`
78/// in the block-paged descriptor fields. `U32` addresses up to ~4 billion pages;
79/// `U64` is required for larger pools.
80///
81/// See `docs/spec/layouts/block-paged.md § Additional Descriptor Fields`.
82///
83/// # Examples
84///
85/// ```
86/// use hurray_core::layout::BlockTableIndexType;
87///
88/// assert_eq!(BlockTableIndexType::U32.wire_byte(), 0x00);
89/// assert_eq!(BlockTableIndexType::U64.wire_byte(), 0x01);
90/// ```
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum BlockTableIndexType {
94 /// 32-bit unsigned index (`block_table_index_type = 0x00`). Default.
95 U32,
96 /// 64-bit unsigned index (`block_table_index_type = 0x01`).
97 U64,
98}
99
100impl BlockTableIndexType {
101 /// Returns the wire byte for this index type.
102 ///
103 /// # Examples
104 ///
105 /// ```
106 /// use hurray_core::layout::BlockTableIndexType;
107 ///
108 /// assert_eq!(BlockTableIndexType::U32.wire_byte(), 0x00);
109 /// assert_eq!(BlockTableIndexType::U64.wire_byte(), 0x01);
110 /// ```
111 #[inline]
112 pub fn wire_byte(self) -> u8 {
113 match self {
114 Self::U32 => 0x00,
115 Self::U64 => 0x01,
116 }
117 }
118
119 /// Constructs a [`BlockTableIndexType`] from its wire byte.
120 ///
121 /// Returns `None` for unrecognized values (future spec extensions).
122 ///
123 /// # Examples
124 ///
125 /// ```
126 /// use hurray_core::layout::BlockTableIndexType;
127 ///
128 /// assert_eq!(BlockTableIndexType::from_wire(0x00), Some(BlockTableIndexType::U32));
129 /// assert_eq!(BlockTableIndexType::from_wire(0x02), None);
130 /// ```
131 #[inline]
132 pub fn from_wire(byte: u8) -> Option<Self> {
133 match byte {
134 0x00 => Some(Self::U32),
135 0x01 => Some(Self::U64),
136 _ => None,
137 }
138 }
139
140 /// Returns the size in bytes of a single index element for this type.
141 ///
142 /// # Examples
143 ///
144 /// ```
145 /// use hurray_core::layout::BlockTableIndexType;
146 ///
147 /// assert_eq!(BlockTableIndexType::U32.element_bytes(), 4);
148 /// assert_eq!(BlockTableIndexType::U64.element_bytes(), 8);
149 /// ```
150 #[inline]
151 pub fn element_bytes(self) -> usize {
152 match self {
153 Self::U32 => 4,
154 Self::U64 => 8,
155 }
156 }
157}
158
159/// The wire sentinel for `layer_index` meaning "not layer-scoped".
160///
161/// When `layer_index` is `0xFFFFFFFF` on the wire the field encodes
162/// [`None`] in Rust; any other value encodes `Some(n)`.
163pub(crate) const LAYER_INDEX_NONE: u32 = 0xFFFF_FFFF;
164
165/// Descriptor for the block-paged indirect layout.
166///
167/// Block-paged stores a KV-cache tensor whose **paged axis** is divided into
168/// fixed-size pages drawn from a shared **page pool**, with a **block table**
169/// mapping each logical page position to a physical page ID. It is the
170/// interchange form of a PagedAttention KV cache.
171///
172/// This layout is **defined only for rank-3 tensors** in this version of the
173/// specification: `[total_tokens, num_heads, head_dim]`.
174///
175/// Three buffers are always required:
176///
177/// | Buffer | Name | Description |
178/// |--------|------|-------------|
179/// | 0 | `page_pool` | Flat pool of fixed-size pages. |
180/// | 1 | `block_table` | Concatenated per-sequence page-ID lists. |
181/// | 2 | `seq_ptr` | CSR-style offset array into `block_table`. |
182///
183/// When the tensor carries quantization parameters, additional buffers appear
184/// at indices 3 and above per `docs/spec/quantization.md § Buffer Table Placement Rules`.
185///
186/// See `docs/spec/layouts/block-paged.md` for the full normative definition.
187///
188/// # Examples
189///
190/// ```
191/// use hurray_core::layout::{BlockPagedLayout, BlockTableIndexType, KvRole, LayoutDescriptor};
192///
193/// let layout = BlockPagedLayout::new(16, 128, 0, 4, KvRole::Key, Some(3), BlockTableIndexType::U32);
194/// assert_eq!(layout.page_size, 16);
195/// assert_eq!(layout.num_pages, 128);
196/// assert_eq!(layout.num_seqs, 4);
197/// assert_eq!(layout.layer_index, Some(3));
198///
199/// let desc = LayoutDescriptor::BlockPaged(layout);
200/// assert_eq!(desc.tag(), 0x0A);
201/// assert_eq!(desc.buffer_count().map(|n| n.get()), Some(3));
202/// ```
203#[derive(Debug, Clone, PartialEq, Eq, Hash)]
204#[non_exhaustive]
205pub struct BlockPagedLayout {
206 /// Tokens per page. MUST be >= 1.
207 ///
208 /// Common values are 16 and 32 (matching vLLM's default `block_size`).
209 pub page_size: u32,
210
211 /// Number of physical pages in `page_pool`.
212 pub num_pages: u64,
213
214 /// The axis subdivided into pages. MUST be 0 in this spec version.
215 ///
216 /// Kept as a field rather than a constant so that the wire format remains
217 /// forward-compatible if a future spec version adds non-zero paged axes.
218 pub paged_axis: u32,
219
220 /// Number of sequences in the batch. MAY be 0 for an empty batch.
221 pub num_seqs: u32,
222
223 /// KV-cache role for this tensor (key, value, or fused/generic).
224 pub kv_role: KvRole,
225
226 /// Transformer layer index, or `None` if the tensor is not layer-scoped.
227 ///
228 /// Wire value `0xFFFFFFFF` maps to `None`; all other values map to `Some(n)`.
229 pub layer_index: Option<u32>,
230
231 /// Element type for both the `block_table` (buffer 1) and `seq_ptr` (buffer 2).
232 ///
233 /// `U32` is the default and addresses up to 4 billion pages.
234 /// `U64` is required for larger pools.
235 pub block_table_index_type: BlockTableIndexType,
236}
237
238impl BlockPagedLayout {
239 /// Creates a new [`BlockPagedLayout`].
240 ///
241 /// # Arguments
242 ///
243 /// - `page_size` — tokens per page (MUST be validated >= 1 later via `validate_against_shape`).
244 /// - `num_pages` — number of physical pages in the pool.
245 /// - `paged_axis` — the axis subdivided into pages (MUST be 0 in this version).
246 /// - `num_seqs` — number of sequences in the batch.
247 /// - `kv_role` — KV-cache role for this tensor.
248 /// - `layer_index` — transformer layer index (`None` = not layer-scoped).
249 /// - `block_table_index_type` — element type for `block_table` and `seq_ptr` buffers.
250 ///
251 /// This constructor does not validate `page_size >= 1` or `paged_axis == 0` because
252 /// the tensor shape is not available at construction time. Call
253 /// [`LayoutDescriptor::validate_against_shape`] to perform all invariant checks
254 /// once the shape is known.
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use hurray_core::layout::{BlockPagedLayout, BlockTableIndexType, KvRole};
260 ///
261 /// // Layer-3 key cache: page_size=16, 64 pages, 2 sequences, uint32 indices.
262 /// let layout = BlockPagedLayout::new(
263 /// 16, 64, 0, 2,
264 /// KvRole::Key,
265 /// Some(3),
266 /// BlockTableIndexType::U32,
267 /// );
268 /// assert_eq!(layout.page_size, 16);
269 /// assert_eq!(layout.layer_index, Some(3));
270 /// ```
271 #[allow(clippy::too_many_arguments)]
272 pub fn new(
273 page_size: u32,
274 num_pages: u64,
275 paged_axis: u32,
276 num_seqs: u32,
277 kv_role: KvRole,
278 layer_index: Option<u32>,
279 block_table_index_type: BlockTableIndexType,
280 ) -> Self {
281 Self {
282 page_size,
283 num_pages,
284 paged_axis,
285 num_seqs,
286 kv_role,
287 layer_index,
288 block_table_index_type,
289 }
290 }
291
292 /// Validates that a quantization scheme is compatible with this block-paged layout.
293 ///
294 /// Per `docs/spec/layouts/block-paged.md § Quantization Compatibility`:
295 ///
296 /// - **Per-tensor** (`scheme_tag = 0x01`): always compatible.
297 /// - **Per-channel** (`scheme_tag = 0x02`): MUST NOT be applied on axis 0 (the
298 /// paged / token axis). `quant_axis` must be 1 (`num_heads`) or 2 (`head_dim`).
299 /// - **Per-block-affine** (`scheme_tag = 0x03`): `quant_axis` MUST be 0 and
300 /// `quant_block_size` MUST equal `page_size`.
301 /// - All other schemes: not validated here; returns `Ok(())`.
302 ///
303 /// Call this at the typed-quantization layer once you have a decoded
304 /// [`crate::QuantizationDescriptor`]. The raw-bytes descriptor layer cannot
305 /// perform this check.
306 ///
307 /// # Arguments
308 ///
309 /// - `scheme_tag` — the `scheme_tag` byte from the quantization descriptor header.
310 /// - `quant_axis` — the `axis` field from the quantization descriptor (relevant for
311 /// per-channel and per-block-affine).
312 /// - `quant_block_size` — the `block_size` field from the per-block-affine descriptor
313 /// (ignored for all other schemes).
314 ///
315 /// # Errors
316 ///
317 /// Returns [`crate::Error::InvalidQuantization`] if the scheme/layout combination
318 /// violates the spec rules above.
319 ///
320 /// # Examples
321 ///
322 /// ```
323 /// use hurray_core::layout::{BlockPagedLayout, BlockTableIndexType, KvRole};
324 ///
325 /// let layout = BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
326 ///
327 /// // Per-tensor (0x01) is always valid.
328 /// assert!(layout.validate_quantization_compatibility(0x01, 0, 0).is_ok());
329 ///
330 /// // Per-channel on axis 1 (num_heads) is valid.
331 /// assert!(layout.validate_quantization_compatibility(0x02, 1, 0).is_ok());
332 ///
333 /// // Per-channel on axis 0 (paged axis) is forbidden.
334 /// assert!(layout.validate_quantization_compatibility(0x02, 0, 0).is_err());
335 ///
336 /// // Per-block-affine with axis=0 and block_size==page_size is valid.
337 /// assert!(layout.validate_quantization_compatibility(0x03, 0, 16).is_ok());
338 ///
339 /// // Per-block-affine with block_size != page_size is forbidden.
340 /// assert!(layout.validate_quantization_compatibility(0x03, 0, 32).is_err());
341 /// ```
342 pub fn validate_quantization_compatibility(
343 &self,
344 scheme_tag: u8,
345 quant_axis: u32,
346 quant_block_size: u32,
347 ) -> crate::Result<()> {
348 const SCHEME_PER_CHANNEL: u8 = 0x02;
349 const SCHEME_PER_BLOCK_AFFINE: u8 = 0x03;
350
351 match scheme_tag {
352 SCHEME_PER_CHANNEL => {
353 // Per-channel MUST NOT be applied to the paged axis (axis 0).
354 // axis 1 (num_heads) and axis 2 (head_dim) are permitted.
355 if quant_axis == 0 {
356 return Err(crate::Error::InvalidQuantization(
357 "block-paged: per-channel quantization (scheme 0x02) MUST NOT be applied \
358 to the paged axis (axis 0); use axis 1 (num_heads) or axis 2 (head_dim)"
359 .to_string(),
360 ));
361 }
362 }
363 SCHEME_PER_BLOCK_AFFINE => {
364 // axis MUST be 0 (the paged / token axis).
365 if quant_axis != 0 {
366 return Err(crate::Error::InvalidQuantization(format!(
367 "block-paged: per-block-affine quantization (scheme 0x03) requires \
368 axis = 0, got axis = {quant_axis}"
369 )));
370 }
371 // block_size MUST equal page_size so scales align with page slots.
372 if quant_block_size != self.page_size {
373 return Err(crate::Error::InvalidQuantization(format!(
374 "block-paged: per-block-affine quantization (scheme 0x03) requires \
375 block_size == page_size ({}), got block_size = {}",
376 self.page_size, quant_block_size
377 )));
378 }
379 }
380 // Per-tensor (0x01) and all other schemes compose without paged-layout constraints.
381 _ => {}
382 }
383 Ok(())
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::layout::LayoutDescriptor;
391 use std::num::NonZeroU8;
392
393 // ── KvRole wire mapping ───────────────────────────────────────────────────
394
395 /// Spec §block-paged.md §Additional Descriptor Fields: kv_role wire bytes.
396 #[test]
397 fn kv_role_wire_byte_key_is_0x00() {
398 assert_eq!(KvRole::Key.wire_byte(), 0x00);
399 }
400
401 #[test]
402 fn kv_role_wire_byte_value_is_0x01() {
403 assert_eq!(KvRole::Value.wire_byte(), 0x01);
404 }
405
406 #[test]
407 fn kv_role_wire_byte_fused_is_0x02() {
408 assert_eq!(KvRole::Fused.wire_byte(), 0x02);
409 }
410
411 #[test]
412 fn kv_role_from_wire_round_trips_all_variants() {
413 for variant in [KvRole::Key, KvRole::Value, KvRole::Fused] {
414 let byte = variant.wire_byte();
415 let decoded = KvRole::from_wire(byte);
416 assert_eq!(
417 decoded,
418 Some(variant),
419 "KvRole::from_wire(0x{byte:02X}) should return {variant:?}"
420 );
421 }
422 }
423
424 #[test]
425 fn kv_role_from_wire_rejects_0x03() {
426 assert_eq!(KvRole::from_wire(0x03), None);
427 }
428
429 #[test]
430 fn kv_role_from_wire_rejects_0xff() {
431 assert_eq!(KvRole::from_wire(0xFF), None);
432 }
433
434 // ── BlockTableIndexType wire mapping ──────────────────────────────────────
435
436 /// Spec §block-paged.md §Additional Descriptor Fields: block_table_index_type wire bytes.
437 #[test]
438 fn block_table_index_type_wire_byte_u32_is_0x00() {
439 assert_eq!(BlockTableIndexType::U32.wire_byte(), 0x00);
440 }
441
442 #[test]
443 fn block_table_index_type_wire_byte_u64_is_0x01() {
444 assert_eq!(BlockTableIndexType::U64.wire_byte(), 0x01);
445 }
446
447 #[test]
448 fn block_table_index_type_from_wire_round_trips_all_variants() {
449 for variant in [BlockTableIndexType::U32, BlockTableIndexType::U64] {
450 let byte = variant.wire_byte();
451 let decoded = BlockTableIndexType::from_wire(byte);
452 assert_eq!(
453 decoded,
454 Some(variant),
455 "BlockTableIndexType::from_wire(0x{byte:02X}) should return {variant:?}"
456 );
457 }
458 }
459
460 #[test]
461 fn block_table_index_type_from_wire_rejects_0x02() {
462 assert_eq!(BlockTableIndexType::from_wire(0x02), None);
463 }
464
465 #[test]
466 fn block_table_index_type_from_wire_rejects_0xff() {
467 assert_eq!(BlockTableIndexType::from_wire(0xFF), None);
468 }
469
470 #[test]
471 fn block_table_index_type_element_bytes_u32_is_4() {
472 assert_eq!(BlockTableIndexType::U32.element_bytes(), 4);
473 }
474
475 #[test]
476 fn block_table_index_type_element_bytes_u64_is_8() {
477 assert_eq!(BlockTableIndexType::U64.element_bytes(), 8);
478 }
479
480 // ── layer_index sentinel ─────────────────────────────────────────────────
481
482 /// Spec §block-paged.md §Additional Descriptor Fields:
483 /// wire value 0xFFFFFFFF encodes None; any other value encodes Some(n).
484 #[test]
485 fn layer_index_none_sentinel_value_is_0xffffffff() {
486 assert_eq!(LAYER_INDEX_NONE, 0xFFFF_FFFF);
487 }
488
489 #[test]
490 fn layer_index_some_zero_is_distinct_from_none() {
491 // Some(0) must differ from None (0xFFFFFFFF sentinel).
492 let layout_none =
493 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, None, BlockTableIndexType::U32);
494 let layout_some_zero =
495 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
496 assert_ne!(layout_none, layout_some_zero);
497 assert_eq!(layout_none.layer_index, None);
498 assert_eq!(layout_some_zero.layer_index, Some(0));
499 }
500
501 #[test]
502 fn layer_index_some_large_value_stored_correctly() {
503 let n = 0xFFFF_FFFE_u32; // one below sentinel
504 let layout =
505 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(n), BlockTableIndexType::U32);
506 assert_eq!(layout.layer_index, Some(n));
507 }
508
509 // ── BlockPagedLayout tag and buffer count ─────────────────────────────────
510
511 /// Spec §block-paged.md: layout tag MUST be 0x0A.
512 #[test]
513 fn block_paged_layout_tag_is_0x0a() {
514 let layout =
515 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
516 let desc = LayoutDescriptor::BlockPaged(layout);
517 assert_eq!(desc.tag(), 0x0A);
518 }
519
520 /// Spec §block-paged.md §Buffer Table: buffer_count MUST be 3.
521 #[test]
522 fn block_paged_buffer_count_is_3() {
523 let layout =
524 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
525 let desc = LayoutDescriptor::BlockPaged(layout);
526 assert_eq!(desc.buffer_count(), NonZeroU8::new(3));
527 }
528
529 // ── Quantization compatibility ────────────────────────────────────────────
530
531 /// Spec §block-paged.md §Quantization Compatibility: per-tensor (0x01) is always valid.
532 #[test]
533 fn quant_compat_per_tensor_always_accepted() {
534 let layout =
535 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
536 // axis and block_size are irrelevant for per-tensor; any values pass.
537 assert!(layout
538 .validate_quantization_compatibility(0x01, 0, 0)
539 .is_ok());
540 assert!(layout
541 .validate_quantization_compatibility(0x01, 99, 999)
542 .is_ok());
543 }
544
545 /// Spec §block-paged.md §Quantization Compatibility: per-channel on axis 0 (paged axis) is forbidden.
546 #[test]
547 fn quant_compat_per_channel_on_axis_0_rejected() {
548 let layout =
549 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
550 let result = layout.validate_quantization_compatibility(0x02, 0, 0);
551 assert!(result.is_err(), "per-channel on axis 0 must be rejected");
552 assert!(matches!(result, Err(crate::Error::InvalidQuantization(_))));
553 }
554
555 /// Spec §block-paged.md §Quantization Compatibility: per-channel on axis 1 (num_heads) is valid.
556 #[test]
557 fn quant_compat_per_channel_on_axis_1_accepted() {
558 let layout =
559 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
560 assert!(layout
561 .validate_quantization_compatibility(0x02, 1, 0)
562 .is_ok());
563 }
564
565 /// Spec §block-paged.md §Quantization Compatibility: per-channel on axis 2 (head_dim) is valid.
566 #[test]
567 fn quant_compat_per_channel_on_axis_2_accepted() {
568 let layout =
569 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
570 assert!(layout
571 .validate_quantization_compatibility(0x02, 2, 0)
572 .is_ok());
573 }
574
575 /// Spec §block-paged.md §Quantization Compatibility: per-block-affine with axis=0 and
576 /// block_size == page_size is valid.
577 #[test]
578 fn quant_compat_per_block_affine_axis_0_block_size_eq_page_size_accepted() {
579 let layout =
580 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
581 assert!(layout
582 .validate_quantization_compatibility(0x03, 0, 16)
583 .is_ok());
584 }
585
586 /// Spec §block-paged.md §Quantization Compatibility: per-block-affine with axis != 0 is rejected.
587 #[test]
588 fn quant_compat_per_block_affine_axis_nonzero_rejected() {
589 let layout =
590 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
591 let result = layout.validate_quantization_compatibility(0x03, 1, 16);
592 assert!(
593 result.is_err(),
594 "per-block-affine with axis=1 must be rejected"
595 );
596 assert!(matches!(result, Err(crate::Error::InvalidQuantization(_))));
597 }
598
599 /// Spec §block-paged.md §Quantization Compatibility: per-block-affine with
600 /// block_size != page_size is rejected.
601 #[test]
602 fn quant_compat_per_block_affine_block_size_mismatch_rejected() {
603 let layout =
604 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
605 let result = layout.validate_quantization_compatibility(0x03, 0, 32);
606 assert!(
607 result.is_err(),
608 "per-block-affine with block_size=32 != page_size=16 must be rejected"
609 );
610 assert!(matches!(result, Err(crate::Error::InvalidQuantization(_))));
611 }
612
613 /// Unknown quantization scheme tag (>= 0x04) passes without error.
614 #[test]
615 fn quant_compat_unknown_scheme_tag_passes() {
616 let layout =
617 BlockPagedLayout::new(16, 64, 0, 2, KvRole::Key, Some(0), BlockTableIndexType::U32);
618 assert!(layout
619 .validate_quantization_compatibility(0x04, 0, 0)
620 .is_ok());
621 assert!(layout
622 .validate_quantization_compatibility(0xFF, 0, 0)
623 .is_ok());
624 }
625}