Skip to main content

fuel_tensor/
kv_block_pool_device.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Device-backed KV block pool — the thin integration layer that binds the pure
3//! [`KvBlockPool`] host-side allocator to real device `Storage` (multi-session
4//! serving, Increment 2, Part 2).
5//!
6//! The pure core ([`fuel_kv_pool`]) owns block *metadata* — free list,
7//! refcounts, per-session block tables — and deliberately touches no device, no
8//! tensors, no model (which is what keeps its eventual `fuel-inference` move
9//! cheap). This module is the counterpart that owns the *bytes*:
10//!
11//! - **Real pool buffers.** `Op::PagedAttn` reads physical K/V caches shaped
12//!   `[num_blocks, block_size, Hkv, D]`. A pool holds `n_layers × 2` of them (a
13//!   physical block is the same slot in *every* layer's K and V buffer — the
14//!   vLLM shared-block-table model), allocated once via the same
15//!   `Op::Alloc → Op::ZeroFill → realize_many` path [`crate::inference_context::
16//!   KvCache::with_capacity`] uses, so the executor's device-handle reuse and
17//!   destructive-fill cleanup come for free.
18//! - **`block_table` / `context_lens` materialization.** `Op::PagedAttn`
19//!   consumes a `[B, max_blocks]` u32 page table + a `[B]` u32 length vector.
20//!   [`DeviceKvPool::materialize_block_table`] projects the core's per-session
21//!   block tables into exactly that host layout, ready to upload as `Op::Const`
22//!   operands.
23//!
24//! Byte movement into blocks (`Op::WriteSlice` at a physical block offset) and
25//! evict/restore's device↔host copy land in the following sub-increments; this
26//! one delivers the buffers + the page-table projection and gates them on a
27//! CPU-realizable structural test.
28
29use std::sync::{Arc, RwLock};
30
31use fuel_dispatch::pipelined::{PipelinedExecutor, StorageCache};
32use fuel_graph::{Graph, Node, NodeId, Op};
33use fuel_ir::{DType, Error, Layout, Result, Shape};
34use fuel_memory::Storage;
35
36use crate::Device;
37use crate::decode_shape::KvAllocId;
38use crate::lazy::Tensor;
39use fuel_dispatch::residency::ResidencyReport;
40use fuel_kv_pool::{
41    Externalized, KvAllocError, KvBlockPool, KvBudget, KvGeometry, PhysBlockId, SessionHandle,
42    non_kv_resident_bytes,
43};
44
45/// Size a pool for a device from a residency analysis of the decode graph
46/// (ROADMAP item 17, row H19).
47///
48/// `report` is `ResidencyPlanner::analyze` over the decode graph built with a
49/// small **probe** pool of geometry `probe` as its leaves; the probe pool's
50/// bytes are removed from the peak so only weights, activations and workspace
51/// count as non-KV residency. `template` supplies the real pool's shape (its
52/// `num_blocks` is ignored); `device_bytes` is what the backend reports for the
53/// device and `utilization` the consumer's entitlement in `(0, 1]`.
54///
55/// Returns `template` with the largest `num_blocks` that fits, or an `Err`
56/// naming why none does. The analysis counts node OUTPUT bytes only, so
57/// backend-internal workspace is not included (ROADMAP item 17, H8): keep
58/// `utilization` below 1 to leave room for it.
59pub fn fit_geometry_from_residency(
60    template: KvGeometry,
61    report: &ResidencyReport,
62    probe: &KvGeometry,
63    device_bytes: u64,
64    utilization: f64,
65) -> Result<KvGeometry> {
66    let budget = KvBudget {
67        device_bytes,
68        utilization,
69        non_kv_resident_bytes: non_kv_resident_bytes(report.peak_bytes as u64, probe),
70    };
71    template
72        .fit_to_budget(&budget)
73        .map_err(|e| msg_err(e.to_string()))
74}
75
76/// Which of a layer's two pool buffers a block operation targets.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub enum BlockKind {
79    K,
80    V,
81}
82
83fn msg_err(s: String) -> crate::Error {
84    fuel_ir::Error::Msg(s).bt()
85}
86
87fn alloc_err(e: KvAllocError) -> crate::Error {
88    msg_err(format!("KV allocator: {e:?}"))
89}
90
91/// The device-backed externalization of a session's evicted state (C-3, lossy).
92/// Wraps the pure core's structural [`Externalized`] handle with the **bytes**
93/// of the detached blocks — one `[block_size · Hkv · D]` f32 vector per
94/// externalized logical slot, per layer, for K and V. The core is byte-free by
95/// design (move-ready), so the device layer is where the block contents live
96/// while a session is suspended. Never carries a recompute instruction (Q9): it
97/// is externalized *state*, restorable as-is into fresh physical blocks.
98pub struct DeviceEvicted {
99    core_handle: Externalized,
100    saved: Vec<SavedBlock>,
101}
102
103/// The captured contents of one detached (exclusive) logical block across all
104/// layers. `k[l]` / `v[l]` are layer `l`'s `[block_size, Hkv, D]` slab as raw
105/// pool-dtype bytes (dtype-agnostic — f32 or bf16, via the byte movement core).
106struct SavedBlock {
107    logical_slot: usize,
108    k: Vec<Vec<u8>>,
109    v: Vec<Vec<u8>>,
110}
111
112impl DeviceEvicted {
113    /// The categories of state this handle covers (delegates the core handle's
114    /// enumeration — the Q9 completeness gate).
115    pub fn covers(&self) -> &[fuel_kv_pool::StateKind] {
116        self.core_handle.covers()
117    }
118    /// This handle's fidelity guarantee (`Lossy` this increment).
119    pub fn fidelity(&self) -> fuel_kv_pool::Fidelity {
120        self.core_handle.fidelity()
121    }
122    /// Number of blocks whose bytes this handle carries (the exclusive,
123    /// actually-detached blocks — shared blocks stay resident and aren't here).
124    pub fn saved_block_count(&self) -> usize {
125        self.saved.len()
126    }
127}
128
129/// A materialized `Op::PagedAttn` page table — the host-side `block_table` +
130/// `context_lens` for a batch of `B` sessions, ready to upload as `Op::Const`
131/// operands. `block_table` is row-major `[B, max_blocks]`; `context_lens` is
132/// `[B]`. Both are `u32` (the op's declared operand dtype).
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct PageTableHost {
135    /// Row-major `[B, max_blocks]` logical→physical map. Rows shorter than
136    /// `max_blocks` are **zero-padded**: index 0 is always an in-bounds physical
137    /// block, and every padded key position is `≥ context_len`, so the op's
138    /// variable-length mask (`key_pos ≥ context_len → −inf`) neutralizes it
139    /// before softmax. Padding is therefore never *read*, only never *invalid*.
140    pub block_table: Vec<u32>,
141    /// Per-sequence context length (`filled_tokens`), `[B]`.
142    pub context_lens: Vec<u32>,
143    /// `B` — number of sessions (rows).
144    pub batch: usize,
145    /// `max_blocks` — the padded row width (the widest session's block count,
146    /// at least 1 so the tensor stays a well-formed rank-2 `[B, max_blocks]`).
147    pub max_blocks: usize,
148}
149
150impl PageTableHost {
151    /// Shape of the `block_table` `Op::Const` — `[B, max_blocks]`.
152    pub fn block_table_shape(&self) -> Shape {
153        Shape::from_dims(&[self.batch, self.max_blocks])
154    }
155    /// Shape of the `context_lens` `Op::Const` — `[B]`.
156    pub fn context_lens_shape(&self) -> Shape {
157        Shape::from_dims(&[self.batch])
158    }
159}
160
161/// Device-backed KV block pool: the pure [`KvBlockPool`] core plus the physical
162/// K/V pool buffers it hands out slots into. Owns the core (so device-aware
163/// evict/restore can move bytes and update metadata atomically); the core is
164/// reachable via [`core`](Self::core) / [`core_mut`](Self::core_mut) for the
165/// pure capacity/lifecycle verbs.
166pub struct DeviceKvPool {
167    core: KvBlockPool,
168    device: Device,
169    dtype: DType,
170    /// `[num_blocks, block_size, Hkv, D]` — the shape of every layer buffer.
171    pool_shape: Shape,
172    /// Per-layer K pool buffers (`k_pools[l]` is layer `l`'s `[num_blocks, …]`).
173    k_pools: Vec<Arc<RwLock<Storage>>>,
174    /// Per-layer V pool buffers.
175    v_pools: Vec<Arc<RwLock<Storage>>>,
176    /// Identity of these pool buffers — the paged twin of
177    /// [`KvCache::alloc_id`](crate::inference_context::KvCache::alloc_id).
178    /// A held [`PagedDecodeSession`](crate::inference_context::PagedDecodeSession)
179    /// bakes these `Arc`s into its `base_cache`, and
180    /// `rebind_and_realize_paged_prebuilt` re-binds only the per-token data
181    /// (token id, RoPE, block_table, context_lens, offset) — never the pool.
182    /// So the held plan reads the pool it was BUILT against no matter which
183    /// `&mut DeviceKvPool` the caller passes, and geometry alone cannot tell
184    /// two same-shaped pools apart. See [`KvAllocId`].
185    alloc_id: KvAllocId,
186}
187
188impl DeviceKvPool {
189    /// Build a device-backed pool: allocate `n_layers × 2` zero-initialized
190    /// `[num_blocks, block_size, Hkv, D]` buffers of `dtype` on `device`, and
191    /// wrap a pure [`KvBlockPool`] over the same geometry.
192    ///
193    /// `geom.elem_size` must equal `dtype.size_in_bytes()` — the geometry's byte
194    /// accounting (`kv_bytes_resident`) and the physical buffers must agree, so a
195    /// mismatch is a build-time `Err`, never a silently wrong budget signal.
196    pub fn new(geom: KvGeometry, dtype: DType, device: &Device) -> Result<Self> {
197        if geom.elem_size != dtype.size_in_bytes() {
198            return Err(Error::Msg(format!(
199                "DeviceKvPool::new: geometry elem_size {} ≠ dtype {:?} size {} — the \
200                 pool's byte accounting would disagree with its physical buffers",
201                geom.elem_size,
202                dtype,
203                dtype.size_in_bytes(),
204            ))
205            .bt());
206        }
207        let n_layers = geom.n_layers;
208        let pool_shape = Shape::from_dims(&[
209            geom.num_blocks,
210            geom.block_size,
211            geom.n_kv_heads,
212            geom.head_dim,
213        ]);
214        let (k_pools, v_pools) = alloc_layer_buffers(&pool_shape, dtype, device, n_layers)?;
215        Ok(Self {
216            core: KvBlockPool::new(geom),
217            device: device.clone(),
218            dtype,
219            pool_shape,
220            k_pools,
221            v_pools,
222            alloc_id: KvAllocId::next(),
223        })
224    }
225
226    /// Identity of the physical pool buffers a held paged plan is welded to.
227    /// See [`KvAllocId`].
228    ///
229    /// There is no re-mint path: `DeviceKvPool` allocates once in [`Self::new`]
230    /// and never replaces a layer buffer. Block *allocation* churn lives in
231    /// `core` (a `KvBlockPool` index) and reaches the graph through the
232    /// per-token `block_table` rebind, so it is correctly NOT part of this
233    /// identity — over-keying it would rebuild the plan on every new block.
234    pub fn alloc_id(&self) -> KvAllocId {
235        self.alloc_id
236    }
237
238    // --- pure-core access -------------------------------------------------
239
240    /// The pure metadata core (capacity/lifecycle verbs live here).
241    pub fn core(&self) -> &KvBlockPool {
242        &self.core
243    }
244    /// Mutable core — `open`/`append`/`evict`/`splice`/… route through this.
245    pub fn core_mut(&mut self) -> &mut KvBlockPool {
246        &mut self.core
247    }
248    /// Pool geometry (delegates to the core).
249    pub fn geometry(&self) -> KvGeometry {
250        self.core.geometry()
251    }
252
253    // --- physical buffers -------------------------------------------------
254
255    /// Number of transformer layers (= number of K buffers = number of V
256    /// buffers).
257    pub fn n_layers(&self) -> usize {
258        self.k_pools.len()
259    }
260    /// `[num_blocks, block_size, Hkv, D]` — every layer buffer's shape.
261    pub fn pool_shape(&self) -> &Shape {
262        &self.pool_shape
263    }
264    /// `[num_blocks · block_size, Hkv, D]` — the pool buffer viewed with its
265    /// leading (block, slot) axes collapsed into ONE flat slot axis. A physical
266    /// `(phys, slot)` then addresses a single row `phys · block_size + slot`, so
267    /// the paged decode's new-token K/V write is one dynamic start on axis 0
268    /// (`Op::WriteSliceDyn` / `Op::WriteSliceDoff`) instead of a concrete
269    /// two-axis `[(phys,phys+1),(slot,slot+1),…]` slab whose ranges change every
270    /// step. [`Self::build_decode_attn_off`] binds a placeholder of THIS shape to
271    /// the same pool buffer — a pure metadata reshape of [`Self::pool_shape`]
272    /// (same element count, row-major order preserved), so the write still
273    /// mutates the persistent buffer in place.
274    pub fn pool_shape_flat(&self) -> Shape {
275        let g = self.geometry();
276        Shape::from_dims(&[g.num_blocks * g.block_size, g.n_kv_heads, g.head_dim])
277    }
278    /// Pool buffer element dtype.
279    pub fn dtype(&self) -> DType {
280        self.dtype
281    }
282    /// The device the pool buffers live on — a paged forward builds + realizes on
283    /// this device (so a CUDA pool runs on CUDA, not CPU).
284    pub fn device(&self) -> &Device {
285        &self.device
286    }
287    /// Layer `l`'s K pool buffer, if `l < n_layers`.
288    pub fn k_pool(&self, layer: usize) -> Option<&Arc<RwLock<Storage>>> {
289        self.k_pools.get(layer)
290    }
291    /// Layer `l`'s V pool buffer, if `l < n_layers`.
292    pub fn v_pool(&self, layer: usize) -> Option<&Arc<RwLock<Storage>>> {
293        self.v_pools.get(layer)
294    }
295
296    // --- block byte movement (device-general via the executor) ------------
297
298    /// Layer `layer`'s K-or-V pool buffer, or a typed error if `layer` is out
299    /// of range.
300    fn buffer(&self, layer: usize, kind: BlockKind) -> crate::Result<&Arc<RwLock<Storage>>> {
301        let v = match kind {
302            BlockKind::K => &self.k_pools,
303            BlockKind::V => &self.v_pools,
304        };
305        v.get(layer).ok_or_else(|| {
306            msg_err(format!(
307                "DeviceKvPool: no layer {layer} (n_layers={})",
308                self.n_layers()
309            ))
310        })
311    }
312
313    /// Elements in one physical block for one buffer (`block_size · Hkv · D`).
314    fn block_elems(&self) -> usize {
315        let g = self.geometry();
316        g.block_size * g.n_kv_heads * g.head_dim
317    }
318
319    /// The `Op::WriteSlice` rectangular ranges that select physical block
320    /// `phys` in a `[num_blocks, block_size, Hkv, D]` pool buffer:
321    /// `[(phys, phys+1), (0, block_size), (0, Hkv), (0, D)]`. Public so a
322    /// consumer building its own decode graph can write a token's K/V slab
323    /// straight into a block without re-deriving the offset.
324    pub fn block_write_ranges(&self, phys: PhysBlockId) -> Vec<(usize, usize)> {
325        let g = self.geometry();
326        let p = phys as usize;
327        vec![
328            (p, p + 1),
329            (0, g.block_size),
330            (0, g.n_kv_heads),
331            (0, g.head_dim),
332        ]
333    }
334
335    /// Write `data` (`block_size · Hkv · D` f32, row-major `[block_size, Hkv,
336    /// D]`) into physical block `phys` of layer `layer`'s K-or-V buffer, in
337    /// place, through the executor (`Op::WriteSlice`) — so it is device-general
338    /// (the same path a CUDA/Vulkan pool uses), not a host poke.
339    ///
340    /// f32-only for now (the CPU-gated increment); the pool's real dtype is
341    /// carried in [`Self::dtype`] and a byte/dtype-generic form is the follow-up
342    /// tracked for the CUDA bf16 pool.
343    pub fn write_block(
344        &self,
345        layer: usize,
346        kind: BlockKind,
347        phys: PhysBlockId,
348        data: &[f32],
349    ) -> crate::Result<()> {
350        if self.dtype != DType::F32 {
351            return Err(msg_err(format!(
352                "DeviceKvPool::write_block: f32-typed API, pool dtype is {:?} — use write_block_bytes",
353                self.dtype,
354            )));
355        }
356        if data.len() != self.block_elems() {
357            return Err(msg_err(format!(
358                "DeviceKvPool::write_block: data len {} ≠ block elems {}",
359                data.len(),
360                self.block_elems(),
361            )));
362        }
363        // f32 typed wrapper over the dtype-agnostic byte core.
364        self.write_block_bytes(layer, kind, phys, bytemuck::cast_slice::<f32, u8>(data))
365    }
366
367    /// Read physical block `phys` of layer `layer`'s K-or-V buffer back to host
368    /// as `block_size · Hkv · D` f32 (row-major `[block_size, Hkv, D]`). Reads
369    /// *through the executor* (`Slice` of the cache-bound buffer, realized) so
370    /// it is device-general — a CUDA/Vulkan pool D2H-copies the block the same
371    /// way. f32-only for now (see [`Self::write_block`]).
372    pub fn read_block(
373        &self,
374        layer: usize,
375        kind: BlockKind,
376        phys: PhysBlockId,
377    ) -> crate::Result<Vec<f32>> {
378        if self.dtype != DType::F32 {
379            return Err(msg_err(format!(
380                "DeviceKvPool::read_block: f32-typed API, pool dtype is {:?} — use read_block_bytes",
381                self.dtype,
382            )));
383        }
384        let bytes = self.read_block_bytes(layer, kind, phys)?;
385        let v: &[f32] = bytemuck::try_cast_slice(&bytes)
386            .map_err(|e| msg_err(format!("DeviceKvPool::read_block: f32 cast: {e:?}")))?;
387        Ok(v.to_vec())
388    }
389
390    /// **rung-2 product** — materialize a registered prefix at a block-aligned,
391    /// NON-ZERO position offset in `dst` via a uniform θ·M RoPE delta-rotation of
392    /// its cached keys. Unlike the pure-core zero-copy
393    /// [`splice_prefix_from`](fuel_kv_pool::KvBlockPool::splice_prefix_from)
394    /// (rung-1, prefix at position 0), this COPIES: the prefix's keys were rotated
395    /// for positions `0..N` and are numerically wrong at `M..M+N`, so fresh blocks
396    /// are allocated and written with rotated K + copied V. `rope_base` is the
397    /// model's θ — a plain param, so a pool-driving consumer supplies its own (the
398    /// §15 seam). Returns the shared token count `N`. `M = filled_tokens(dst)` must
399    /// be block-aligned; validation + fresh-block bookkeeping is
400    /// [`KvBlockPool::alloc_shifted_prefix_slots`](fuel_kv_pool::KvBlockPool::alloc_shifted_prefix_slots),
401    /// which is transactional (a refusal allocates nothing). f32 pool.
402    pub fn splice_prefix_shifted(
403        &mut self,
404        prefix: fuel_kv_pool::PrefixId,
405        dst: fuel_kv_pool::SessionHandle,
406        rope_base: f64,
407    ) -> crate::Result<usize> {
408        let g = self.geometry();
409        let (m, pairs) = self
410            .core_mut()
411            .alloc_shifted_prefix_slots(prefix, dst)
412            .map_err(|e| msg_err(format!("DeviceKvPool::splice_prefix_shifted: {e:?}")))?;
413        for (src, fresh) in &pairs {
414            for l in 0..g.n_layers {
415                // K: delta-rotate the cached (post-RoPE) block by θ·M into the fresh block.
416                let k = self.read_block(l, BlockKind::K, *src)?;
417                let rot = crate::lazy::Tensor::rope_delta_rotate_block_f32(
418                    self.device(),
419                    &k,
420                    rope_base,
421                    m,
422                    g.block_size,
423                    g.n_kv_heads,
424                    g.head_dim,
425                );
426                self.write_block(l, BlockKind::K, *fresh, &rot)?;
427                // V: position-independent, copied verbatim (dtype-agnostic bytes).
428                let v = self.read_block_bytes(l, BlockKind::V, *src)?;
429                self.write_block_bytes(l, BlockKind::V, *fresh, &v)?;
430            }
431        }
432        Ok(pairs.len() * g.block_size)
433    }
434
435    /// Dtype-agnostic byte-level block WRITE — the movement core that the f32
436    /// [`Self::write_block`] wrapper and the evict/restore/CoW paths build on.
437    /// `bytes` is the block's raw content in the pool's dtype
438    /// (`block_elems · dtype.size_in_bytes()` bytes, row-major `[block_size, Hkv,
439    /// D]`). Backend-neutral: builds a dtype-tagged `Op::Const` from the bytes and
440    /// `Op::WriteSlice`s it into the persistent pool buffer through the executor —
441    /// the same path a CUDA/Vulkan (or future) pool uses; no host poke, no
442    /// per-backend code.
443    pub fn write_block_bytes(
444        &self,
445        layer: usize,
446        kind: BlockKind,
447        phys: PhysBlockId,
448        bytes: &[u8],
449    ) -> crate::Result<()> {
450        let g = self.geometry();
451        if phys as usize >= g.num_blocks {
452            return Err(msg_err(format!(
453                "DeviceKvPool::write_block_bytes: physical block {phys} out of range (num_blocks={})",
454                g.num_blocks,
455            )));
456        }
457        let want = self.block_elems() * self.dtype_elem_bytes()?;
458        if bytes.len() != want {
459            return Err(msg_err(format!(
460                "DeviceKvPool::write_block_bytes: {} bytes ≠ block bytes {} \
461                 ([block_size {}, Hkv {}, D {}] for {:?})",
462                bytes.len(),
463                want,
464                g.block_size,
465                g.n_kv_heads,
466                g.head_dim,
467                self.dtype,
468            )));
469        }
470        let buf = Arc::clone(self.buffer(layer, kind)?);
471        // Dtype-tagged Const from the raw bytes (backend-neutral) → WriteSlice into
472        // the persistent pool buffer; realized as u8 (a byte reinterpret, discarded).
473        let src = self.const_from_bytes(
474            bytes,
475            Shape::from_dims(&[1, g.block_size, g.n_kv_heads, g.head_dim]),
476        )?;
477        let dest = src.const_placeholder_like(self.pool_shape.clone(), self.dtype);
478        let post = dest.write_slice(&src, self.block_write_ranges(phys))?;
479        let mut cache = StorageCache::new();
480        cache.insert(dest.node_id(), buf);
481        let _ = crate::pipelined_bridge::realize_one_bytes_with_initial(
482            post.graph_handle(),
483            post.node_id(),
484            &self.device,
485            cache,
486        )?;
487        Ok(())
488    }
489
490    /// Dtype-agnostic byte-level block READ — sibling of [`Self::write_block_bytes`].
491    /// Returns the block's raw bytes in the pool's dtype via the byte-view entry
492    /// `realize_one_bytes` (a byte reinterpret of the realized `Slice`, correct for ANY
493    /// dtype by design — there is no per-dtype read path; GAP-327 keeps this UNGUARDED).
494    pub fn read_block_bytes(
495        &self,
496        layer: usize,
497        kind: BlockKind,
498        phys: PhysBlockId,
499    ) -> crate::Result<Vec<u8>> {
500        let g = self.geometry();
501        if phys as usize >= g.num_blocks {
502            return Err(msg_err(format!(
503                "DeviceKvPool::read_block_bytes: physical block {phys} out of range (num_blocks={})",
504                g.num_blocks,
505            )));
506        }
507        let buf = Arc::clone(self.buffer(layer, kind)?);
508        // Bind the pool buffer to a placeholder, slice the one block out, realize
509        // as u8 — a byte reinterpret, so the read is correct for ANY dtype. The
510        // f32 anchor only mints the graph (mirrors the historical read_block).
511        let anchor = Tensor::from_f32(vec![0.0], Shape::from_dims(&[1]), &self.device)?;
512        let dest = anchor.const_placeholder_like(self.pool_shape.clone(), self.dtype);
513        let block = dest
514            .slice(0, phys as usize, 1)?
515            .reshape(Shape::from_dims(&[self.block_elems()]))?;
516        let mut cache = StorageCache::new();
517        cache.insert(dest.node_id(), buf);
518        crate::pipelined_bridge::realize_one_bytes_with_initial(
519            block.graph_handle(),
520            block.node_id(),
521            &self.device,
522            cache,
523        )
524    }
525
526    /// Byte width of one pool element for byte-level movement. F32/BF16 only (the
527    /// activation dtypes the CUDA bf16 leg needs); F16 movement is a trivial
528    /// follow-up (needs a `Tensor::from_f16_on` const source).
529    fn dtype_elem_bytes(&self) -> crate::Result<usize> {
530        match self.dtype {
531            DType::F32 => Ok(4),
532            DType::BF16 => Ok(2),
533            other => Err(msg_err(format!(
534                "DeviceKvPool: byte movement unsupported for dtype {other:?} (F32/BF16)",
535            ))),
536        }
537    }
538
539    /// Build a fresh-graph `Op::Const` of the pool's dtype from raw little-endian
540    /// element bytes — the backend-neutral source [`Self::write_block_bytes`]
541    /// WriteSlices. Dispatches `self.dtype` to the matching typed graph
542    /// constructor via alignment-safe `try_cast_slice` (never panics). For BF16 a
543    /// tiny f32 anchor mints the graph (mirrors `read_block_bytes`'s anchor); the
544    /// bf16 const is a sibling on it.
545    fn const_from_bytes(&self, bytes: &[u8], shape: Shape) -> crate::Result<Tensor> {
546        let dev = &self.device;
547        match self.dtype {
548            DType::F32 => {
549                let v: &[f32] = bytemuck::try_cast_slice(bytes)
550                    .map_err(|e| msg_err(format!("const_from_bytes: f32 cast: {e:?}")))?;
551                Ok(Tensor::from_f32(v.to_vec(), shape, dev)?)
552            }
553            DType::BF16 => {
554                let v: &[half::bf16] = bytemuck::try_cast_slice(bytes)
555                    .map_err(|e| msg_err(format!("const_from_bytes: bf16 cast: {e:?}")))?;
556                let anchor = Tensor::from_f32(vec![0.0f32], Shape::from_dims(&[1]), dev)?;
557                Ok(Tensor::from_bf16_on(
558                    anchor.graph(),
559                    v.to_vec(),
560                    shape,
561                    dev,
562                )?)
563            }
564            other => Err(msg_err(format!(
565                "DeviceKvPool::const_from_bytes: unsupported dtype {other:?} (F32/BF16)",
566            ))),
567        }
568    }
569
570    // --- page-table materialization (C: Op::PagedAttn operands) -----------
571
572    /// Project the core's per-session block tables into the `Op::PagedAttn`
573    /// `[B, max_blocks]` u32 `block_table` + `[B]` u32 `context_lens`, one row
574    /// per session in `sessions` order (that order defines the op's batch axis).
575    ///
576    /// Fails (typed, never a panic) if any session is unknown or has an
577    /// externalized slot — a session must be resident to back live attention, so
578    /// a mis-sequenced call surfaces as [`KvAllocError`] rather than routing
579    /// through a reclaimed block. Shorter rows are zero-padded (see
580    /// [`PageTableHost::block_table`]).
581    pub fn materialize_block_table(
582        &self,
583        sessions: &[SessionHandle],
584    ) -> std::result::Result<PageTableHost, KvAllocError> {
585        let batch = sessions.len();
586        let mut rows: Vec<Vec<PhysBlockId>> = Vec::with_capacity(batch);
587        let mut context_lens: Vec<u32> = Vec::with_capacity(batch);
588        for &s in sessions {
589            rows.push(self.core.session_block_table(s)?);
590            let filled = self
591                .core
592                .filled_tokens(s)
593                .ok_or(KvAllocError::UnknownSession)?;
594            context_lens.push(filled as u32);
595        }
596        // At least 1 column so the tensor is a well-formed rank-2 [B, max_blocks].
597        let max_blocks = rows.iter().map(|r| r.len()).max().unwrap_or(0).max(1);
598        let mut block_table = vec![0u32; batch * max_blocks];
599        for (bi, row) in rows.iter().enumerate() {
600            for (i, &p) in row.iter().enumerate() {
601                block_table[bi * max_blocks + i] = p;
602            }
603        }
604        Ok(PageTableHost {
605            block_table,
606            context_lens,
607            batch,
608            max_blocks,
609        })
610    }
611
612    /// Capacity-padded sibling of [`Self::materialize_block_table`]: every row is
613    /// right-padded with 0 to a FIXED width `max_blocks_cap`, so the block_table
614    /// shape `[B, max_blocks_cap]` is constant across decode steps — the paged
615    /// plan-once prerequisite. Padded entries (0) are in-bounds and neutralized
616    /// by `Op::PagedAttn`'s `context_len` mask (a padded key position is always
617    /// `>= context_len`). Errors if a session already occupies more than
618    /// `max_blocks_cap` blocks.
619    pub fn materialize_block_table_padded(
620        &self,
621        sessions: &[SessionHandle],
622        max_blocks_cap: usize,
623    ) -> std::result::Result<PageTableHost, KvAllocError> {
624        let batch = sessions.len();
625        let cap = max_blocks_cap.max(1); // >= 1 so the tensor is well-formed rank-2
626        let mut block_table = vec![0u32; batch * cap];
627        let mut context_lens: Vec<u32> = Vec::with_capacity(batch);
628        for (bi, &s) in sessions.iter().enumerate() {
629            let row = self.core.session_block_table(s)?;
630            if row.len() > cap {
631                // The fixed capacity can't hold this session's blocks — a caller
632                // bug (pass a cap >= ceil(max_seq_len / block_size)). Never truncate.
633                return Err(KvAllocError::OutOfBlocks {
634                    need: row.len(),
635                    have: cap,
636                });
637            }
638            for (i, &p) in row.iter().enumerate() {
639                block_table[bi * cap + i] = p;
640            }
641            let filled = self
642                .core
643                .filled_tokens(s)
644                .ok_or(KvAllocError::UnknownSession)?;
645            context_lens.push(filled as u32);
646        }
647        Ok(PageTableHost {
648            block_table,
649            context_lens,
650            batch,
651            max_blocks: cap,
652        })
653    }
654
655    // --- paged decode step (the storage integration building block) -------
656
657    /// Build (does NOT realize) one decode step's paged attention for one layer:
658    /// write the new token's `[Hkv, D]` K/V into its physical pool slot, then run
659    /// `Op::PagedAttn` over the (post-write) pool via the session's block table.
660    /// The graph-builder half of paged decode — the model's forward calls this
661    /// per layer, binds `k_pool(layer)`/`v_pool(layer)` into the realize cache
662    /// (the caller owns the binding + the single realize for the whole forward),
663    /// and threads the result into the o-projection.
664    ///
665    /// All tensors are on `q`'s graph: `k_pool_ph`/`v_pool_ph` are
666    /// [`const_placeholder_like`](Tensor::const_placeholder_like) placeholders
667    /// the caller binds to this layer's pool buffers; `block_table`/`context_lens`
668    /// are the [`materialize_block_table`](Self::materialize_block_table) tensors;
669    /// `q` is `[1, Hq, 1, D]`; `k_new`/`v_new` are `[1, Hkv, 1, D]` (the new
670    /// token, post-RoPE for K). `(phys_block, slot)` is where the new token lands
671    /// — `phys_block = resident_block(session, tok_pos / block_size)`,
672    /// `slot = tok_pos % block_size`, after the session has been `append`ed to
673    /// cover `tok_pos`. Returns the attention output `[1, Hq, 1, D]`.
674    ///
675    /// The write mutates the bound pool buffer in place (so it persists for the
676    /// next step) AND feeds this step's `Op::PagedAttn` — one graph, one realize,
677    /// exactly like the contiguous `write_slice`-then-attend path it replaces.
678    pub fn build_decode_attn(
679        &self,
680        k_pool_ph: &Tensor,
681        v_pool_ph: &Tensor,
682        q: &Tensor,
683        k_new: &Tensor,
684        v_new: &Tensor,
685        block_table: &Tensor,
686        context_lens: &Tensor,
687        phys_block: PhysBlockId,
688        slot: usize,
689        scale: f32,
690    ) -> crate::Result<Tensor> {
691        let g = self.geometry();
692        let p = phys_block as usize;
693        // The new token occupies one slot of one physical block: [1, 1, Hkv, D].
694        let slot_ranges = vec![
695            (p, p + 1),
696            (slot, slot + 1),
697            (0, g.n_kv_heads),
698            (0, g.head_dim),
699        ];
700        let slot_shape = Shape::from_dims(&[1, 1, g.n_kv_heads, g.head_dim]);
701        // [1, Hkv, 1, D] → [1, 1, Hkv, D] is a pure reshape (h-major order is
702        // identical), aligning the new token to the pool block's slot layout.
703        let k_slot = k_new.reshape(slot_shape.clone())?;
704        let v_slot = v_new.reshape(slot_shape)?;
705        let post_k = k_pool_ph.write_slice(&k_slot, slot_ranges.clone())?;
706        let post_v = v_pool_ph.write_slice(&v_slot, slot_ranges)?;
707        q.paged_attn(
708            &post_k,
709            &post_v,
710            block_table,
711            context_lens,
712            None,
713            scale,
714            g.block_size,
715            None,
716        )
717    }
718
719    /// Batched (`B = K`) sibling of [`Self::build_decode_attn`] — one decode step over
720    /// K sessions sharing this layer's pool buffers (paged-storage PS4a). Each
721    /// session `i` contributes one new token: its `[Hkv, D]` K/V (row `i` of
722    /// `k_new`/`v_new`, shaped `[K, Hkv, 1, D]`) is written into its own physical
723    /// slot `writes[i] = (phys, slot)`; then a single `Op::PagedAttn` at batch
724    /// `K` reads all K sessions via the `[K, max_blocks]` `block_table` +
725    /// `[K]` `context_lens`. `q` is `[K, Hq, 1, D]`; returns `[K, Hq, 1, D]`.
726    ///
727    /// The K per-session writes chain on the same bound pool buffer (each
728    /// `write_slice` returns the post-write handle) so all land before the op
729    /// reads — one graph, one realize, exactly like the single-session path.
730    /// `writes` must be non-empty and in `block_table`/`q` batch-row order.
731    pub fn build_decode_attn_batched(
732        &self,
733        k_pool_ph: &Tensor,
734        v_pool_ph: &Tensor,
735        q: &Tensor,
736        k_new: &Tensor,
737        v_new: &Tensor,
738        block_table: &Tensor,
739        context_lens: &Tensor,
740        writes: &[(PhysBlockId, usize)],
741        scale: f32,
742    ) -> crate::Result<Tensor> {
743        let g = self.geometry();
744        let slot_shape = Shape::from_dims(&[1, 1, g.n_kv_heads, g.head_dim]);
745        let mut post_k: Option<Tensor> = None;
746        let mut post_v: Option<Tensor> = None;
747        for (i, &(phys, slot)) in writes.iter().enumerate() {
748            let p = phys as usize;
749            let ranges = vec![
750                (p, p + 1),
751                (slot, slot + 1),
752                (0, g.n_kv_heads),
753                (0, g.head_dim),
754            ];
755            // Row i of the batched new K/V: [K,Hkv,1,D] --slice0--> [1,Hkv,1,D]
756            // --reshape--> [1,1,Hkv,D] (the block-slot layout).
757            let k_row = k_new.slice(0, i, 1)?.reshape(slot_shape.clone())?;
758            let v_row = v_new.slice(0, i, 1)?.reshape(slot_shape.clone())?;
759            let dest_k = post_k.as_ref().unwrap_or(k_pool_ph);
760            let dest_v = post_v.as_ref().unwrap_or(v_pool_ph);
761            post_k = Some(dest_k.write_slice(&k_row, ranges.clone())?);
762            post_v = Some(dest_v.write_slice(&v_row, ranges)?);
763        }
764        let post_k = post_k.ok_or_else(|| {
765            msg_err("build_decode_attn_batched: writes is empty (need ≥ 1 session)".into())
766        })?;
767        // `post_k`/`post_v` are assigned together in the loop above, so this is
768        // unreachable — but a serving path should degrade to an error rather
769        // than abort the process if that pairing ever drifts.
770        let post_v = post_v.ok_or_else(|| {
771            msg_err("build_decode_attn_batched: post_v unset while post_k is set".into())
772        })?;
773        q.paged_attn(
774            &post_k,
775            &post_v,
776            block_table,
777            context_lens,
778            None,
779            scale,
780            g.block_size,
781            None,
782        )
783    }
784
785    /// **Plan-once sibling of [`Self::build_decode_attn`]** — the new token's K/V
786    /// is written at a FLATTENED, runtime-resolved offset instead of a concrete
787    /// two-axis slab, so the write op is structurally identical across decode
788    /// steps (only the bound offset changes). The pool buffer is viewed as
789    /// [`Self::pool_shape_flat`] `[num_blocks·block_size, Hkv, D]`; the new token
790    /// `[1, Hkv, D]` lands at row `linear = phys·block_size + slot` via a single
791    /// dynamic start on axis 0. The post-write flat buffer is reshaped back to
792    /// `[num_blocks, block_size, Hkv, D]` for `Op::PagedAttn`.
793    ///
794    /// `k_pool_ph`/`v_pool_ph` must be `const_placeholder_like(pool_shape_flat())`
795    /// placeholders the caller binds to this layer's pool buffers (NOTE: the flat
796    /// shape, not [`Self::pool_shape`]). The offset is supplied one of two ways,
797    /// mirroring the contiguous KV-write split in `apply_layer_with_kv_writes`:
798    /// - `write_off = Some(off)`: a rank-0 `I64` tensor read device-side at kernel
799    ///   launch (`Op::WriteSliceDoff`) — the CUDA-graph-capturable arm.
800    /// - `write_off = None`: `Op::WriteSliceDyn` with `DynScalar::Sym(write_sym)`,
801    ///   the caller binding `write_sym → linear` in the per-pass `SymEnv` — the
802    ///   backend-generic (CPU/Vulkan) arm.
803    ///
804    /// Both land the same slab at the same offset — bit-identical to the concrete
805    /// [`Self::build_decode_attn`]. Returns the attention output `[1, Hq, 1, D]`.
806    pub fn build_decode_attn_off(
807        &self,
808        k_pool_ph: &Tensor,
809        v_pool_ph: &Tensor,
810        q: &Tensor,
811        k_new: &Tensor,
812        v_new: &Tensor,
813        block_table: &Tensor,
814        context_lens: &Tensor,
815        write_off: Option<&Tensor>,
816        write_sym: fuel_ir::SymId,
817        scale: f32,
818    ) -> crate::Result<Tensor> {
819        let g = self.geometry();
820        // New token as a single flat row: [1, Hkv, 1, D] → [1, Hkv, D] (h-major
821        // order preserved), matching one row of the flattened pool's slot axis.
822        let flat_row = Shape::from_dims(&[1, g.n_kv_heads, g.head_dim]);
823        let k_row = k_new.reshape(flat_row.clone())?;
824        let v_row = v_new.reshape(flat_row)?;
825        // Width-1 slab on axis 0; the start is the dynamic offset
826        // (linear = phys·block_size + slot). ranges[0].0 is ignored on the
827        // dynamic axis — the width (1) must equal the source's axis-0 dim.
828        let ranges = vec![(0usize, 1usize), (0, g.n_kv_heads), (0, g.head_dim)];
829        // Write directly into the flat-shaped bound placeholder (no reshape
830        // BETWEEN the binding and the write) so the in-place pool mutation is
831        // preserved exactly like the contiguous `write_slice_doff` precedent.
832        let (post_flat_k, post_flat_v) = match write_off {
833            Some(off) => (
834                k_pool_ph.write_slice_doff(&k_row, off, 0, ranges.clone())?,
835                v_pool_ph.write_slice_doff(&v_row, off, 0, ranges)?,
836            ),
837            None => {
838                let dyn_off = fuel_ir::DynScalar::Sym(write_sym);
839                (
840                    k_pool_ph.write_slice_dyn(&k_row, ranges.clone(), 0, dyn_off)?,
841                    v_pool_ph.write_slice_dyn(&v_row, ranges, 0, dyn_off)?,
842                )
843            }
844        };
845        // Reshape the post-write flat buffers back to the paged-cache shape for
846        // Op::PagedAttn — a consumer-side view; the write already mutated the
847        // persistent pool buffer in place.
848        let pool4 = Shape::from_dims(&[g.num_blocks, g.block_size, g.n_kv_heads, g.head_dim]);
849        let post_k = post_flat_k.reshape(pool4.clone())?;
850        let post_v = post_flat_v.reshape(pool4)?;
851        q.paged_attn(
852            &post_k,
853            &post_v,
854            block_table,
855            context_lens,
856            None,
857            scale,
858            g.block_size,
859            None,
860        )
861    }
862
863    /// **Copy-on-write guard** for the paged decode write path. Ensure the
864    /// physical block backing logical slot `logical_block` of `session` is
865    /// exclusively owned before a new token is written into it. If the block is
866    /// **shared** (spliced, refcount > 1), break the share (`cow_break` → a fresh
867    /// block) and COPY the shared block's bytes (all layers, K + V) into it, so
868    /// the session keeps the shared-prefix content while its write no longer
869    /// mutates a block another session still references. Returns the (now
870    /// private) physical block to write into; an exclusive block is returned
871    /// unchanged. **Must be called after `append` and before writing the token**
872    /// — otherwise decoding a spliced session silently corrupts its co-sharers.
873    pub fn ensure_writable_block(
874        &mut self,
875        session: SessionHandle,
876        logical_block: usize,
877    ) -> crate::Result<PhysBlockId> {
878        let old = self
879            .core
880            .resident_block(session, logical_block)
881            .ok_or_else(|| {
882                msg_err(format!(
883                    "ensure_writable_block: logical block {logical_block} not resident"
884                ))
885            })?;
886        if self.core.block_refcount(old) <= 1 {
887            return Ok(old); // exclusive — writable in place, no copy needed
888        }
889        // Shared → copy-on-write: fresh private block, then copy old → new so the
890        // session's prefix content survives (the donor's block is untouched).
891        let new = self
892            .core
893            .cow_break(session, logical_block)
894            .map_err(alloc_err)?;
895        for l in 0..self.n_layers() {
896            let k = self.read_block_bytes(l, BlockKind::K, old)?;
897            self.write_block_bytes(l, BlockKind::K, new, &k)?;
898            let v = self.read_block_bytes(l, BlockKind::V, old)?;
899            self.write_block_bytes(l, BlockKind::V, new, &v)?;
900        }
901        Ok(new)
902    }
903
904    // --- C-3: device-backed evict / restore (bytes move device↔host) ------
905
906    /// Evict a session's entire resident state: capture the bytes of its
907    /// exclusive blocks (D2H, all layers, K and V) into a [`DeviceEvicted`]
908    /// handle, then hand the physical blocks back to the pool via the core.
909    /// `restore` re-materializes it byte-for-byte. Shared (spliced) blocks are
910    /// never detached and carry no bytes here (they stay resident).
911    pub fn evict(&mut self, s: SessionHandle) -> crate::Result<DeviceEvicted> {
912        let n = self
913            .core
914            .session_blocks(s)
915            .ok_or_else(|| alloc_err(KvAllocError::UnknownSession))?;
916        let all: Vec<usize> = (0..n).collect();
917        self.evict_blocks(s, &all)
918    }
919
920    /// Evict a SET of a session's logical blocks (sub-session tiering). Captures
921    /// the bytes of exactly the *exclusive* blocks in `indices` (a shared block
922    /// stays resident — its sharer still needs it), then calls the core's
923    /// refcount-aware [`KvBlockPool::evict_blocks`]. The returned handle carries
924    /// the detached bytes; the rest of the session keeps decoding.
925    pub fn evict_blocks(
926        &mut self,
927        s: SessionHandle,
928        indices: &[usize],
929    ) -> crate::Result<DeviceEvicted> {
930        let n_layers = self.n_layers();
931        // Capture bytes BEFORE the core detaches anything (an evicted block is
932        // returned to the free pool and may be overwritten immediately). Only
933        // exclusive resident blocks are actually detached, so only those are
934        // read back — a shared or already-externalized slot moves no bytes.
935        let to_save: Vec<usize> = indices
936            .iter()
937            .copied()
938            .filter(|&i| match self.core.resident_block(s, i) {
939                Some(p) => self.core.block_refcount(p) == 1,
940                None => false,
941            })
942            .collect();
943        let mut saved = Vec::with_capacity(to_save.len());
944        for &i in &to_save {
945            let p = self.core.resident_block(s, i).ok_or_else(|| {
946                msg_err(format!(
947                    "evict: slot {i} stopped being resident between the \
948                     residency filter and the save loop"
949                ))
950            })?;
951            let mut k = Vec::with_capacity(n_layers);
952            let mut v = Vec::with_capacity(n_layers);
953            for l in 0..n_layers {
954                k.push(self.read_block_bytes(l, BlockKind::K, p)?);
955                v.push(self.read_block_bytes(l, BlockKind::V, p)?);
956            }
957            saved.push(SavedBlock {
958                logical_slot: i,
959                k,
960                v,
961            });
962        }
963        let report = self.core.evict_blocks(s, indices).map_err(alloc_err)?;
964        Ok(DeviceEvicted {
965            core_handle: report.handle,
966            saved,
967        })
968    }
969
970    /// Re-materialize an evicted session: the core allocates fresh physical
971    /// blocks for the externalized slots, then the saved bytes are written back
972    /// (H2D) into whichever blocks the pool handed out — so the restored content
973    /// is byte-identical regardless of physical reuse. Fails (typed) if the pool
974    /// can't fit the session's blocks again (`OutOfBlocks`).
975    pub fn restore(&mut self, s: SessionHandle, handle: DeviceEvicted) -> crate::Result<()> {
976        let DeviceEvicted { core_handle, saved } = handle;
977        self.core.restore(s, core_handle).map_err(alloc_err)?;
978        for sb in &saved {
979            let p = self
980                .core
981                .resident_block(s, sb.logical_slot)
982                .ok_or_else(|| {
983                    msg_err(format!(
984                        "DeviceKvPool::restore: slot {} not resident after core restore — \
985                     the core and device handle disagree (internal bug)",
986                        sb.logical_slot,
987                    ))
988                })?;
989            for l in 0..self.n_layers() {
990                self.write_block_bytes(l, BlockKind::K, p, &sb.k[l])?;
991                self.write_block_bytes(l, BlockKind::V, p, &sb.v[l])?;
992            }
993        }
994        Ok(())
995    }
996}
997
998/// The pool's K and V storages as two parallel columns, indexed by layer.
999/// Kept as two `Vec`s rather than a `Vec` of pairs because the device pool
1000/// allocates each column as one contiguous run.
1001type KvStorageColumns = (Vec<Arc<RwLock<Storage>>>, Vec<Arc<RwLock<Storage>>>);
1002
1003/// Allocate `n_layers` pairs of zero-initialized `shape` buffers on `device`,
1004/// returning `(k_pools, v_pools)`. Mirrors [`crate::inference_context::KvCache::
1005/// with_capacity`]'s `Op::Alloc → Op::ZeroFill → realize_many` path: one Alloc
1006/// (uninit) + ZeroFill (destructive in-place zero) pair per buffer, all realized
1007/// in a single pass so device-handle reuse is automatic.
1008fn alloc_layer_buffers(
1009    shape: &Shape,
1010    dtype: DType,
1011    device: &Device,
1012    n_layers: usize,
1013) -> Result<KvStorageColumns> {
1014    let target_loc = device.location();
1015    let graph = Arc::new(RwLock::new(Graph::new()));
1016    let mut cache = StorageCache::new();
1017
1018    // Non-CPU targets need a device anchor in the cache so the first Op::Alloc's
1019    // device lookup succeeds (see with_capacity's note). CPU returns None.
1020    if let Some(seed) = crate::pipelined_bridge::device_seed_storage(device)? {
1021        let anchor_id = graph
1022            .write()
1023            .map_err(|_| Error::Msg("graph lock poisoned during DeviceKvPool build".into()).bt())?
1024            .push(Node {
1025                op: Op::Const,
1026                inputs: vec![],
1027                shape: Shape::from_dims(&[4]),
1028                dtype: DType::U8,
1029            });
1030        cache.insert(anchor_id, Arc::new(RwLock::new(seed)));
1031    }
1032
1033    // Emit 2*n_layers (Alloc, ZeroFill) pairs — K then V per layer.
1034    let zero_fill_ids: Vec<NodeId> = {
1035        let mut g = graph
1036            .write()
1037            .map_err(|_| Error::Msg("graph lock poisoned during DeviceKvPool build".into()).bt())?;
1038        (0..(2 * n_layers))
1039            .map(|_| {
1040                let alloc_id = g.push(Node {
1041                    op: Op::Alloc { target: target_loc },
1042                    inputs: vec![],
1043                    shape: shape.clone(),
1044                    dtype,
1045                });
1046                g.push(Node {
1047                    op: Op::ZeroFill,
1048                    inputs: vec![alloc_id],
1049                    shape: shape.clone(),
1050                    dtype,
1051                })
1052            })
1053            .collect()
1054    };
1055
1056    let realized = PipelinedExecutor::realize_many(Arc::clone(&graph), &zero_fill_ids, cache)?;
1057    if realized.len() != 2 * n_layers {
1058        return Err(Error::Msg(format!(
1059            "DeviceKvPool::new: realize_many returned {} storages for {} Op::ZeroFill \
1060             targets — internal bug",
1061            realized.len(),
1062            2 * n_layers,
1063        ))
1064        .bt());
1065    }
1066
1067    let mut it = realized.into_iter();
1068    let mut k_pools = Vec::with_capacity(n_layers);
1069    let mut v_pools = Vec::with_capacity(n_layers);
1070    // The length was checked to be exactly `2 * n_layers` just above, so the
1071    // iterator cannot run short — but pairing K with V by consuming two items
1072    // per turn is the kind of invariant that breaks silently under a later
1073    // edit, and getting it wrong would mis-pair pools rather than fail loudly.
1074    let short = |what: &str, l: usize| {
1075        crate::Error::Msg(format!(
1076            "kv pool realize: iterator ran short at layer {l} fetching {what}, \
1077             despite a checked length of {} — internal bug",
1078            2 * n_layers
1079        ))
1080        .bt()
1081    };
1082    for l in 0..n_layers {
1083        let (k_arc, _) = it.next().ok_or_else(|| short("K", l))?;
1084        let (v_arc, _) = it.next().ok_or_else(|| short("V", l))?;
1085        k_pools.push(k_arc);
1086        v_pools.push(v_arc);
1087    }
1088    let _ = Layout::contiguous(shape.clone()); // pool_shape is the layout; kept explicit for clarity
1089    Ok((k_pools, v_pools))
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094    use super::*;
1095
1096    fn geom(n_layers: usize, num_blocks: usize, block_size: usize) -> KvGeometry {
1097        KvGeometry {
1098            n_layers,
1099            num_blocks,
1100            block_size,
1101            n_kv_heads: 2,
1102            head_dim: 8,
1103            elem_size: 4, // f32
1104        }
1105    }
1106
1107    /// The pool allocates exactly `n_layers` K buffers + `n_layers` V buffers,
1108    /// each a real, correctly-sized, zero-initialized `[num_blocks, block_size,
1109    /// Hkv, D]` device buffer.
1110    #[test]
1111    fn allocates_two_pool_buffers_per_layer_with_the_paged_cache_shape() {
1112        let g = geom(
1113            /*n_layers*/ 3, /*num_blocks*/ 8, /*block_size*/ 4,
1114        );
1115        let pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
1116
1117        assert_eq!(pool.n_layers(), 3, "one K + one V buffer per layer");
1118        assert_eq!(
1119            pool.pool_shape().dims(),
1120            &[8, 4, 2, 8],
1121            "[num_blocks, block_size, Hkv, D]"
1122        );
1123        let want_elems = 8 * 4 * 2 * 8;
1124        for l in 0..3 {
1125            for (name, buf) in [("k", pool.k_pool(l)), ("v", pool.v_pool(l))] {
1126                let s = buf
1127                    .unwrap_or_else(|| panic!("{name}_pool({l}) exists"))
1128                    .read()
1129                    .unwrap();
1130                assert_eq!(s.dtype(), DType::F32, "{name}[{l}] dtype");
1131                assert_eq!(s.elem_count(), want_elems, "{name}[{l}] element count");
1132            }
1133        }
1134        assert!(pool.k_pool(3).is_none(), "no layer 3 in a 3-layer pool");
1135    }
1136
1137    /// A geometry whose `elem_size` disagrees with the requested dtype is a
1138    /// build-time typed error — the byte-accounting/buffer mismatch never ships.
1139    #[test]
1140    fn dtype_vs_elem_size_mismatch_is_a_build_time_error() {
1141        let mut g = geom(1, 4, 4);
1142        g.elem_size = 2; // claims bf16-sized elements…
1143        let err = DeviceKvPool::new(g, DType::F32, &Device::cpu()); // …but asks for f32 buffers
1144        assert!(
1145            err.is_err(),
1146            "elem_size 2 vs f32 (4 bytes) must be rejected"
1147        );
1148    }
1149
1150    /// The page table projects the core's *actual* per-session physical block
1151    /// ids (a non-identity permutation here) into row-major `[B, max_blocks]`,
1152    /// zero-pads short rows, and reports each session's `filled_tokens` as its
1153    /// context length. A stub that emitted an identity `[0,1,2,…]` table would
1154    /// fail against the permuted layout.
1155    #[test]
1156    fn materialize_block_table_projects_the_cores_permuted_layout() {
1157        let mut pool = DeviceKvPool::new(geom(1, 16, 4), DType::F32, &Device::cpu()).unwrap();
1158
1159        // Force a NON-identity physical assignment: open a filler session, then
1160        // a real one, discard the filler, and open more — so the second real
1161        // session's blocks are not 0,1,2,… in order.
1162        let filler = pool.core_mut().open();
1163        pool.core_mut().append(filler, 8).unwrap(); // grabs physical 0,1
1164        let sa = pool.core_mut().open();
1165        pool.core_mut().append(sa, 12).unwrap(); // grabs physical 2,3,4 (3 blocks, 12 tokens)
1166        pool.core_mut().discard(filler); // returns 0,1 to the free list
1167        let sb = pool.core_mut().open();
1168        pool.core_mut().append(sb, 6).unwrap(); // grabs freed blocks (6 tokens → 2 blocks)
1169
1170        // Ground truth straight from the core.
1171        let a_blocks = pool.core().session_block_table(sa).unwrap();
1172        let b_blocks = pool.core().session_block_table(sb).unwrap();
1173        assert_eq!(a_blocks.len(), 3);
1174        assert_eq!(b_blocks.len(), 2);
1175
1176        let pt = pool.materialize_block_table(&[sa, sb]).unwrap();
1177        assert_eq!(pt.batch, 2);
1178        assert_eq!(pt.max_blocks, 3, "widest session (sa) sets the row width");
1179        assert_eq!(
1180            pt.context_lens,
1181            vec![12u32, 6u32],
1182            "context_len = filled_tokens per session"
1183        );
1184
1185        // Row 0 = sa's three physical ids; row 1 = sb's two ids then zero pad.
1186        let row_a = &pt.block_table[0..3];
1187        let row_b = &pt.block_table[3..6];
1188        assert_eq!(
1189            row_a,
1190            &[a_blocks[0], a_blocks[1], a_blocks[2]],
1191            "sa row = its real physical ids"
1192        );
1193        assert_eq!(
1194            row_b[0..2],
1195            [b_blocks[0], b_blocks[1]],
1196            "sb row = its real physical ids"
1197        );
1198        assert_eq!(row_b[2], 0, "sb's short row is zero-padded");
1199        assert_eq!(pt.block_table_shape().dims(), &[2, 3]);
1200        assert_eq!(pt.context_lens_shape().dims(), &[2]);
1201    }
1202
1203    /// `write_block` mutates the *persistent* pool buffer in place (through the
1204    /// executor's `Op::WriteSlice`), and `read_block` reads it straight back —
1205    /// the device byte-movement round trip. If the WriteSlice produced a fresh
1206    /// output instead of mutating the bound buffer, read_block would still see
1207    /// the zero-init block and this fails.
1208    #[test]
1209    fn write_block_then_read_block_round_trips_in_place() {
1210        let pool = DeviceKvPool::new(geom(2, 8, 4), DType::F32, &Device::cpu()).unwrap();
1211        let block_elems = 4 * 2 * 8; // block_size · Hkv · D
1212        let data: Vec<f32> = (0..block_elems).map(|i| i as f32 + 0.5).collect();
1213
1214        pool.write_block(1, BlockKind::K, 5, &data).unwrap();
1215        let back = pool.read_block(1, BlockKind::K, 5).unwrap();
1216        assert_eq!(
1217            back, data,
1218            "block content survives the device write→read round trip"
1219        );
1220    }
1221
1222    /// rung-2 product: `splice_prefix_shifted` materializes the shared prefix at a
1223    /// block-aligned offset M as FRESH blocks whose K is the cached K delta-rotated
1224    /// by θ·M and whose V is copied verbatim. Verifies the orchestration — correct
1225    /// src→fresh mapping, per-layer loop, the offset threaded as the delta, and V
1226    /// left unrotated — against the Task-1 primitive.
1227    #[test]
1228    fn splice_prefix_shifted_rotates_k_copies_v() {
1229        let g = KvGeometry {
1230            n_layers: 2,
1231            n_kv_heads: 2,
1232            head_dim: 4,
1233            num_blocks: 64,
1234            block_size: 4,
1235            elem_size: DType::F32.size_in_bytes(),
1236        };
1237        let mut pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
1238        let rope_base = 10000.0f64;
1239        let block_elems = 4 * 2 * 4; // block_size · Hkv · D
1240
1241        // Donor: 2 full blocks with deterministic, distinct K and V per (block, layer).
1242        let donor = pool.core_mut().open();
1243        pool.core_mut().append(donor, 8).unwrap();
1244        let d0 = pool.core().resident_block(donor, 0).unwrap();
1245        let d1 = pool.core().resident_block(donor, 1).unwrap();
1246        for (bi, phys) in [(0usize, d0), (1usize, d1)] {
1247            for l in 0..2usize {
1248                let base = (bi * 100 + l * 10) as f32;
1249                let k: Vec<f32> = (0..block_elems).map(|i| (base + i as f32) * 0.01).collect();
1250                let v: Vec<f32> = (0..block_elems)
1251                    .map(|i| (base + i as f32) * 0.02 + 1.0)
1252                    .collect();
1253                pool.write_block(l, BlockKind::K, phys, &k).unwrap();
1254                pool.write_block(l, BlockKind::V, phys, &v).unwrap();
1255            }
1256        }
1257        let pid = pool.core_mut().register_prefix(donor, 2).unwrap();
1258
1259        // Sharer prefilled to a block-aligned offset M = 8.
1260        let dst = pool.core_mut().open();
1261        pool.core_mut().append(dst, 8).unwrap();
1262        let n = pool.splice_prefix_shifted(pid, dst, rope_base).unwrap();
1263        assert_eq!(n, 8, "shared token count = 2 blocks * block_size");
1264
1265        // The shifted prefix is dst's slots 2,3 (after its 2 preamble blocks).
1266        for (dst_slot, src_phys) in [(2usize, d0), (3usize, d1)] {
1267            let fresh = pool.core().resident_block(dst, dst_slot).unwrap();
1268            for l in 0..2usize {
1269                let src_k = pool.read_block(l, BlockKind::K, src_phys).unwrap();
1270                let want = crate::lazy::Tensor::rope_delta_rotate_block_f32(
1271                    &Device::cpu(),
1272                    &src_k,
1273                    rope_base,
1274                    8,
1275                    4,
1276                    2,
1277                    4,
1278                );
1279                let got = pool.read_block(l, BlockKind::K, fresh).unwrap();
1280                let md = want
1281                    .iter()
1282                    .zip(&got)
1283                    .map(|(a, b)| (a - b).abs())
1284                    .fold(0.0f32, f32::max);
1285                assert!(
1286                    md < 1e-6,
1287                    "K delta-rotated by θ·8 (layer {l}, slot {dst_slot}, maxdiff {md})"
1288                );
1289                assert_eq!(
1290                    pool.read_block(l, BlockKind::V, fresh).unwrap(),
1291                    pool.read_block(l, BlockKind::V, src_phys).unwrap(),
1292                    "V copied verbatim (layer {l}, slot {dst_slot})",
1293                );
1294            }
1295        }
1296    }
1297
1298    /// A block write touches ONLY the addressed (layer, kind, phys) — every
1299    /// other block/buffer stays zero-initialized. Catches an offset/stride bug
1300    /// that would smear the write across blocks or the wrong buffer.
1301    #[test]
1302    fn write_block_is_isolated_to_its_layer_kind_and_physical_block() {
1303        let pool = DeviceKvPool::new(geom(2, 8, 4), DType::F32, &Device::cpu()).unwrap();
1304        let block_elems = 4 * 2 * 8;
1305        let ones = vec![1.0_f32; block_elems];
1306        let zeros = vec![0.0_f32; block_elems];
1307
1308        pool.write_block(0, BlockKind::K, 3, &ones).unwrap();
1309
1310        assert_eq!(
1311            pool.read_block(0, BlockKind::K, 3).unwrap(),
1312            ones,
1313            "target block written"
1314        );
1315        assert_eq!(
1316            pool.read_block(0, BlockKind::K, 2).unwrap(),
1317            zeros,
1318            "neighbor block untouched"
1319        );
1320        assert_eq!(
1321            pool.read_block(0, BlockKind::K, 4).unwrap(),
1322            zeros,
1323            "neighbor block untouched"
1324        );
1325        assert_eq!(
1326            pool.read_block(0, BlockKind::V, 3).unwrap(),
1327            zeros,
1328            "same-layer V untouched"
1329        );
1330        assert_eq!(
1331            pool.read_block(1, BlockKind::K, 3).unwrap(),
1332            zeros,
1333            "other-layer K untouched"
1334        );
1335    }
1336
1337    /// Out-of-range physical block / wrong data length are typed errors, never
1338    /// panics or out-of-bounds writes.
1339    #[test]
1340    fn block_io_bounds_are_typed_errors() {
1341        let pool = DeviceKvPool::new(geom(1, 4, 4), DType::F32, &Device::cpu()).unwrap();
1342        let block_elems = 4 * 2 * 8;
1343        assert!(
1344            pool.write_block(0, BlockKind::K, 4, &vec![0.0; block_elems])
1345                .is_err(),
1346            "phys 4 ≥ num_blocks 4"
1347        );
1348        assert!(
1349            pool.read_block(0, BlockKind::K, 9).is_err(),
1350            "read phys 9 out of range"
1351        );
1352        assert!(
1353            pool.write_block(0, BlockKind::K, 0, &[1.0, 2.0]).is_err(),
1354            "short data rejected"
1355        );
1356        assert!(
1357            pool.write_block(3, BlockKind::K, 0, &vec![0.0; block_elems])
1358                .is_err(),
1359            "no layer 3"
1360        );
1361    }
1362
1363    /// PC-1: the dtype-agnostic byte movement core round-trips an f32 pool's
1364    /// bytes byte-identically — the f32 baseline for the byte API. Movement, not
1365    /// compute, so fully CPU-verifiable.
1366    #[test]
1367    fn write_read_block_bytes_round_trips_f32() {
1368        let pool = DeviceKvPool::new(geom(2, 8, 4), DType::F32, &Device::cpu()).unwrap();
1369        let block_elems = 4 * 2 * 8; // block_size · Hkv · D
1370        let vals: Vec<f32> = (0..block_elems).map(|i| i as f32 + 0.5).collect();
1371        let bytes: Vec<u8> = bytemuck::cast_slice::<f32, u8>(&vals).to_vec();
1372
1373        pool.write_block_bytes(1, BlockKind::K, 5, &bytes).unwrap();
1374        let back = pool.read_block_bytes(1, BlockKind::K, 5).unwrap();
1375        assert_eq!(
1376            back, bytes,
1377            "f32 block bytes survive the byte-level round trip"
1378        );
1379    }
1380
1381    /// PC-1: the byte movement core is DTYPE-AGNOSTIC — a bf16 pool's 2-byte
1382    /// elements round-trip byte-identically with NO bf16 compute kernel (pure
1383    /// data movement: dtype-tagged `Op::Const` → `Op::WriteSlice` → `Op::Slice` →
1384    /// `realize::<u8>`). This is the real new capability the CUDA bf16 pool's
1385    /// evict/restore/CoW ride on.
1386    #[test]
1387    fn write_read_block_bytes_round_trips_bf16() {
1388        let mut g = geom(2, 8, 4);
1389        g.elem_size = 2; // bf16 = 2 bytes/elem
1390        let pool = DeviceKvPool::new(g, DType::BF16, &Device::cpu()).unwrap();
1391        let block_elems = 4 * 2 * 8;
1392        // Valid bf16 content (avoid NaN-canonicalization ambiguity), as raw bytes.
1393        let vals: Vec<half::bf16> = (0..block_elems)
1394            .map(|i| half::bf16::from_f32(i as f32 * 0.25 - 3.0))
1395            .collect();
1396        let bytes: Vec<u8> = bytemuck::cast_slice::<half::bf16, u8>(&vals).to_vec();
1397        assert_eq!(bytes.len(), block_elems * 2, "bf16 block = 2 bytes/elem");
1398
1399        pool.write_block_bytes(1, BlockKind::K, 5, &bytes).unwrap();
1400        let back = pool.read_block_bytes(1, BlockKind::K, 5).unwrap();
1401        assert_eq!(
1402            back, bytes,
1403            "bf16 block bytes survive the byte-level round trip"
1404        );
1405    }
1406
1407    /// PC-1: evict→restore is byte-exact on a BF16 pool — the dtype-agnostic
1408    /// movement makes suspend/resume (C-3) real for the CUDA bf16 pool, not just
1409    /// f32. CPU-verifiable (movement, not compute). Pre-PC-1 this errors: evict
1410    /// reads via the f32 `read_block`, which rejects a bf16 pool.
1411    #[test]
1412    fn bf16_pool_evict_restore_is_byte_exact() {
1413        let mut g = geom(2, 8, 4);
1414        g.elem_size = 2;
1415        let mut pool = DeviceKvPool::new(g, DType::BF16, &Device::cpu()).unwrap();
1416        let block_elems = 4 * 2 * 8;
1417        let vals: Vec<half::bf16> = (0..block_elems)
1418            .map(|i| half::bf16::from_f32(i as f32 * 0.5 - 5.0))
1419            .collect();
1420        let bytes: Vec<u8> = bytemuck::cast_slice::<half::bf16, u8>(&vals).to_vec();
1421
1422        let s = pool.core_mut().open();
1423        pool.core_mut().append(s, 1).unwrap(); // one exclusive block resident
1424        let phys = pool.core().resident_block(s, 0).unwrap();
1425        for l in 0..pool.n_layers() {
1426            pool.write_block_bytes(l, BlockKind::K, phys, &bytes)
1427                .unwrap();
1428            pool.write_block_bytes(l, BlockKind::V, phys, &bytes)
1429                .unwrap();
1430        }
1431
1432        let handle = pool.evict(s).unwrap();
1433        pool.restore(s, handle).unwrap();
1434
1435        let phys2 = pool.core().resident_block(s, 0).unwrap();
1436        for l in 0..pool.n_layers() {
1437            assert_eq!(
1438                pool.read_block_bytes(l, BlockKind::K, phys2).unwrap(),
1439                bytes,
1440                "K byte-exact after bf16 evict/restore (layer {l})",
1441            );
1442            assert_eq!(
1443                pool.read_block_bytes(l, BlockKind::V, phys2).unwrap(),
1444                bytes,
1445                "V byte-exact after bf16 evict/restore (layer {l})",
1446            );
1447        }
1448    }
1449
1450    /// Small deterministic pseudo-random f32 fill (mirrors paged_attn_oracle).
1451    fn rand_f32(n: usize, seed: u32) -> Vec<f32> {
1452        let mut s = seed;
1453        (0..n)
1454            .map(|_| {
1455                s = s.wrapping_mul(1103515245).wrapping_add(12345);
1456                ((s >> 16) as u16 as f32 / 65535.0 - 0.5) * 0.5
1457            })
1458            .collect()
1459    }
1460
1461    /// Paged plan-once Task 1: a capacity-PADDED block_table (`[B, CAP]`,
1462    /// 0-padded) plus the `context_len` mask produces byte-identical `paged_attn`
1463    /// output to the L-fitted block_table (`[B, N]`). Padded key positions are
1464    /// `>= context_len`, so the op's mask drives them to `−inf` before softmax
1465    /// (`exp(−inf)=0`, added exactly), leaving the result bit-identical. That
1466    /// means the block_table SHAPE can be a FIXED capacity across decode steps
1467    /// instead of growing with L — the structural prerequisite for one cached plan.
1468    #[test]
1469    fn padded_block_table_paged_attn_matches_unpadded() {
1470        let (hq, hkv, d, block_size, sk) = (2usize, 2usize, 4usize, 4usize, 12usize);
1471        let num_blocks = 8;
1472        let cap = 6; // fixed capacity > the session's 3 resident blocks
1473        let scale = 1.0f32 / (d as f32).sqrt();
1474        let dev = Device::cpu();
1475
1476        let g = KvGeometry {
1477            n_layers: 1,
1478            num_blocks,
1479            block_size,
1480            n_kv_heads: hkv,
1481            head_dim: d,
1482            elem_size: 4,
1483        };
1484        let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
1485        let s = pool.core_mut().open();
1486        pool.core_mut().append(s, sk).unwrap(); // 12 tokens → 3 blocks
1487        let phys = pool.core().session_block_table(s).unwrap();
1488
1489        let k_all = rand_f32(sk * hkv * d, 2);
1490        let v_all = rand_f32(sk * hkv * d, 3);
1491        let q_data = rand_f32(hq * d, 1);
1492        let per_block = block_size * hkv * d;
1493        for (b, &p) in phys.iter().enumerate() {
1494            pool.write_block(
1495                0,
1496                BlockKind::K,
1497                p,
1498                &k_all[b * per_block..(b + 1) * per_block],
1499            )
1500            .unwrap();
1501            pool.write_block(
1502                0,
1503                BlockKind::V,
1504                p,
1505                &v_all[b * per_block..(b + 1) * per_block],
1506            )
1507            .unwrap();
1508        }
1509
1510        let pt_fit = pool.materialize_block_table(&[s]).unwrap();
1511        let pt_pad = pool.materialize_block_table_padded(&[s], cap).unwrap();
1512        assert_eq!(
1513            pt_fit.block_table_shape().dims(),
1514            &[1, phys.len()],
1515            "L-fitted width"
1516        );
1517        assert_eq!(
1518            pt_pad.block_table_shape().dims(),
1519            &[1, cap],
1520            "padded width is the fixed capacity"
1521        );
1522        assert_eq!(
1523            pt_pad.context_lens, pt_fit.context_lens,
1524            "same context_lens"
1525        );
1526
1527        let run = |pt: &PageTableHost| -> Vec<f32> {
1528            let q =
1529                Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
1530            let kc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1531            let vc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1532            let bt = q
1533                .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
1534                .unwrap();
1535            let cl = q
1536                .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
1537                .unwrap();
1538            let out = q
1539                .paged_attn(&kc, &vc, &bt, &cl, None, scale, block_size, None)
1540                .unwrap();
1541            let mut cache = StorageCache::new();
1542            cache.insert(kc.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
1543            cache.insert(vc.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
1544            crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1545                out.graph_handle(),
1546                out.node_id(),
1547                &dev,
1548                cache,
1549            )
1550            .unwrap()
1551        };
1552
1553        assert_eq!(
1554            run(&pt_pad),
1555            run(&pt_fit),
1556            "capacity-padded block_table (mask-neutralized pad) == L-fitted, byte-identical",
1557        );
1558    }
1559
1560    /// Paged plan-once Task 2: writing the new token's K/V via a FLATTENED
1561    /// runtime offset (`linear = phys·block_size + slot`, one dynamic start on
1562    /// the pool's collapsed slot axis) lands byte-identically to the concrete
1563    /// two-axis `write_slice` — for BOTH the `SymEnv` (`write_slice_dyn`) and the
1564    /// device-offset (`write_slice_doff`) arms — and the resulting `paged_attn`
1565    /// output is bit-identical. That makes the K/V write op structurally
1566    /// invariant across decode steps (only the bound offset changes), removing
1567    /// the last per-step graph-shape variability blocking one cached plan.
1568    ///
1569    /// A filler forces a permuted physical layout so the new token's physical
1570    /// block (2) ≠ its slot (2): an offset that dropped the `phys·block_size`
1571    /// term (or the `slot`) would write the WRONG physical block and diverge on
1572    /// both the read-back bytes AND the attention output.
1573    #[test]
1574    fn symbolic_offset_write_matches_concrete_write() {
1575        let (hq, hkv, d, block_size) = (2usize, 2usize, 8usize, 4usize);
1576        let num_blocks = 8;
1577        let sk = 6usize; // context tokens; new token is position 6 → slot 2, block 2
1578        let scale = 1.0f32 / (d as f32).sqrt();
1579        let dev = Device::cpu();
1580        let g = KvGeometry {
1581            n_layers: 1,
1582            num_blocks,
1583            block_size,
1584            n_kv_heads: hkv,
1585            head_dim: d,
1586            elem_size: 4,
1587        };
1588
1589        // Deterministic shared inputs (identical across all three pools).
1590        let q_data = rand_f32(hq * d, 1);
1591        let k_new_data = rand_f32(hkv * d, 4);
1592        let v_new_data = rand_f32(hkv * d, 5);
1593        let q_shape = Shape::from_dims(&[1, hq, 1, d]);
1594        let kv_new_shape = Shape::from_dims(&[1, hkv, 1, d]);
1595
1596        // Build a fresh, identically-prepared pool: a filler grabs physical
1597        // block 0, then the session gets blocks 1,2; append 1 for the new token.
1598        let setup = || -> (DeviceKvPool, PhysBlockId, usize, PageTableHost) {
1599            let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
1600            let filler = pool.core_mut().open();
1601            pool.core_mut().append(filler, block_size).unwrap(); // physical block 0
1602            let s = pool.core_mut().open();
1603            pool.core_mut().append(s, sk).unwrap(); // physical blocks 1,2 (6 tokens)
1604            let tok_pos = sk; // new token position (6)
1605            pool.core_mut().append(s, 1).unwrap(); // filled = 7 (covers position 6)
1606            let phys = pool.core().resident_block(s, tok_pos / block_size).unwrap();
1607            let slot = tok_pos % block_size;
1608            let pt = pool.materialize_block_table(&[s]).unwrap();
1609            (pool, phys, slot, pt)
1610        };
1611
1612        // ---- reference: the concrete two-axis write (build_decode_attn) ----
1613        let (pool_ref, phys, slot, pt) = setup();
1614        assert_eq!(
1615            phys, 2,
1616            "permuted layout — new token's physical block is 2, not 0"
1617        );
1618        assert_eq!(slot, 2, "new token lands at slot 2");
1619        let linear = phys as usize * block_size + slot; // 10
1620        let attn_ref = {
1621            let q = Tensor::from_f32(q_data.clone(), q_shape.clone(), &dev).unwrap();
1622            // k_new/v_new must be siblings of q (build_decode_attn's same-graph
1623            // contract) — Tensor::from_f32 would mint separate graphs.
1624            let k_new = q
1625                .const_f32_like(k_new_data.clone(), kv_new_shape.clone())
1626                .unwrap();
1627            let v_new = q
1628                .const_f32_like(v_new_data.clone(), kv_new_shape.clone())
1629                .unwrap();
1630            let k_ph = q.const_placeholder_like(pool_ref.pool_shape().clone(), DType::F32);
1631            let v_ph = q.const_placeholder_like(pool_ref.pool_shape().clone(), DType::F32);
1632            let bt = q
1633                .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
1634                .unwrap();
1635            let cl = q
1636                .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
1637                .unwrap();
1638            let out = pool_ref
1639                .build_decode_attn(
1640                    &k_ph, &v_ph, &q, &k_new, &v_new, &bt, &cl, phys, slot, scale,
1641                )
1642                .unwrap();
1643            let mut cache = StorageCache::new();
1644            cache.insert(k_ph.node_id(), Arc::clone(pool_ref.k_pool(0).unwrap()));
1645            cache.insert(v_ph.node_id(), Arc::clone(pool_ref.v_pool(0).unwrap()));
1646            crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1647                out.graph_handle(),
1648                out.node_id(),
1649                &dev,
1650                cache,
1651            )
1652            .unwrap()
1653        };
1654        let ref_k_bytes = pool_ref.read_block_bytes(0, BlockKind::K, phys).unwrap();
1655        let ref_v_bytes = pool_ref.read_block_bytes(0, BlockKind::V, phys).unwrap();
1656
1657        // ---- dyn arm: SymEnv-bound flattened offset (write_slice_dyn) ----
1658        let sym = fuel_ir::SymId(0);
1659        let (pool_dyn, _p, _s, pt_dyn) = setup();
1660        let attn_dyn = {
1661            let q = Tensor::from_f32(q_data.clone(), q_shape.clone(), &dev).unwrap();
1662            // k_new/v_new must be siblings of q (build_decode_attn's same-graph
1663            // contract) — Tensor::from_f32 would mint separate graphs.
1664            let k_new = q
1665                .const_f32_like(k_new_data.clone(), kv_new_shape.clone())
1666                .unwrap();
1667            let v_new = q
1668                .const_f32_like(v_new_data.clone(), kv_new_shape.clone())
1669                .unwrap();
1670            let k_ph = q.const_placeholder_like(pool_dyn.pool_shape_flat(), DType::F32);
1671            let v_ph = q.const_placeholder_like(pool_dyn.pool_shape_flat(), DType::F32);
1672            let bt = q
1673                .const_u32_like(pt_dyn.block_table.clone(), pt_dyn.block_table_shape())
1674                .unwrap();
1675            let cl = q
1676                .const_u32_like(pt_dyn.context_lens.clone(), pt_dyn.context_lens_shape())
1677                .unwrap();
1678            let out = pool_dyn
1679                .build_decode_attn_off(&k_ph, &v_ph, &q, &k_new, &v_new, &bt, &cl, None, sym, scale)
1680                .unwrap();
1681            let mut cache = StorageCache::new();
1682            cache.insert(k_ph.node_id(), Arc::clone(pool_dyn.k_pool(0).unwrap()));
1683            cache.insert(v_ph.node_id(), Arc::clone(pool_dyn.v_pool(0).unwrap()));
1684            let mut env = fuel_ir::SymEnv::new();
1685            env.bind(sym, linear).unwrap();
1686            crate::pipelined_bridge::realize_one_as_with_initial_env::<f32>(
1687                out.graph_handle(),
1688                out.node_id(),
1689                &dev,
1690                cache,
1691                &env,
1692            )
1693            .unwrap()
1694        };
1695        assert_eq!(
1696            pool_dyn.read_block_bytes(0, BlockKind::K, phys).unwrap(),
1697            ref_k_bytes,
1698            "dyn write: K block bytes match the concrete write",
1699        );
1700        assert_eq!(
1701            pool_dyn.read_block_bytes(0, BlockKind::V, phys).unwrap(),
1702            ref_v_bytes,
1703            "dyn write: V block bytes match the concrete write",
1704        );
1705        assert_eq!(
1706            attn_dyn, attn_ref,
1707            "dyn arm paged_attn output is bit-identical to concrete"
1708        );
1709
1710        // ---- doff arm: device rank-0 I64 offset (write_slice_doff) ----
1711        let (pool_doff, _p, _s, pt_doff) = setup();
1712        let attn_doff = {
1713            let q = Tensor::from_f32(q_data.clone(), q_shape.clone(), &dev).unwrap();
1714            // k_new/v_new must be siblings of q (build_decode_attn's same-graph
1715            // contract) — Tensor::from_f32 would mint separate graphs.
1716            let k_new = q
1717                .const_f32_like(k_new_data.clone(), kv_new_shape.clone())
1718                .unwrap();
1719            let v_new = q
1720                .const_f32_like(v_new_data.clone(), kv_new_shape.clone())
1721                .unwrap();
1722            let k_ph = q.const_placeholder_like(pool_doff.pool_shape_flat(), DType::F32);
1723            let v_ph = q.const_placeholder_like(pool_doff.pool_shape_flat(), DType::F32);
1724            let bt = q
1725                .const_u32_like(pt_doff.block_table.clone(), pt_doff.block_table_shape())
1726                .unwrap();
1727            let cl = q
1728                .const_u32_like(pt_doff.context_lens.clone(), pt_doff.context_lens_shape())
1729                .unwrap();
1730            let off = q
1731                .const_i64_like(vec![linear as i64], Shape::from_dims(&[]))
1732                .unwrap();
1733            let out = pool_doff
1734                .build_decode_attn_off(
1735                    &k_ph,
1736                    &v_ph,
1737                    &q,
1738                    &k_new,
1739                    &v_new,
1740                    &bt,
1741                    &cl,
1742                    Some(&off),
1743                    sym,
1744                    scale,
1745                )
1746                .unwrap();
1747            let mut cache = StorageCache::new();
1748            cache.insert(k_ph.node_id(), Arc::clone(pool_doff.k_pool(0).unwrap()));
1749            cache.insert(v_ph.node_id(), Arc::clone(pool_doff.v_pool(0).unwrap()));
1750            crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1751                out.graph_handle(),
1752                out.node_id(),
1753                &dev,
1754                cache,
1755            )
1756            .unwrap()
1757        };
1758        assert_eq!(
1759            pool_doff.read_block_bytes(0, BlockKind::K, phys).unwrap(),
1760            ref_k_bytes,
1761            "doff write: K block bytes match the concrete write",
1762        );
1763        assert_eq!(
1764            pool_doff.read_block_bytes(0, BlockKind::V, phys).unwrap(),
1765            ref_v_bytes,
1766            "doff write: V block bytes match the concrete write",
1767        );
1768        assert_eq!(
1769            attn_doff, attn_ref,
1770            "doff arm paged_attn output is bit-identical to concrete"
1771        );
1772    }
1773
1774    /// THE INTEGRATION GATE. A session's K/V, written into the pool's physical
1775    /// blocks via `write_block` at the *core-assigned* (non-identity) physical
1776    /// positions, then read by `Op::PagedAttn` through the *materialized*
1777    /// block_table, must equal a hand-computed dense attention over the logical
1778    /// sequence. This composes every Part-2 piece — core placement + write_block +
1779    /// materialize_block_table + the real fused op bound to the real pool
1780    /// buffers. The physical layout is a permutation (blocks 2,3,4, not 0,1,2),
1781    /// so a paged_attn that ignored the block_table would read the wrong blocks
1782    /// and fail.
1783    #[test]
1784    fn pool_routed_paged_attn_matches_dense_reference_over_a_permuted_layout() {
1785        let (hq, hkv, d, block_size, sk) = (2usize, 2usize, 4usize, 4usize, 12usize);
1786        let num_blocks = 8;
1787        let scale = 1.0f32 / (d as f32).sqrt();
1788        let dev = Device::cpu();
1789
1790        let g = KvGeometry {
1791            n_layers: 1,
1792            num_blocks,
1793            block_size,
1794            n_kv_heads: hkv,
1795            head_dim: d,
1796            elem_size: 4,
1797        };
1798        let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
1799
1800        // Force a NON-identity physical assignment: a filler grabs blocks 0,1;
1801        // the real session then gets blocks 2,3,4.
1802        let filler = pool.core_mut().open();
1803        pool.core_mut().append(filler, 8).unwrap();
1804        let s = pool.core_mut().open();
1805        pool.core_mut().append(s, sk).unwrap(); // 12 tokens → 3 blocks: 2,3,4
1806        let phys = pool.core().session_block_table(s).unwrap();
1807        assert_eq!(phys, vec![2, 3, 4], "permuted layout — not identity 0,1,2");
1808
1809        // Logical K/V for the session, row-major [sk, Hkv, D]; q is [1, Hq, 1, D].
1810        let k_all = rand_f32(sk * hkv * d, 2);
1811        let v_all = rand_f32(sk * hkv * d, 3);
1812        let q_data = rand_f32(hq * d, 1);
1813
1814        // Write each logical block into its core-assigned physical block.
1815        let per_block = block_size * hkv * d;
1816        for (b, &p) in phys.iter().enumerate() {
1817            let kb = &k_all[b * per_block..(b + 1) * per_block];
1818            let vb = &v_all[b * per_block..(b + 1) * per_block];
1819            pool.write_block(0, BlockKind::K, p, kb).unwrap();
1820            pool.write_block(0, BlockKind::V, p, vb).unwrap();
1821        }
1822
1823        // Materialize the page table straight from the core.
1824        let pt = pool.materialize_block_table(&[s]).unwrap();
1825        assert_eq!(pt.context_lens, vec![sk as u32]);
1826
1827        // Build the paged_attn graph, binding the REAL pool buffers as k/v cache.
1828        let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
1829        let kc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1830        let vc = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
1831        let bt = q
1832            .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
1833            .unwrap();
1834        let cl = q
1835            .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
1836            .unwrap();
1837        let out = q
1838            .paged_attn(&kc, &vc, &bt, &cl, None, scale, block_size, None)
1839            .unwrap();
1840
1841        let mut cache = StorageCache::new();
1842        cache.insert(kc.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
1843        cache.insert(vc.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
1844        let got = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
1845            out.graph_handle(),
1846            out.node_id(),
1847            &dev,
1848            cache,
1849        )
1850        .unwrap();
1851
1852        // Dense reference: softmax(scale · q·kᵀ) · v per head, over all sk keys.
1853        let mut want = vec![0.0f32; hq * d];
1854        for h in 0..hq {
1855            let mut scores = vec![0.0f32; sk];
1856            for t in 0..sk {
1857                let mut dot = 0.0f32;
1858                for dd in 0..d {
1859                    dot += q_data[h * d + dd] * k_all[(t * hkv + h) * d + dd];
1860                }
1861                scores[t] = dot * scale;
1862            }
1863            let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1864            let mut denom = 0.0f32;
1865            for sc in scores.iter_mut() {
1866                *sc = (*sc - m).exp();
1867                denom += *sc;
1868            }
1869            for t in 0..sk {
1870                let p = scores[t] / denom;
1871                for dd in 0..d {
1872                    want[h * d + dd] += p * v_all[(t * hkv + h) * d + dd];
1873                }
1874            }
1875        }
1876
1877        assert_eq!(got.len(), want.len());
1878        for (i, (&a, &b)) in got.iter().zip(want.iter()).enumerate() {
1879            let diff = (a - b).abs();
1880            let denom = a.abs().max(b.abs()).max(f32::MIN_POSITIVE);
1881            assert!(
1882                diff < 1e-5 || diff / denom < 1e-5,
1883                "[{i}]: pool={a} dense={b} (abs={diff})"
1884            );
1885        }
1886    }
1887
1888    /// C-3 device round trip: a session's K/V, evicted (bytes captured D2H, its
1889    /// physical blocks returned to the pool) and then restored (bytes written
1890    /// H2D into freshly-assigned blocks), is byte-identical — even after every
1891    /// physical block was scribbled with garbage while the session was
1892    /// suspended. Distinct content per (slot, layer, kind) catches any mixup.
1893    #[test]
1894    fn device_evict_then_restore_is_a_byte_exact_round_trip_across_block_reuse() {
1895        let (n_layers, num_blocks, block_size) = (2usize, 8usize, 4usize);
1896        let mut pool = DeviceKvPool::new(
1897            geom(n_layers, num_blocks, block_size),
1898            DType::F32,
1899            &Device::cpu(),
1900        )
1901        .unwrap();
1902        let block_elems = block_size * 2 * 8; // Hkv=2, D=8 from geom()
1903
1904        // Distinct content per (logical slot, layer, kind).
1905        let content = |slot: usize, layer: usize, kind: BlockKind| -> Vec<f32> {
1906            let base =
1907                (slot * 1000 + layer * 100 + if kind == BlockKind::K { 0 } else { 50 }) as f32;
1908            (0..block_elems).map(|e| base + e as f32 * 0.001).collect()
1909        };
1910
1911        let s = pool.core_mut().open();
1912        pool.core_mut().append(s, 8).unwrap(); // 2 blocks
1913        for slot in 0..2 {
1914            let p = pool.core().resident_block(s, slot).unwrap();
1915            for l in 0..n_layers {
1916                pool.write_block(l, BlockKind::K, p, &content(slot, l, BlockKind::K))
1917                    .unwrap();
1918                pool.write_block(l, BlockKind::V, p, &content(slot, l, BlockKind::V))
1919                    .unwrap();
1920            }
1921        }
1922
1923        let free_before = pool.core().free_blocks();
1924        let handle = pool.evict(s).unwrap();
1925        assert_eq!(
1926            handle.saved_block_count(),
1927            2,
1928            "both exclusive blocks captured"
1929        );
1930        assert_eq!(handle.fidelity(), fuel_kv_pool::Fidelity::Lossy);
1931        assert_eq!(
1932            pool.core().free_blocks(),
1933            free_before + 2,
1934            "2 blocks returned to the pool"
1935        );
1936
1937        // Scribble EVERY physical block — the session's old blocks are free and
1938        // could be reused; restore must overwrite whatever it grabs.
1939        for p in 0..num_blocks as PhysBlockId {
1940            for l in 0..n_layers {
1941                pool.write_block(l, BlockKind::K, p, &vec![-9.0; block_elems])
1942                    .unwrap();
1943                pool.write_block(l, BlockKind::V, p, &vec![-9.0; block_elems])
1944                    .unwrap();
1945            }
1946        }
1947
1948        pool.restore(s, handle).unwrap();
1949        assert_eq!(
1950            pool.core().session_blocks(s),
1951            Some(2),
1952            "session resident again"
1953        );
1954
1955        // Every (slot, layer, kind) reads back its original content.
1956        for slot in 0..2 {
1957            let p = pool.core().resident_block(s, slot).unwrap();
1958            for l in 0..n_layers {
1959                assert_eq!(
1960                    pool.read_block(l, BlockKind::K, p).unwrap(),
1961                    content(slot, l, BlockKind::K),
1962                    "K slot {slot} layer {l} restored byte-exact",
1963                );
1964                assert_eq!(
1965                    pool.read_block(l, BlockKind::V, p).unwrap(),
1966                    content(slot, l, BlockKind::V),
1967                    "V slot {slot} layer {l} restored byte-exact",
1968                );
1969            }
1970        }
1971    }
1972
1973    /// A partial (sub-session) evict of a spliced session moves bytes only for
1974    /// the EXCLUSIVE blocks; the shared prefix stays resident and carries no
1975    /// bytes in the handle (the sharer still references it).
1976    #[test]
1977    fn device_partial_evict_captures_only_exclusive_blocks() {
1978        let mut pool = DeviceKvPool::new(geom(1, 16, 4), DType::F32, &Device::cpu()).unwrap();
1979        let a = pool.core_mut().open();
1980        pool.core_mut().append(a, 9).unwrap(); // 3 blocks
1981        let b = pool.core_mut().open();
1982        pool.core_mut().splice(a, b, 0, 2).unwrap(); // b shares A's first 2 blocks
1983
1984        // Evict all of A: blocks 0,1 are shared (kept), block 2 is exclusive.
1985        let handle = pool.evict(a).unwrap();
1986        assert_eq!(
1987            handle.saved_block_count(),
1988            1,
1989            "only the 1 exclusive block's bytes are captured; the 2 shared stay resident",
1990        );
1991        // B is untouched and still resident.
1992        assert_eq!(pool.core().session_blocks(b), Some(2));
1993    }
1994
1995    /// THE PAGED-DECODE GATE. Decode T tokens ONE AT A TIME into the pool —
1996    /// append a token, write its K/V into the freshly-grown block slot, and
1997    /// attend via `Op::PagedAttn` — and at every step the attention output must
1998    /// equal a dense `softmax(scale·q·kᵀ)·v` over the tokens accumulated so far.
1999    /// This is the multi-step write/accumulate path the storage integration
2000    /// rests on: block-boundary growth (block_size 4, 10 steps → 3 blocks), the
2001    /// running context_len mask, and the in-place pool writes persisting across
2002    /// steps. (2c proved single-shot paged_attn over a pool; this proves the
2003    /// token-by-token decode loop.)
2004    #[test]
2005    fn paged_decode_multistep_matches_dense_reference_across_block_growth() {
2006        let (hq, hkv, d, block_size) = (2usize, 2usize, 4usize, 4usize);
2007        let (num_blocks, n_steps) = (8usize, 10usize);
2008        let scale = 1.0f32 / (d as f32).sqrt();
2009        let dev = Device::cpu();
2010        let g = KvGeometry {
2011            n_layers: 1,
2012            num_blocks,
2013            block_size,
2014            n_kv_heads: hkv,
2015            head_dim: d,
2016            elem_size: 4,
2017        };
2018        let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
2019        let session = pool.core_mut().open();
2020
2021        // Accumulated logical K/V for the dense reference (each entry [Hkv·D]).
2022        let mut k_hist: Vec<Vec<f32>> = Vec::new();
2023        let mut v_hist: Vec<Vec<f32>> = Vec::new();
2024
2025        for t in 0..n_steps {
2026            let q_data = rand_f32(hq * d, 100 + t as u32);
2027            let k_data = rand_f32(hkv * d, 200 + t as u32);
2028            let v_data = rand_f32(hkv * d, 300 + t as u32);
2029            k_hist.push(k_data.clone());
2030            v_hist.push(v_data.clone());
2031
2032            // Grow the session by one token; find where token t lands.
2033            pool.core_mut().append(session, 1).unwrap();
2034            let phys = pool.core().resident_block(session, t / block_size).unwrap();
2035            let slot = t % block_size;
2036            let pt = pool.materialize_block_table(&[session]).unwrap();
2037            assert_eq!(
2038                pt.context_lens,
2039                vec![(t + 1) as u32],
2040                "context_len tracks tokens so far"
2041            );
2042
2043            // Build the paged decode-step graph on q's graph, bind the pool.
2044            let q =
2045                Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
2046            let kph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2047            let vph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2048            let knew = q
2049                .const_f32_like(k_data, Shape::from_dims(&[1, hkv, 1, d]))
2050                .unwrap();
2051            let vnew = q
2052                .const_f32_like(v_data, Shape::from_dims(&[1, hkv, 1, d]))
2053                .unwrap();
2054            let bt = q
2055                .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
2056                .unwrap();
2057            let cl = q
2058                .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
2059                .unwrap();
2060            let attn = pool
2061                .build_decode_attn(&kph, &vph, &q, &knew, &vnew, &bt, &cl, phys, slot, scale)
2062                .unwrap();
2063
2064            let mut cache = StorageCache::new();
2065            cache.insert(kph.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
2066            cache.insert(vph.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
2067            let got = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
2068                attn.graph_handle(),
2069                attn.node_id(),
2070                &dev,
2071                cache,
2072            )
2073            .unwrap();
2074
2075            // Dense reference: q attends to tokens 0..=t.
2076            let mut want = vec![0.0f32; hq * d];
2077            for h in 0..hq {
2078                let mut scores = vec![0.0f32; t + 1];
2079                for (j, sc) in scores.iter_mut().enumerate() {
2080                    let mut dot = 0.0f32;
2081                    for dd in 0..d {
2082                        dot += q_data[h * d + dd] * k_hist[j][h * d + dd];
2083                    }
2084                    *sc = dot * scale;
2085                }
2086                let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2087                let mut denom = 0.0f32;
2088                for sc in scores.iter_mut() {
2089                    *sc = (*sc - m).exp();
2090                    denom += *sc;
2091                }
2092                for (j, &p) in scores.iter().enumerate() {
2093                    let w = p / denom;
2094                    for dd in 0..d {
2095                        want[h * d + dd] += w * v_hist[j][h * d + dd];
2096                    }
2097                }
2098            }
2099
2100            assert_eq!(got.len(), want.len());
2101            for (i, (&a, &b)) in got.iter().zip(want.iter()).enumerate() {
2102                let diff = (a - b).abs();
2103                let den = a.abs().max(b.abs()).max(f32::MIN_POSITIVE);
2104                assert!(
2105                    diff < 1e-5 || diff / den < 1e-5,
2106                    "step {t} [{i}]: paged={a} dense={b} (abs={diff})",
2107                );
2108            }
2109        }
2110        assert_eq!(
2111            pool.core().session_blocks(session),
2112            Some(3),
2113            "10 tokens / block_size 4 = 3 blocks"
2114        );
2115    }
2116
2117    /// PS4c substrate — SPLICE on the paged path (prefix sharing / residual-
2118    /// stream donation). Session A fills blocks with known K/V; session B splices
2119    /// A's blocks (shared, refcount 2); a paged attention for B reads the SHARED
2120    /// blocks via B's materialized block_table and equals a dense reference over
2121    /// A's K/V. Proves the core's refcounted splice composes with the paged read
2122    /// path — B decodes over A's prefix with zero copy.
2123    #[test]
2124    fn spliced_shared_blocks_are_read_by_paged_attention() {
2125        let (hq, hkv, d, block_size) = (2usize, 2usize, 4usize, 4usize);
2126        let (num_blocks, sk) = (8usize, 8usize); // sk=8 → 2 blocks
2127        let scale = 1.0f32 / (d as f32).sqrt();
2128        let dev = Device::cpu();
2129        let g = KvGeometry {
2130            n_layers: 1,
2131            num_blocks,
2132            block_size,
2133            n_kv_heads: hkv,
2134            head_dim: d,
2135            elem_size: 4,
2136        };
2137        let mut pool = DeviceKvPool::new(g, DType::F32, &dev).unwrap();
2138
2139        // A fills 2 blocks with known K/V.
2140        let a = pool.core_mut().open();
2141        pool.core_mut().append(a, sk).unwrap();
2142        let k_all = rand_f32(sk * hkv * d, 2);
2143        let v_all = rand_f32(sk * hkv * d, 3);
2144        let per_block = block_size * hkv * d;
2145        let a_blocks = pool.core().session_block_table(a).unwrap();
2146        for (bi, &p) in a_blocks.iter().enumerate() {
2147            pool.write_block(
2148                0,
2149                BlockKind::K,
2150                p,
2151                &k_all[bi * per_block..(bi + 1) * per_block],
2152            )
2153            .unwrap();
2154            pool.write_block(
2155                0,
2156                BlockKind::V,
2157                p,
2158                &v_all[bi * per_block..(bi + 1) * per_block],
2159            )
2160            .unwrap();
2161        }
2162
2163        // B splices A's 2 blocks — shared, not copied.
2164        let b = pool.core_mut().open();
2165        pool.core_mut().splice(a, b, 0, 2).unwrap();
2166        assert_eq!(
2167            pool.core().session_block_table(b).unwrap(),
2168            a_blocks,
2169            "B shares A's exact blocks"
2170        );
2171        assert_eq!(
2172            pool.core().block_refcount(a_blocks[0]),
2173            2,
2174            "shared → refcount 2"
2175        );
2176
2177        // A query for B attends over the shared prefix (context_len = B's filled = 8).
2178        let pt = pool.materialize_block_table(&[b]).unwrap();
2179        assert_eq!(
2180            pt.context_lens,
2181            vec![sk as u32],
2182            "spliced prefix length carried to B"
2183        );
2184        let q_data = rand_f32(hq * d, 1);
2185        let q = Tensor::from_f32(q_data.clone(), Shape::from_dims(&[1, hq, 1, d]), &dev).unwrap();
2186        let kph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2187        let vph = q.const_placeholder_like(pool.pool_shape().clone(), DType::F32);
2188        let bt = q
2189            .const_u32_like(pt.block_table.clone(), pt.block_table_shape())
2190            .unwrap();
2191        let cl = q
2192            .const_u32_like(pt.context_lens.clone(), pt.context_lens_shape())
2193            .unwrap();
2194        let out = q
2195            .paged_attn(&kph, &vph, &bt, &cl, None, scale, block_size, None)
2196            .unwrap();
2197        let mut cache = StorageCache::new();
2198        cache.insert(kph.node_id(), Arc::clone(pool.k_pool(0).unwrap()));
2199        cache.insert(vph.node_id(), Arc::clone(pool.v_pool(0).unwrap()));
2200        let got = crate::pipelined_bridge::realize_one_as_with_initial::<f32>(
2201            out.graph_handle(),
2202            out.node_id(),
2203            &dev,
2204            cache,
2205        )
2206        .unwrap();
2207
2208        // Dense reference: q attends to A's sk shared tokens.
2209        let mut want = vec![0.0f32; hq * d];
2210        for h in 0..hq {
2211            let mut scores = vec![0.0f32; sk];
2212            for (t, sc) in scores.iter_mut().enumerate() {
2213                let mut dot = 0.0f32;
2214                for dd in 0..d {
2215                    dot += q_data[h * d + dd] * k_all[(t * hkv + h) * d + dd];
2216                }
2217                *sc = dot * scale;
2218            }
2219            let m = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
2220            let mut denom = 0.0f32;
2221            for sc in scores.iter_mut() {
2222                *sc = (*sc - m).exp();
2223                denom += *sc;
2224            }
2225            for (t, &p) in scores.iter().enumerate() {
2226                let w = p / denom;
2227                for dd in 0..d {
2228                    want[h * d + dd] += w * v_all[(t * hkv + h) * d + dd];
2229                }
2230            }
2231        }
2232        for (i, (&x, &y)) in got.iter().zip(want.iter()).enumerate() {
2233            let diff = (x - y).abs();
2234            let den = x.abs().max(y.abs()).max(f32::MIN_POSITIVE);
2235            assert!(
2236                diff < 1e-5 || diff / den < 1e-5,
2237                "[{i}]: spliced={x} dense={y} (abs={diff})"
2238            );
2239        }
2240    }
2241
2242    /// The copy-on-write guard (regression for the adversarial-verification
2243    /// finding): `ensure_writable_block` on a SHARED (spliced) block breaks the
2244    /// share into a fresh private block holding a COPY of the donor's bytes,
2245    /// drops the donor's refcount, and a subsequent write to the new block does
2246    /// NOT touch the donor. Without this, decoding a spliced session corrupts its
2247    /// co-sharers.
2248    #[test]
2249    fn ensure_writable_block_copy_on_writes_a_shared_block() {
2250        let (hkv, d, block_size) = (2usize, 4usize, 4usize);
2251        let g = KvGeometry {
2252            n_layers: 1,
2253            num_blocks: 8,
2254            block_size,
2255            n_kv_heads: hkv,
2256            head_dim: d,
2257            elem_size: 4,
2258        };
2259        let mut pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
2260        let block_elems = block_size * hkv * d;
2261
2262        // A fills one block with known K.
2263        let a = pool.core_mut().open();
2264        pool.core_mut().append(a, block_size).unwrap();
2265        let a_phys = pool.core().resident_block(a, 0).unwrap();
2266        let a_data: Vec<f32> = (0..block_elems).map(|i| i as f32 + 1.0).collect();
2267        pool.write_block(0, BlockKind::K, a_phys, &a_data).unwrap();
2268
2269        // B splices A's block (shared, refcount 2).
2270        let b = pool.core_mut().open();
2271        pool.core_mut().splice(a, b, 0, 1).unwrap();
2272        assert_eq!(
2273            pool.core().resident_block(b, 0),
2274            Some(a_phys),
2275            "B shares A's block"
2276        );
2277        assert_eq!(pool.core().block_refcount(a_phys), 2);
2278
2279        // CoW: B gets a fresh private block holding a COPY of A's bytes.
2280        let b_phys = pool.ensure_writable_block(b, 0).unwrap();
2281        assert_ne!(b_phys, a_phys, "B got a fresh private block");
2282        assert_eq!(
2283            pool.core().block_refcount(a_phys),
2284            1,
2285            "A's block no longer shared"
2286        );
2287        assert_eq!(
2288            pool.read_block(0, BlockKind::K, b_phys).unwrap(),
2289            a_data,
2290            "B's block is a copy of A's"
2291        );
2292
2293        // Writing B's copy must not touch A's block.
2294        pool.write_block(0, BlockKind::K, b_phys, &vec![-1.0; block_elems])
2295            .unwrap();
2296        assert_eq!(
2297            pool.read_block(0, BlockKind::K, a_phys).unwrap(),
2298            a_data,
2299            "A's block intact after B writes its own copy",
2300        );
2301
2302        // An exclusive block is returned unchanged (no needless copy).
2303        assert_eq!(
2304            pool.ensure_writable_block(a, 0).unwrap(),
2305            a_phys,
2306            "exclusive block: no CoW"
2307        );
2308    }
2309
2310    /// Materializing over an unknown or externalized session is a typed error
2311    /// (delegates the core's resident-only guard), never a panic.
2312    #[test]
2313    fn materialize_rejects_a_non_resident_session() {
2314        let mut pool = DeviceKvPool::new(geom(1, 16, 4), DType::F32, &Device::cpu()).unwrap();
2315        let s = pool.core_mut().open();
2316        pool.core_mut().append(s, 9).unwrap();
2317        pool.core_mut().evict(s).unwrap(); // slots now externalized
2318        let err = pool.materialize_block_table(&[s]);
2319        assert_eq!(err, Err(KvAllocError::SessionNotResident { slot: 0 }));
2320    }
2321
2322    /// A decode-shaped graph for sizing tests: a weights leaf plus a probe pool
2323    /// (`2 * n_layers` K/V leaves of `probe_blocks` blocks) each read by one op.
2324    /// Returns the residency report and the probe geometry.
2325    fn probe_report(weight_elems: usize, probe_blocks: usize) -> (ResidencyReport, KvGeometry) {
2326        use fuel_dispatch::residency::ResidencyPlanner;
2327        let probe = geom(/*n_layers*/ 2, probe_blocks, /*block_size*/ 4);
2328        let mut g = Graph::new();
2329        let mut push = |op: Op, inputs: Vec<NodeId>, dims: &[usize]| {
2330            g.push(Node {
2331                op,
2332                inputs,
2333                shape: Shape::from_dims(dims),
2334                dtype: DType::F32,
2335            })
2336        };
2337        let w = push(Op::Const, vec![], &[weight_elems]);
2338        let mut roots = vec![push(Op::Neg, vec![w], &[weight_elems])];
2339        for _ in 0..(2 * probe.n_layers) {
2340            let dims = [
2341                probe_blocks,
2342                probe.block_size,
2343                probe.n_kv_heads,
2344                probe.head_dim,
2345            ];
2346            let leaf = push(Op::Const, vec![], &dims);
2347            roots.push(push(Op::Neg, vec![leaf], &dims));
2348        }
2349        (ResidencyPlanner::analyze(&g, &roots), probe)
2350    }
2351
2352    /// The sized geometry is exactly what the arithmetic says: floor of
2353    /// (usable - (peak - probe pool)) / block bytes, and the pool it describes
2354    /// really allocates with its free list at that size.
2355    #[test]
2356    fn fit_geometry_from_residency_matches_the_arithmetic_and_allocates() {
2357        let (report, probe) = probe_report(/*weight_elems*/ 4096, /*probe_blocks*/ 2);
2358        let non_kv = report.peak_bytes as u64 - probe.block_bytes() * probe.num_blocks as u64;
2359        let template = geom(2, 1, 4);
2360        let usable = non_kv + 10 * template.block_bytes() + 123; // room for 10 blocks + slack
2361        let g = fit_geometry_from_residency(template, &report, &probe, usable, 1.0).unwrap();
2362        assert_eq!(
2363            g.num_blocks, 10,
2364            "10 whole blocks fit, the 123 B slack is not a block"
2365        );
2366        let pool = DeviceKvPool::new(g, DType::F32, &Device::cpu()).unwrap();
2367        assert_eq!(pool.core().free_blocks(), 10);
2368    }
2369
2370    /// Retained sabotage sibling: forgetting to remove the probe pool from the
2371    /// peak double-counts it and under-sizes the pool. The gate above must be
2372    /// able to tell, so assert the double-counted answer is strictly smaller.
2373    #[test]
2374    fn forgetting_the_probe_pool_undersizes_the_pool() {
2375        let (report, probe) = probe_report(4096, 2);
2376        let template = geom(2, 1, 4);
2377        let usable =
2378            (report.peak_bytes as u64 - probe.block_bytes() * 2) + 10 * template.block_bytes();
2379        let right = fit_geometry_from_residency(template, &report, &probe, usable, 1.0).unwrap();
2380        let naive = template
2381            .fit_to_budget(&KvBudget {
2382                device_bytes: usable,
2383                utilization: 1.0,
2384                non_kv_resident_bytes: report.peak_bytes as u64,
2385            })
2386            .unwrap();
2387        assert!(
2388            naive.num_blocks < right.num_blocks,
2389            "{naive:?} vs {right:?}"
2390        );
2391    }
2392
2393    /// A device too small for the graph's own residency is a typed error, not a
2394    /// zero-block pool.
2395    #[test]
2396    fn fit_geometry_from_residency_errors_when_the_graph_alone_does_not_fit() {
2397        let (report, probe) = probe_report(1 << 20, 2);
2398        let r = fit_geometry_from_residency(geom(2, 1, 4), &report, &probe, 1024, 1.0);
2399        assert!(r.is_err());
2400    }
2401}