1use rlx_ir::quant::QuantScheme;
15use std::collections::HashMap;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::{Arc, OnceLock, RwLock};
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
20struct DequantKey {
21 w_off: u64,
22 len: u64,
23 k: u32,
24 n: u32,
25 scheme: u8,
26 sample: u64,
28}
29
30fn weight_bytes_sample(w_bytes: &[u8]) -> u64 {
31 use std::hash::{Hash, Hasher};
34 let mut hasher = std::collections::hash_map::DefaultHasher::new();
35 let n = w_bytes.len();
36 n.hash(&mut hasher);
37 if n == 0 {
38 return hasher.finish();
39 }
40 const CHUNK: usize = 64;
41 let head = CHUNK.min(n);
42 w_bytes[..head].hash(&mut hasher);
43 if n > CHUNK * 2 {
44 let mid = n / 2;
45 let half = CHUNK / 2;
46 w_bytes[mid - half..mid + half].hash(&mut hasher);
47 w_bytes[n - CHUNK..].hash(&mut hasher);
48 } else if n > CHUNK {
49 w_bytes[n - head..].hash(&mut hasher);
50 }
51 hasher.finish()
52}
53
54fn scheme_tag(scheme: QuantScheme) -> u8 {
55 match scheme {
56 QuantScheme::GgufQ4K => 1,
57 QuantScheme::GgufQ5K => 2,
58 QuantScheme::GgufQ6K => 3,
59 QuantScheme::GgufQ8K => 4,
60 QuantScheme::GgufQ4_0 => 5,
61 QuantScheme::GgufQ8_0 => 6,
62 QuantScheme::GgufQ2K => 7,
63 QuantScheme::GgufQ3K => 8,
64 QuantScheme::GgufIQ4NL => 9,
65 QuantScheme::GgufIQ4XS => 10,
66 QuantScheme::GgufIQ2XXS => 11,
67 QuantScheme::GgufIQ2XS => 12,
68 QuantScheme::GgufIQ2S => 13,
69 QuantScheme::GgufIQ3XXS => 14,
70 QuantScheme::GgufIQ3S => 15,
71 QuantScheme::GgufIQ1S => 16,
72 QuantScheme::GgufIQ1M => 17,
73 QuantScheme::GgufTQ1_0 => 18,
74 QuantScheme::GgufTQ2_0 => 19,
75 QuantScheme::GgufMXFP4 => 20,
76 QuantScheme::GgufNVFP4 => 21,
77 QuantScheme::GgufQ4_1 => 22,
78 QuantScheme::GgufQ5_0 => 23,
79 QuantScheme::GgufQ5_1 => 24,
80 QuantScheme::GgufQ1_0 => 25,
81 QuantScheme::GgufQ2_0 => 26,
82 QuantScheme::GgufFV5 => 27,
83 QuantScheme::GgufFV5B => 28,
84 QuantScheme::GgufG8_0 => 29,
85 QuantScheme::GgufPtq1_0 => 30,
86 _ => 255,
87 }
88}
89
90fn dequant_gguf_serial(w_bytes: &[u8], n_elems: usize, scheme: QuantScheme) -> Vec<f32> {
91 match scheme {
92 QuantScheme::GgufQ4K => rlx_gguf::dequant_q4_k(w_bytes, n_elems),
93 QuantScheme::GgufQ5K => rlx_gguf::dequant_q5_k(w_bytes, n_elems),
94 QuantScheme::GgufQ6K => rlx_gguf::dequant_q6_k(w_bytes, n_elems),
95 QuantScheme::GgufQ8K => rlx_gguf::dequant_q8_k(w_bytes, n_elems),
96 QuantScheme::GgufQ2K => rlx_gguf::dequant_q2_k(w_bytes, n_elems),
97 QuantScheme::GgufQ3K => rlx_gguf::dequant_q3_k(w_bytes, n_elems),
98 QuantScheme::GgufQ4_0 => rlx_gguf::dequant_q4_0(w_bytes, n_elems),
99 QuantScheme::GgufQ4_1 => rlx_gguf::dequant_q4_1(w_bytes, n_elems),
100 QuantScheme::GgufQ5_0 => rlx_gguf::dequant_q5_0(w_bytes, n_elems),
101 QuantScheme::GgufQ5_1 => rlx_gguf::dequant_q5_1(w_bytes, n_elems),
102 QuantScheme::GgufQ8_0 => rlx_gguf::dequant_q8_0(w_bytes, n_elems),
103 QuantScheme::GgufIQ4NL => rlx_gguf::iq_dequant::dequant_iq4_nl(w_bytes, n_elems),
104 QuantScheme::GgufIQ4XS => rlx_gguf::iq_dequant::dequant_iq4_xs(w_bytes, n_elems),
105 QuantScheme::GgufIQ2XXS => rlx_gguf::iq_dequant::dequant_iq2_xxs(w_bytes, n_elems),
106 QuantScheme::GgufIQ2XS => rlx_gguf::iq_dequant::dequant_iq2_xs(w_bytes, n_elems),
107 QuantScheme::GgufIQ2S => rlx_gguf::iq_dequant::dequant_iq2_s(w_bytes, n_elems),
108 QuantScheme::GgufIQ3XXS => rlx_gguf::iq_dequant::dequant_iq3_xxs(w_bytes, n_elems),
109 QuantScheme::GgufIQ3S => rlx_gguf::iq_dequant::dequant_iq3_s(w_bytes, n_elems),
110 QuantScheme::GgufIQ1S => rlx_gguf::iq_dequant::dequant_iq1_s(w_bytes, n_elems),
111 QuantScheme::GgufIQ1M => rlx_gguf::iq_dequant::dequant_iq1_m(w_bytes, n_elems),
112 QuantScheme::GgufTQ1_0 => rlx_gguf::tq_dequant::dequant_tq1_0(w_bytes, n_elems),
113 QuantScheme::GgufTQ2_0 => rlx_gguf::tq_dequant::dequant_tq2_0(w_bytes, n_elems),
114 QuantScheme::GgufMXFP4 => rlx_gguf::mx_dequant::dequant_mxfp4(w_bytes, n_elems),
115 QuantScheme::GgufNVFP4 => rlx_gguf::mx_dequant::dequant_nvfp4(w_bytes, n_elems),
116 QuantScheme::GgufQ1_0 => rlx_gguf::q1_dequant::dequant_q1_0(w_bytes, n_elems),
117 QuantScheme::GgufQ2_0 => rlx_gguf::q2_dequant::dequant_q2_0(w_bytes, n_elems),
118 QuantScheme::GgufFV5 => rlx_gguf::fv5_dequant::dequant_fv5(w_bytes, n_elems),
119 QuantScheme::GgufFV5B => rlx_gguf::fv5_dequant::dequant_fv5b(w_bytes, n_elems),
120 QuantScheme::GgufG8_0 => rlx_gguf::g8_dequant::dequant_g8_0(w_bytes, n_elems),
121 QuantScheme::GgufPtq1_0 => rlx_gguf::ptq1_dequant::dequant_ptq1_0(w_bytes, n_elems),
122 other => panic!("dequant_cache: unsupported GGUF scheme {other:?}"),
123 }
124 .expect("GGUF dequant failed")
125}
126
127fn dequant_gguf(w_bytes: &[u8], k: usize, n: usize, scheme: QuantScheme) -> Vec<f32> {
137 use rayon::prelude::*;
138 const QK_K: usize = 256;
139 let n_elems = k * n;
140 let parallel = n_elems >= QK_K * 512
141 && n_elems.is_multiple_of(QK_K)
142 && matches!(scheme, QuantScheme::GgufQ4K | QuantScheme::GgufQ6K)
143 && rlx_ir::env::var("RLX_DEQUANT_PARALLEL").as_deref() != Some("0");
144 if parallel {
145 let block_bytes = scheme.gguf_block_bytes() as usize;
146 let n_blocks = n_elems / QK_K;
147 if w_bytes.len() >= n_blocks * block_bytes {
148 let wb = &w_bytes[..n_blocks * block_bytes];
149 let mut out = vec![0f32; n_elems];
150 let threads = rayon::current_num_threads().max(1);
154 let blocks_per_chunk = n_blocks.div_ceil(threads).max(1);
155 out.par_chunks_mut(blocks_per_chunk * QK_K)
156 .enumerate()
157 .for_each(|(ci, out_chunk)| {
158 let base = ci * blocks_per_chunk;
159 for j in 0..(out_chunk.len() / QK_K) {
160 let bb = &wb[(base + j) * block_bytes..(base + j + 1) * block_bytes];
161 let arr: &mut [f32; QK_K] = (&mut out_chunk[j * QK_K..(j + 1) * QK_K])
162 .try_into()
163 .expect("256-elem block");
164 match scheme {
165 QuantScheme::GgufQ4K => rlx_gguf::dequant_q4_k_block(bb, arr),
166 QuantScheme::GgufQ6K => rlx_gguf::dequant_q6_k_block(bb, arr),
167 _ => unreachable!(),
168 }
169 }
170 });
171 return out;
172 }
173 }
174 dequant_gguf_serial(w_bytes, n_elems, scheme)
175}
176
177struct Entry {
179 data: Arc<[f32]>,
180 bytes: usize,
181 last: AtomicU64,
184}
185
186#[derive(Default)]
188struct DequantCache {
189 map: HashMap<DequantKey, Entry>,
190 total_bytes: usize,
191}
192
193impl DequantCache {
194 fn insert_evict(
198 &mut self,
199 key: DequantKey,
200 arc: &Arc<[f32]>,
201 bytes: usize,
202 max: Option<usize>,
203 ) {
204 let tick = TICK.fetch_add(1, Ordering::Relaxed);
205 if let Some(prev) = self.map.insert(
206 key,
207 Entry {
208 data: Arc::clone(arc),
209 bytes,
210 last: AtomicU64::new(tick),
211 },
212 ) {
213 self.total_bytes -= prev.bytes;
215 }
216 self.total_bytes += bytes;
217
218 let Some(max) = max else { return };
219 while self.total_bytes > max && self.map.len() > 1 {
220 let victim = self
221 .map
222 .iter()
223 .filter(|(k, _)| **k != key)
224 .min_by_key(|(_, e)| e.last.load(Ordering::Relaxed))
225 .map(|(k, _)| *k);
226 match victim {
227 Some(v) => {
228 if let Some(e) = self.map.remove(&v) {
229 self.total_bytes -= e.bytes;
230 EVICTED_BYTES.fetch_add(e.bytes as u64, Ordering::Relaxed);
231 }
232 }
233 None => break,
234 }
235 }
236 }
237}
238
239static CACHE: OnceLock<RwLock<DequantCache>> = OnceLock::new();
240static TICK: AtomicU64 = AtomicU64::new(0);
242
243static EVICTED_BYTES: AtomicU64 = AtomicU64::new(0);
245
246static THRASH_PURGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
248
249pub fn cache_thrashing() -> bool {
262 match max_cache_bytes() {
263 Some(max) => EVICTED_BYTES.load(Ordering::Relaxed) > (max as u64) / 4,
269 None => false,
270 }
271}
272
273fn cache_enabled() -> bool {
274 !matches!(
275 rlx_ir::env::var("RLX_DEQUANT_CACHE").as_deref(),
276 Some("0") | Some("false") | Some("off")
277 )
278}
279
280fn physical_ram_bytes() -> Option<usize> {
292 static RAM: OnceLock<Option<usize>> = OnceLock::new();
293 *RAM.get_or_init(physical_ram_bytes_uncached)
294}
295
296fn physical_ram_bytes_uncached() -> Option<usize> {
297 #[cfg(target_os = "linux")]
298 {
299 let text = std::fs::read_to_string("/proc/meminfo").ok()?;
300 for line in text.lines() {
301 if let Some(rest) = line.strip_prefix("MemTotal:") {
302 let kb: usize = rest.split_whitespace().next()?.parse().ok()?;
303 return Some(kb * 1024);
304 }
305 }
306 None
307 }
308 #[cfg(target_vendor = "apple")]
309 {
310 let out = std::process::Command::new("/usr/sbin/sysctl")
314 .args(["-n", "hw.memsize"])
315 .output()
316 .ok()?;
317 String::from_utf8(out.stdout).ok()?.trim().parse().ok()
318 }
319 #[cfg(not(any(target_os = "linux", target_vendor = "apple")))]
320 {
321 None
322 }
323}
324
325const DEFAULT_CACHE_RAM_FRACTION: f64 = 0.25;
334
335fn max_cache_bytes() -> Option<usize> {
346 match rlx_ir::env::var("RLX_DEQUANT_CACHE_MAX_BYTES") {
347 Some(s) => {
348 let s = s.trim().to_ascii_lowercase();
349 if s == "0" || s == "unbounded" || s == "off" {
350 return None;
351 }
352 s.parse::<usize>().ok().filter(|&b| b > 0)
353 }
354 None => physical_ram_bytes()
355 .map(|ram| (ram as f64 * DEFAULT_CACHE_RAM_FRACTION) as usize)
356 .filter(|&b| b > 0),
357 }
358}
359
360pub fn gguf_weight_f32(
365 w_off: usize,
366 w_bytes: &[u8],
367 k: usize,
368 n: usize,
369 scheme: QuantScheme,
370) -> Arc<[f32]> {
371 if !cache_enabled() {
372 return Arc::from(dequant_gguf(w_bytes, k, n, scheme).into_boxed_slice());
373 }
374 let key = DequantKey {
375 w_off: w_off as u64,
376 len: w_bytes.len() as u64,
377 k: k as u32,
378 n: n as u32,
379 scheme: scheme_tag(scheme),
380 sample: weight_bytes_sample(w_bytes),
381 };
382 let cache = CACHE.get_or_init(|| RwLock::new(DequantCache::default()));
383 if let Some(hit) = {
385 let r = cache.read().expect("dequant cache poisoned");
386 r.map.get(&key).map(|e| {
387 e.last
388 .store(TICK.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
389 Arc::clone(&e.data)
390 })
391 } {
392 return hit;
393 }
394 let dense = dequant_gguf(w_bytes, k, n, scheme);
395 const MAX_CACHED_ELEMS: usize = 256 * 1024 * 1024; if dense.len() > MAX_CACHED_ELEMS {
399 return Arc::from(dense.into_boxed_slice());
400 }
401 let bytes = std::mem::size_of_val(&dense[..]);
402 let arc: Arc<[f32]> = Arc::from(dense.into_boxed_slice());
403 if cache_thrashing() {
409 if !THRASH_PURGED.swap(true, Ordering::Relaxed) {
414 let mut w = cache.write().expect("dequant cache poisoned");
415 w.map.clear();
416 w.total_bytes = 0;
417 }
418 return arc;
419 }
420 {
421 let mut w = cache.write().expect("dequant cache poisoned");
422 if let Some(e) = w.map.get(&key) {
424 e.last
425 .store(TICK.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
426 return Arc::clone(&e.data);
427 }
428 w.insert_evict(key, &arc, bytes, max_cache_bytes());
429 }
430 arc
431}
432
433pub fn clear_dequant_cache() {
435 if let Some(c) = CACHE.get() {
436 let mut w = c.write().expect("dequant cache poisoned");
437 w.map.clear();
438 w.total_bytes = 0;
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn gguf_dequant_cache_hits_on_second_lookup() {
448 clear_dequant_cache();
449 const QK_K: usize = 256;
450 let mut packed = Vec::new();
451 packed.extend_from_slice(&half::f16::from_f32(1.0).to_le_bytes());
452 packed.extend_from_slice(&half::f16::from_f32(1.0).to_le_bytes());
453 let mut scales = [0u8; 12];
454 for s in &mut scales[0..4] {
455 *s = 0x01;
456 }
457 packed.extend_from_slice(&scales);
458 packed.extend(std::iter::repeat_n(0x77u8, QK_K / 2));
459 let k = 256;
460 let n = 1;
461 let w_off = 4096;
462 let a = gguf_weight_f32(w_off, &packed, k, n, QuantScheme::GgufQ4K);
463 let b = gguf_weight_f32(w_off, &packed, k, n, QuantScheme::GgufQ4K);
464 assert!(Arc::ptr_eq(&a, &b), "same offset + bytes should hit");
465 let other_off = gguf_weight_f32(w_off + 999, &packed, k, n, QuantScheme::GgufQ4K);
467 assert!(
468 !Arc::ptr_eq(&a, &other_off),
469 "different w_off should miss even with identical bytes"
470 );
471 let mut other = packed.clone();
472 other[0] ^= 0x01;
473 let c = gguf_weight_f32(w_off, &other, k, n, QuantScheme::GgufQ4K);
474 assert!(
475 !Arc::ptr_eq(&a, &c),
476 "different bytes at same offset should miss"
477 );
478 }
479
480 #[test]
481 fn gguf_dequant_cache_q4_1_q5_hits() {
482 clear_dequant_cache();
483 let w: Vec<f32> = (0..512).map(|i| (i as f32 * 0.01).sin()).collect();
484 for (scheme, ggml) in [
485 (QuantScheme::GgufQ4_1, rlx_gguf::GgmlType::Q4_1),
486 (QuantScheme::GgufQ5_0, rlx_gguf::GgmlType::Q5_0),
487 (QuantScheme::GgufQ5_1, rlx_gguf::GgmlType::Q5_1),
488 ] {
489 let packed = rlx_gguf::quantize(&w, ggml).unwrap();
490 let a = gguf_weight_f32(0, &packed, 32, 16, scheme);
491 let b = gguf_weight_f32(0, &packed, 32, 16, scheme);
492 assert!(Arc::ptr_eq(&a, &b), "cache hit expected for {scheme:?}");
493 }
494 }
495
496 fn key(off: u64) -> DequantKey {
497 DequantKey {
498 w_off: off,
499 len: 100,
500 k: 10,
501 n: 10,
502 scheme: 1,
503 sample: off,
504 }
505 }
506
507 #[test]
508 fn lru_evicts_to_budget() {
509 let mut c = DequantCache::default();
510 let data: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
511 let bytes = 400; c.insert_evict(key(1), &data, bytes, Some(1000));
514 c.insert_evict(key(2), &data, bytes, Some(1000));
515 assert_eq!(c.map.len(), 2);
516 assert_eq!(c.total_bytes, 800);
517 c.insert_evict(key(3), &data, bytes, Some(1000));
518 assert!(
519 c.total_bytes <= 1000,
520 "total {} exceeds budget",
521 c.total_bytes
522 );
523 assert_eq!(c.map.len(), 2);
524 assert!(
525 !c.map.contains_key(&key(1)),
526 "LRU (key 1) should be evicted"
527 );
528 assert!(
529 c.map.contains_key(&key(3)),
530 "just-inserted key must be kept"
531 );
532 }
533
534 #[test]
535 fn recent_hit_survives_eviction() {
536 let mut c = DequantCache::default();
537 let data: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
538 let bytes = 400;
539 c.insert_evict(key(1), &data, bytes, Some(1000));
540 c.insert_evict(key(2), &data, bytes, Some(1000));
541 c.map
543 .get(&key(1))
544 .unwrap()
545 .last
546 .store(TICK.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
547 c.insert_evict(key(3), &data, bytes, Some(1000));
548 assert!(
549 c.map.contains_key(&key(1)),
550 "recently touched key must survive"
551 );
552 assert!(
553 !c.map.contains_key(&key(2)),
554 "untouched LRU (key 2) evicted"
555 );
556 }
557
558 #[test]
559 fn entry_larger_than_budget_still_lands() {
560 let mut c = DequantCache::default();
561 let big: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
562 c.insert_evict(key(1), &big, 400, Some(100)); assert_eq!(c.map.len(), 1);
564 assert!(c.map.contains_key(&key(1)));
565 }
566
567 #[test]
568 fn unbounded_when_no_budget() {
569 let mut c = DequantCache::default();
570 let data: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
571 for i in 0..10 {
572 c.insert_evict(key(i), &data, 400, None);
573 }
574 assert_eq!(c.map.len(), 10, "no budget → nothing evicted");
575 assert_eq!(c.total_bytes, 4000);
576 }
577}