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