#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SeqId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeqIdOverflow;
impl std::fmt::Display for SeqIdOverflow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SeqId overflow: u32::MAX is reserved (cfa-finding-F7)")
}
}
impl std::error::Error for SeqIdOverflow {}
impl SeqId {
pub fn new(v: u32) -> Result<Self, SeqIdOverflow> {
if v == u32::MAX {
Err(SeqIdOverflow)
} else {
Ok(SeqId(v))
}
}
pub const RESERVED: u32 = u32::MAX;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SlotId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MultiSeqLayout {
SeparateSlots,
Paged,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MultiSeqError {
SlotOutOfRange {
slot: SlotId,
max_slots: u32,
},
SlotOom {
slot: SlotId,
needed_bytes: u64,
budget_bytes: u64,
},
LayoutNotSupported {
layout: MultiSeqLayout,
},
CapabilityUnsupported {
capability: &'static str,
},
}
impl std::fmt::Display for MultiSeqError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SlotOutOfRange { slot, max_slots } => write!(
f,
"multi-seq KV cache slot {:?} out of range; valid slots are 0..{}",
slot.0, max_slots
),
Self::SlotOom {
slot,
needed_bytes,
budget_bytes,
} => write!(
f,
"multi-seq KV cache slot {:?} out of memory: append needed \
{needed_bytes} bytes; per-slot budget {budget_bytes} bytes \
(ADR-040 §3.5 — map to 429 + Retry-After upstream)",
slot.0
),
Self::LayoutNotSupported { layout } => write!(
f,
"multi-seq KV cache layout {layout:?} is not supported in this build \
(Phase A iter-1 ships SeparateSlots only; Paged is reserved for a \
future PagedAttention ADR per ADR-040 §3.1)"
),
Self::CapabilityUnsupported { capability } => write!(
f,
"capability not yet implemented in this impl: {capability} (HTTP 501)"
),
}
}
}
impl std::error::Error for MultiSeqError {}
pub trait MultiSeqKvCache {
fn layout(&self) -> MultiSeqLayout;
fn slot_count(&self) -> u32;
fn seq_len(&self, slot: SlotId) -> Result<u32, MultiSeqError>;
fn append_for_seq(&mut self, slot: SlotId, n_tokens: u32) -> Result<(), MultiSeqError>;
fn drop_seq(&mut self, slot: SlotId) -> Result<(), MultiSeqError>;
fn fork_seq(&mut self, src: SlotId, dst: SlotId) -> Result<(), MultiSeqError>;
}
#[derive(Debug, Clone)]
pub struct NoopMultiSeqKvCache {
layout: MultiSeqLayout,
slot_lens: Vec<u32>,
}
impl NoopMultiSeqKvCache {
pub fn new(slot_count: u32, layout: MultiSeqLayout) -> Self {
Self {
layout,
slot_lens: vec![0u32; slot_count as usize],
}
}
fn check_slot(&self, slot: SlotId) -> Result<(), MultiSeqError> {
let max = self.slot_lens.len() as u32;
if slot.0 >= max {
Err(MultiSeqError::SlotOutOfRange {
slot,
max_slots: max,
})
} else {
Ok(())
}
}
fn check_layout_supported(&self) -> Result<(), MultiSeqError> {
match self.layout {
MultiSeqLayout::SeparateSlots => Ok(()),
MultiSeqLayout::Paged => Err(MultiSeqError::LayoutNotSupported {
layout: MultiSeqLayout::Paged,
}),
}
}
}
impl MultiSeqKvCache for NoopMultiSeqKvCache {
fn layout(&self) -> MultiSeqLayout {
self.layout
}
fn slot_count(&self) -> u32 {
self.slot_lens.len() as u32
}
fn seq_len(&self, slot: SlotId) -> Result<u32, MultiSeqError> {
self.check_slot(slot)?;
Ok(self.slot_lens[slot.0 as usize])
}
fn append_for_seq(&mut self, slot: SlotId, n_tokens: u32) -> Result<(), MultiSeqError> {
self.check_slot(slot)?;
self.check_layout_supported()?;
let entry = &mut self.slot_lens[slot.0 as usize];
*entry = entry.saturating_add(n_tokens);
Ok(())
}
fn drop_seq(&mut self, slot: SlotId) -> Result<(), MultiSeqError> {
self.check_slot(slot)?;
self.check_layout_supported()?;
self.slot_lens[slot.0 as usize] = 0;
Ok(())
}
fn fork_seq(&mut self, src: SlotId, dst: SlotId) -> Result<(), MultiSeqError> {
self.check_slot(src)?;
self.check_slot(dst)?;
self.check_layout_supported()?;
let src_len = self.slot_lens[src.0 as usize];
self.slot_lens[dst.0 as usize] = src_len;
Ok(())
}
}
#[allow(dead_code)]
const _ID_NEWTYPES_ARE_DISTINCT: () = ();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn noop_cache_starts_with_zero_seq_len_per_slot() {
let cache = NoopMultiSeqKvCache::new(4, MultiSeqLayout::SeparateSlots);
assert_eq!(cache.slot_count(), 4);
assert_eq!(cache.layout(), MultiSeqLayout::SeparateSlots);
for i in 0..4 {
assert_eq!(cache.seq_len(SlotId(i)).unwrap(), 0, "slot {i} not zero");
}
}
#[test]
fn noop_cache_append_advances_seq_len() {
let mut cache = NoopMultiSeqKvCache::new(4, MultiSeqLayout::SeparateSlots);
cache.append_for_seq(SlotId(1), 3).unwrap();
assert_eq!(cache.seq_len(SlotId(1)).unwrap(), 3);
assert_eq!(cache.seq_len(SlotId(0)).unwrap(), 0);
assert_eq!(cache.seq_len(SlotId(2)).unwrap(), 0);
assert_eq!(cache.seq_len(SlotId(3)).unwrap(), 0);
cache.append_for_seq(SlotId(1), 5).unwrap();
assert_eq!(cache.seq_len(SlotId(1)).unwrap(), 8);
}
#[test]
fn noop_cache_slot_out_of_range_errors_named() {
let mut cache = NoopMultiSeqKvCache::new(2, MultiSeqLayout::SeparateSlots);
let err = cache.append_for_seq(SlotId(2), 1).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(2),
max_slots: 2
},
"append OOR must populate both fields; got {err:?}"
);
let err = cache.seq_len(SlotId(7)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(7),
max_slots: 2
},
"seq_len OOR must populate both fields; got {err:?}"
);
let err = cache.drop_seq(SlotId(42)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(42),
max_slots: 2
},
"drop OOR must populate both fields; got {err:?}"
);
}
#[test]
fn noop_cache_drop_resets_seq_len_to_zero() {
let mut cache = NoopMultiSeqKvCache::new(2, MultiSeqLayout::SeparateSlots);
cache.append_for_seq(SlotId(0), 11).unwrap();
assert_eq!(cache.seq_len(SlotId(0)).unwrap(), 11);
cache.drop_seq(SlotId(0)).unwrap();
assert_eq!(cache.seq_len(SlotId(0)).unwrap(), 0);
cache.drop_seq(SlotId(0)).unwrap();
assert_eq!(cache.seq_len(SlotId(0)).unwrap(), 0);
}
#[test]
fn noop_cache_fork_copies_seq_len_from_src_to_dst() {
let mut cache = NoopMultiSeqKvCache::new(3, MultiSeqLayout::SeparateSlots);
cache.append_for_seq(SlotId(0), 5).unwrap();
cache.fork_seq(SlotId(0), SlotId(2)).unwrap();
assert_eq!(cache.seq_len(SlotId(2)).unwrap(), 5, "dst must match src");
assert_eq!(
cache.seq_len(SlotId(0)).unwrap(),
5,
"src must be unchanged"
);
assert_eq!(cache.seq_len(SlotId(1)).unwrap(), 0);
}
#[test]
fn noop_cache_fork_src_oob_errors() {
let mut cache = NoopMultiSeqKvCache::new(2, MultiSeqLayout::SeparateSlots);
let err = cache.fork_seq(SlotId(5), SlotId(0)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(5),
max_slots: 2
},
"fork src OOR must surface src ID first; got {err:?}"
);
}
#[test]
fn noop_cache_fork_dst_oob_errors() {
let mut cache = NoopMultiSeqKvCache::new(2, MultiSeqLayout::SeparateSlots);
let err = cache.fork_seq(SlotId(0), SlotId(9)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(9),
max_slots: 2
},
"fork dst OOR must surface dst ID; got {err:?}"
);
}
#[test]
fn noop_cache_paged_in_bounds_returns_layout_not_supported() {
let mut cache = NoopMultiSeqKvCache::new(4, MultiSeqLayout::Paged);
assert_eq!(cache.slot_count(), 4);
assert_eq!(cache.layout(), MultiSeqLayout::Paged);
let err = cache.append_for_seq(SlotId(0), 1).unwrap_err();
assert_eq!(
err,
MultiSeqError::LayoutNotSupported {
layout: MultiSeqLayout::Paged
},
"Paged in-bounds append must trip LayoutNotSupported; got {err:?}"
);
let err = cache.drop_seq(SlotId(0)).unwrap_err();
assert_eq!(
err,
MultiSeqError::LayoutNotSupported {
layout: MultiSeqLayout::Paged
},
"Paged in-bounds drop must trip LayoutNotSupported; got {err:?}"
);
let err = cache.fork_seq(SlotId(0), SlotId(1)).unwrap_err();
assert_eq!(
err,
MultiSeqError::LayoutNotSupported {
layout: MultiSeqLayout::Paged
},
"Paged in-bounds fork must trip LayoutNotSupported; got {err:?}"
);
}
#[test]
fn noop_cache_paged_out_of_bounds_returns_slot_out_of_range() {
let mut cache = NoopMultiSeqKvCache::new(2, MultiSeqLayout::Paged);
let err = cache.append_for_seq(SlotId(7), 1).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(7),
max_slots: 2
},
"Paged + OOR append must trip SlotOutOfRange (bounds-first); got {err:?}"
);
let err = cache.drop_seq(SlotId(7)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(7),
max_slots: 2
},
"Paged + OOR drop must trip SlotOutOfRange (bounds-first); got {err:?}"
);
let err = cache.fork_seq(SlotId(7), SlotId(0)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(7),
max_slots: 2
},
"Paged + fork OOR src must trip SlotOutOfRange for src (bounds-first); got {err:?}"
);
let err = cache.fork_seq(SlotId(0), SlotId(9)).unwrap_err();
assert_eq!(
err,
MultiSeqError::SlotOutOfRange {
slot: SlotId(9),
max_slots: 2
},
"Paged + fork OOR dst must trip SlotOutOfRange for dst (bounds-first); got {err:?}"
);
}
#[test]
fn seq_id_and_slot_id_are_distinct_types() {
let s = SlotId(7);
let q = SeqId::new(7).unwrap();
assert_eq!(s.0, 7);
assert_eq!(q.0, 7);
}
#[test]
fn seq_id_new_rejects_u32_max() {
assert!(matches!(SeqId::new(u32::MAX), Err(SeqIdOverflow)));
}
#[test]
fn seq_id_new_accepts_zero_and_max_minus_one() {
let zero = SeqId::new(0).expect("0 must be a valid SeqId");
assert_eq!(zero.0, 0);
let last = SeqId::new(u32::MAX - 1).expect("u32::MAX - 1 must be a valid SeqId");
assert_eq!(last.0, u32::MAX - 1);
}
#[test]
fn seq_id_reserved_constant_is_u32_max() {
assert_eq!(SeqId::RESERVED, u32::MAX);
}
#[test]
fn seq_id_overflow_display_mentions_cfa_finding() {
let s = format!("{}", SeqIdOverflow);
assert!(
s.contains("u32::MAX"),
"Display must name the reserved value: {s}"
);
assert!(
s.contains("cfa-finding-F7"),
"Display must reference the iter-1.5 finding: {s}"
);
}
#[test]
fn multi_seq_error_display_names_fields() {
let e = MultiSeqError::SlotOutOfRange {
slot: SlotId(11),
max_slots: 4,
};
let d = format!("{e:?}");
assert!(d.contains("SlotOutOfRange"), "Debug missing variant: {d}");
assert!(d.contains("11"), "Debug missing slot id: {d}");
assert!(d.contains("4"), "Debug missing max_slots: {d}");
let s = format!("{e}");
assert!(s.contains("11"), "Display missing slot id: {s}");
assert!(s.contains("0..4"), "Display missing valid range: {s}");
let e = MultiSeqError::SlotOom {
slot: SlotId(2),
needed_bytes: 1024,
budget_bytes: 256,
};
let d = format!("{e:?}");
assert!(d.contains("SlotOom"), "Debug missing variant: {d}");
assert!(d.contains('2'), "Debug missing slot id: {d}");
assert!(d.contains("1024"), "Debug missing needed: {d}");
assert!(d.contains("256"), "Debug missing budget: {d}");
let s = format!("{e}");
assert!(s.contains("1024"), "Display missing needed: {s}");
assert!(s.contains("256"), "Display missing budget: {s}");
assert!(
s.contains("429"),
"Display must reference the 429 mapping: {s}"
);
let e = MultiSeqError::LayoutNotSupported {
layout: MultiSeqLayout::Paged,
};
let d = format!("{e:?}");
assert!(
d.contains("LayoutNotSupported"),
"Debug missing variant: {d}"
);
assert!(d.contains("Paged"), "Debug missing layout: {d}");
let s = format!("{e}");
assert!(s.contains("Paged"), "Display missing layout: {s}");
assert!(
s.contains("PagedAttention") || s.contains("future"),
"Display must point to the future ADR: {s}"
);
}
#[test]
fn multi_seq_error_capability_unsupported_display_names_capability() {
let e = MultiSeqError::CapabilityUnsupported {
capability: "fork_seq cross-slot copy (Qwen35 HybridKvCache)",
};
let s = format!("{e}");
assert!(
s.contains("fork_seq cross-slot copy"),
"Display must carry the capability label verbatim: {s}"
);
assert!(
s.contains("HTTP 501"),
"Display must name the HTTP 501 upstream mapping (iter-2.5 M1): {s}"
);
let d = format!("{e:?}");
assert!(
d.contains("CapabilityUnsupported"),
"Debug missing variant: {d}"
);
assert!(
d.contains("fork_seq cross-slot copy"),
"Debug missing capability label: {d}"
);
}
#[test]
fn multi_seq_error_capability_unsupported_distinct_from_slot_oom() {
let cap = MultiSeqError::CapabilityUnsupported {
capability: "anything",
};
let legacy_sentinel = MultiSeqError::SlotOom {
slot: SlotId(0),
needed_bytes: 0,
budget_bytes: 0,
};
let real_oom = MultiSeqError::SlotOom {
slot: SlotId(3),
needed_bytes: 1024,
budget_bytes: 256,
};
assert_ne!(
cap, legacy_sentinel,
"CapabilityUnsupported must be discriminant-distinct from \
the legacy SlotOom {{ 0, 0 }} sentinel iter-2a used \
(iter-2.5 M1 closes the mantra violation)"
);
assert_ne!(
cap, real_oom,
"CapabilityUnsupported must be discriminant-distinct from \
a real SlotOom — 501 vs 429 upstream"
);
let cap_same = MultiSeqError::CapabilityUnsupported {
capability: "anything",
};
assert_eq!(cap, cap_same, "Eq round-trips for identical labels");
}
#[test]
fn noop_cache_usable_as_trait_object() {
let mut cache: Box<dyn MultiSeqKvCache> =
Box::new(NoopMultiSeqKvCache::new(2, MultiSeqLayout::SeparateSlots));
assert_eq!(cache.slot_count(), 2);
cache.append_for_seq(SlotId(0), 4).unwrap();
assert_eq!(cache.seq_len(SlotId(0)).unwrap(), 4);
cache.drop_seq(SlotId(0)).unwrap();
assert_eq!(cache.seq_len(SlotId(0)).unwrap(), 0);
}
}