1use rlx_ir::Shape;
27use std::collections::HashMap;
28use std::sync::Arc;
29use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct WeightHandle(u64);
36
37impl WeightHandle {
38 pub fn id(self) -> u64 {
39 self.0
40 }
41}
42
43#[derive(Debug, Clone)]
46pub enum WeightKind {
47 Base,
49 LoraAdapter { adapter: String },
53 TiedAlias { target: WeightHandle },
57}
58
59#[derive(Debug)]
61pub struct WeightEntry {
62 pub name: String,
63 pub shape: Shape,
64 pub kind: WeightKind,
65 pub bytes: Arc<[u8]>,
69 pub refs: AtomicUsize,
74}
75
76pub struct WeightRegistry {
80 by_name: HashMap<String, WeightHandle>,
81 by_handle: HashMap<u64, Arc<WeightEntry>>,
82 next_id: AtomicU64,
83}
84
85impl WeightRegistry {
86 pub fn new() -> Self {
87 Self {
88 by_name: HashMap::new(),
89 by_handle: HashMap::new(),
90 next_id: AtomicU64::new(0),
91 }
92 }
93
94 fn alloc_id(&self) -> u64 {
95 self.next_id.fetch_add(1, Ordering::Relaxed)
96 }
97
98 pub fn register(
102 &mut self,
103 name: impl Into<String>,
104 shape: Shape,
105 bytes: Arc<[u8]>,
106 kind: WeightKind,
107 ) -> WeightHandle {
108 let name = name.into();
109 if let Some(&h) = self.by_name.get(&name) {
110 return h;
111 }
112 let id = self.alloc_id();
113 let h = WeightHandle(id);
114 let entry = Arc::new(WeightEntry {
115 name: name.clone(),
116 shape,
117 kind,
118 bytes,
119 refs: AtomicUsize::new(0),
120 });
121 self.by_name.insert(name, h);
122 self.by_handle.insert(id, entry);
123 h
124 }
125
126 pub fn lookup(&self, name: &str) -> Option<WeightHandle> {
128 self.by_name.get(name).copied()
129 }
130
131 pub fn get(&self, handle: WeightHandle) -> Option<&Arc<WeightEntry>> {
133 let entry = self.by_handle.get(&handle.0)?;
134 if let WeightKind::TiedAlias { target } = entry.kind {
135 return self.by_handle.get(&target.0);
136 }
137 Some(entry)
138 }
139
140 pub fn pin(&self, handle: WeightHandle) -> Option<usize> {
142 let entry = self.by_handle.get(&handle.0)?;
143 Some(entry.refs.fetch_add(1, Ordering::Relaxed) + 1)
144 }
145
146 pub fn release(&self, handle: WeightHandle) -> Option<usize> {
150 let entry = self.by_handle.get(&handle.0)?;
151 let prev = entry.refs.fetch_sub(1, Ordering::Relaxed);
152 debug_assert!(prev >= 1, "release on a zero-refcount entry");
153 Some(prev - 1)
154 }
155
156 pub fn unregister(&mut self, handle: WeightHandle) -> Option<String> {
159 let entry = self.by_handle.remove(&handle.0)?;
160 debug_assert_eq!(
161 entry.refs.load(Ordering::Relaxed),
162 0,
163 "unregister on a still-referenced entry: refs={}",
164 entry.refs.load(Ordering::Relaxed)
165 );
166 self.by_name.remove(&entry.name);
167 Some(entry.name.clone())
168 }
169
170 pub fn total_bytes(&self) -> usize {
174 self.by_handle
175 .values()
176 .filter(|e| !matches!(e.kind, WeightKind::TiedAlias { .. }))
177 .map(|e| e.bytes.len())
178 .sum()
179 }
180
181 pub fn lora_adapter_handles(&self, adapter: &str) -> Vec<WeightHandle> {
184 let mut v: Vec<WeightHandle> = self
185 .by_handle
186 .iter()
187 .filter_map(|(&id, e)| match &e.kind {
188 WeightKind::LoraAdapter { adapter: a } if a == adapter => Some(WeightHandle(id)),
189 _ => None,
190 })
191 .collect();
192 v.sort_by_key(|h| h.0);
193 v
194 }
195
196 pub fn lora_adapter_names(&self) -> Vec<String> {
198 let mut s: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
199 for e in self.by_handle.values() {
200 if let WeightKind::LoraAdapter { adapter } = &e.kind {
201 s.insert(adapter.clone());
202 }
203 }
204 s.into_iter().collect()
205 }
206
207 pub fn len(&self) -> usize {
208 self.by_handle.len()
209 }
210 pub fn is_empty(&self) -> bool {
211 self.by_handle.is_empty()
212 }
213}
214
215impl Default for WeightRegistry {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use rlx_ir::DType;
225
226 fn shape() -> Shape {
227 Shape::new(&[8, 8], DType::F32)
228 }
229 fn bytes(n: usize) -> Arc<[u8]> {
230 vec![0u8; n].into()
231 }
232
233 #[test]
234 fn register_and_lookup() {
235 let mut r = WeightRegistry::new();
236 let h = r.register("w", shape(), bytes(256), WeightKind::Base);
237 assert_eq!(r.lookup("w"), Some(h));
238 let entry = r.get(h).unwrap();
239 assert_eq!(entry.name, "w");
240 assert_eq!(entry.bytes.len(), 256);
241 }
242
243 #[test]
244 fn register_is_idempotent() {
245 let mut r = WeightRegistry::new();
246 let h1 = r.register("w", shape(), bytes(128), WeightKind::Base);
247 let h2 = r.register("w", shape(), bytes(999), WeightKind::Base);
248 assert_eq!(h1, h2);
250 assert_eq!(r.get(h1).unwrap().bytes.len(), 128);
251 }
252
253 #[test]
254 fn pin_release_balance() {
255 let mut r = WeightRegistry::new();
256 let h = r.register("w", shape(), bytes(64), WeightKind::Base);
257 assert_eq!(r.pin(h), Some(1));
258 assert_eq!(r.pin(h), Some(2));
259 assert_eq!(r.release(h), Some(1));
260 assert_eq!(r.release(h), Some(0));
261 assert_eq!(r.unregister(h), Some("w".to_string()));
263 assert!(r.lookup("w").is_none());
264 }
265
266 #[test]
267 fn tied_alias_resolves_to_target() {
268 let mut r = WeightRegistry::new();
269 let target = r.register("embed", shape(), bytes(128), WeightKind::Base);
270 let alias = r.register(
271 "lm_head",
272 shape(),
273 bytes(0), WeightKind::TiedAlias { target },
275 );
276 let resolved = r.get(alias).unwrap();
277 assert_eq!(resolved.name, "embed");
278 assert_eq!(resolved.bytes.len(), 128);
279 }
280
281 #[test]
282 fn total_bytes_skips_aliases() {
283 let mut r = WeightRegistry::new();
284 let _t = r.register("embed", shape(), bytes(100), WeightKind::Base);
285 let _a = r.register(
286 "lm_head",
287 shape(),
288 bytes(0),
289 WeightKind::TiedAlias {
290 target: r.lookup("embed").unwrap(),
291 },
292 );
293 let _b = r.register("ffn", shape(), bytes(200), WeightKind::Base);
294 assert_eq!(r.total_bytes(), 300, "alias must not double-count");
295 }
296
297 #[test]
298 fn lora_grouping() {
299 let mut r = WeightRegistry::new();
300 let _b = r.register("ffn", shape(), bytes(100), WeightKind::Base);
301 r.register(
302 "ffn.lora.a",
303 shape(),
304 bytes(8),
305 WeightKind::LoraAdapter {
306 adapter: "code".into(),
307 },
308 );
309 r.register(
310 "ffn.lora.b",
311 shape(),
312 bytes(8),
313 WeightKind::LoraAdapter {
314 adapter: "code".into(),
315 },
316 );
317 r.register(
318 "attn.lora.a",
319 shape(),
320 bytes(8),
321 WeightKind::LoraAdapter {
322 adapter: "math".into(),
323 },
324 );
325
326 let mut adapters = r.lora_adapter_names();
327 adapters.sort();
328 assert_eq!(adapters, vec!["code".to_string(), "math".to_string()]);
329
330 let code_handles = r.lora_adapter_handles("code");
331 assert_eq!(code_handles.len(), 2);
332 }
333}