metrale_storage/
scratch_pool.rs1use anyhow::{Result, bail};
18use std::collections::{HashMap, VecDeque};
19
20use crate::cuda_min::DeviceBuffer;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct ResidentKey {
24 pub layer: u32,
25 pub block: u32,
26}
27
28#[derive(Clone, Copy, Debug)]
29pub struct ScratchDims {
30 pub num_slots: u32,
31 pub num_kv_heads: u16,
32 pub group_stride: u64,
34}
35
36impl ScratchDims {
37 pub fn slot_bytes(&self) -> usize {
38 (2 * self.num_kv_heads as u64 * self.group_stride) as usize
39 }
40 pub fn pool_bytes(&self) -> usize {
41 self.num_slots as usize * self.slot_bytes()
42 }
43}
44
45pub struct ScratchPool {
46 dims: ScratchDims,
47 pool: DeviceBuffer,
48 residents: Vec<Option<ResidentKey>>,
49 lookup: HashMap<ResidentKey, u32>,
50 free_list: VecDeque<u32>,
51}
52
53impl ScratchPool {
54 pub fn new(dims: ScratchDims) -> Result<Self> {
55 if dims.num_slots == 0 {
56 bail!("ScratchPool requires at least one slot");
57 }
58 let pool = DeviceBuffer::new(dims.pool_bytes())?;
59 let residents = vec![None; dims.num_slots as usize];
60 let free_list = (0..dims.num_slots).collect();
61 Ok(Self {
62 dims,
63 pool,
64 residents,
65 lookup: HashMap::new(),
66 free_list,
67 })
68 }
69
70 pub fn dims(&self) -> ScratchDims {
71 self.dims
72 }
73 pub fn pool_dev_ptr(&self) -> u64 {
74 self.pool.ptr
75 }
76 pub fn slot_dev_ptr(&self, slot: u32) -> u64 {
77 self.pool.ptr + (slot as u64) * (self.dims.slot_bytes() as u64)
78 }
79 pub fn slot_k_ptr(&self, slot: u32, kv_head: u16) -> u64 {
80 self.slot_dev_ptr(slot) + (kv_head as u64) * self.dims.group_stride
81 }
82 pub fn slot_v_ptr(&self, slot: u32, kv_head: u16) -> u64 {
83 self.slot_dev_ptr(slot)
84 + (self.dims.num_kv_heads as u64 + kv_head as u64) * self.dims.group_stride
85 }
86
87 pub fn lookup(&self, key: ResidentKey) -> Option<u32> {
88 self.lookup.get(&key).copied()
89 }
90
91 pub fn invalidate(&mut self, key: ResidentKey) {
95 if let Some(slot) = self.lookup.remove(&key) {
96 self.residents[slot as usize] = None;
97 self.free_list.push_back(slot);
98 }
99 }
100
101 pub fn capacity(&self) -> u32 {
102 self.dims.num_slots
103 }
104 pub fn free_count(&self) -> u32 {
105 self.free_list.len() as u32
106 }
107
108 pub fn assign(&mut self, key: ResidentKey, evict_candidates: &[u32]) -> Result<u32> {
113 if let Some(&slot) = self.lookup.get(&key) {
114 return Ok(slot);
115 }
116 let slot = match self.free_list.pop_front() {
117 Some(s) => s,
118 None => {
119 let mut chosen = None;
122 for &c in evict_candidates {
123 if self
124 .residents
125 .get(c as usize)
126 .and_then(|r| r.as_ref())
127 .is_some()
128 {
129 chosen = Some(c);
130 break;
131 }
132 }
133 let s = chosen.ok_or_else(|| {
134 anyhow::anyhow!("no slot available and no eviction candidate is resident")
135 })?;
136 if let Some(prev) = self.residents[s as usize].take() {
137 self.lookup.remove(&prev);
138 }
139 s
140 }
141 };
142 self.residents[slot as usize] = Some(key);
143 self.lookup.insert(key, slot);
144 Ok(slot)
145 }
146
147 pub fn residents(&self) -> Vec<(u32, ResidentKey)> {
149 self.residents
150 .iter()
151 .enumerate()
152 .filter_map(|(i, r)| r.map(|k| (i as u32, k)))
153 .collect()
154 }
155
156 pub fn clear(&mut self) {
157 self.lookup.clear();
158 for r in self.residents.iter_mut() {
159 *r = None;
160 }
161 self.free_list.clear();
162 for s in 0..self.dims.num_slots {
163 self.free_list.push_back(s);
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn ctx() -> crate::cuda_min::CudaCtx {
173 crate::cuda_min::CudaCtx::new(0).expect("cuda init")
174 }
175
176 #[test]
177 #[ignore = "requires GPU"]
178 fn assign_and_lookup() {
179 let _ctx = ctx();
180 let mut pool = ScratchPool::new(ScratchDims {
181 num_slots: 4,
182 num_kv_heads: 2,
183 group_stride: 4096,
184 })
185 .unwrap();
186 let k0 = ResidentKey { layer: 0, block: 7 };
187 let s0 = pool.assign(k0, &[]).unwrap();
188 assert_eq!(pool.lookup(k0), Some(s0));
189 let s0_again = pool.assign(k0, &[]).unwrap();
190 assert_eq!(s0, s0_again);
191 for b in 8..11 {
192 pool.assign(ResidentKey { layer: 0, block: b }, &[])
193 .unwrap();
194 }
195 assert_eq!(pool.free_count(), 0);
196 let evicted = pool
197 .assign(
198 ResidentKey {
199 layer: 0,
200 block: 99,
201 },
202 &[s0],
203 )
204 .unwrap();
205 assert_eq!(evicted, s0);
206 assert_eq!(pool.lookup(k0), None);
207 }
208
209 #[test]
210 #[ignore = "requires GPU"]
211 fn slot_pointer_layout() {
212 let _ctx = ctx();
213 let pool = ScratchPool::new(ScratchDims {
214 num_slots: 2,
215 num_kv_heads: 4,
216 group_stride: 4096,
217 })
218 .unwrap();
219 let base = pool.pool_dev_ptr();
220 assert_eq!(pool.slot_dev_ptr(0), base);
221 assert_eq!(pool.slot_dev_ptr(1), base + 8 * 4096);
222 assert_eq!(pool.slot_k_ptr(0, 2), base + 2 * 4096);
223 assert_eq!(pool.slot_v_ptr(0, 2), base + (4 + 2) * 4096);
224 }
225}