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