use rlx_ir::quant::QuantScheme;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock, RwLock};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
struct DequantKey {
w_off: u64,
len: u64,
k: u32,
n: u32,
scheme: u8,
sample: u64,
}
fn weight_bytes_sample(w_bytes: &[u8]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
let n = w_bytes.len();
n.hash(&mut hasher);
if n == 0 {
return hasher.finish();
}
const CHUNK: usize = 64;
let head = CHUNK.min(n);
w_bytes[..head].hash(&mut hasher);
if n > CHUNK * 2 {
let mid = n / 2;
let half = CHUNK / 2;
w_bytes[mid - half..mid + half].hash(&mut hasher);
w_bytes[n - CHUNK..].hash(&mut hasher);
} else if n > CHUNK {
w_bytes[n - head..].hash(&mut hasher);
}
hasher.finish()
}
fn scheme_tag(scheme: QuantScheme) -> u8 {
match scheme {
QuantScheme::GgufQ4K => 1,
QuantScheme::GgufQ5K => 2,
QuantScheme::GgufQ6K => 3,
QuantScheme::GgufQ8K => 4,
QuantScheme::GgufQ4_0 => 5,
QuantScheme::GgufQ8_0 => 6,
QuantScheme::GgufQ2K => 7,
QuantScheme::GgufQ3K => 8,
QuantScheme::GgufIQ4NL => 9,
QuantScheme::GgufIQ4XS => 10,
QuantScheme::GgufIQ2XXS => 11,
QuantScheme::GgufIQ2XS => 12,
QuantScheme::GgufIQ2S => 13,
QuantScheme::GgufIQ3XXS => 14,
QuantScheme::GgufIQ3S => 15,
QuantScheme::GgufIQ1S => 16,
QuantScheme::GgufIQ1M => 17,
QuantScheme::GgufTQ1_0 => 18,
QuantScheme::GgufTQ2_0 => 19,
QuantScheme::GgufMXFP4 => 20,
QuantScheme::GgufNVFP4 => 21,
QuantScheme::GgufQ4_1 => 22,
QuantScheme::GgufQ5_0 => 23,
QuantScheme::GgufQ5_1 => 24,
QuantScheme::GgufQ1_0 => 25,
QuantScheme::GgufQ2_0 => 26,
QuantScheme::GgufFV5 => 27,
QuantScheme::GgufFV5B => 28,
QuantScheme::GgufG8_0 => 29,
QuantScheme::GgufPtq1_0 => 30,
_ => 255,
}
}
fn dequant_gguf_serial(w_bytes: &[u8], n_elems: usize, scheme: QuantScheme) -> Vec<f32> {
match scheme {
QuantScheme::GgufQ4K => rlx_gguf::dequant_q4_k(w_bytes, n_elems),
QuantScheme::GgufQ5K => rlx_gguf::dequant_q5_k(w_bytes, n_elems),
QuantScheme::GgufQ6K => rlx_gguf::dequant_q6_k(w_bytes, n_elems),
QuantScheme::GgufQ8K => rlx_gguf::dequant_q8_k(w_bytes, n_elems),
QuantScheme::GgufQ2K => rlx_gguf::dequant_q2_k(w_bytes, n_elems),
QuantScheme::GgufQ3K => rlx_gguf::dequant_q3_k(w_bytes, n_elems),
QuantScheme::GgufQ4_0 => rlx_gguf::dequant_q4_0(w_bytes, n_elems),
QuantScheme::GgufQ4_1 => rlx_gguf::dequant_q4_1(w_bytes, n_elems),
QuantScheme::GgufQ5_0 => rlx_gguf::dequant_q5_0(w_bytes, n_elems),
QuantScheme::GgufQ5_1 => rlx_gguf::dequant_q5_1(w_bytes, n_elems),
QuantScheme::GgufQ8_0 => rlx_gguf::dequant_q8_0(w_bytes, n_elems),
QuantScheme::GgufIQ4NL => rlx_gguf::iq_dequant::dequant_iq4_nl(w_bytes, n_elems),
QuantScheme::GgufIQ4XS => rlx_gguf::iq_dequant::dequant_iq4_xs(w_bytes, n_elems),
QuantScheme::GgufIQ2XXS => rlx_gguf::iq_dequant::dequant_iq2_xxs(w_bytes, n_elems),
QuantScheme::GgufIQ2XS => rlx_gguf::iq_dequant::dequant_iq2_xs(w_bytes, n_elems),
QuantScheme::GgufIQ2S => rlx_gguf::iq_dequant::dequant_iq2_s(w_bytes, n_elems),
QuantScheme::GgufIQ3XXS => rlx_gguf::iq_dequant::dequant_iq3_xxs(w_bytes, n_elems),
QuantScheme::GgufIQ3S => rlx_gguf::iq_dequant::dequant_iq3_s(w_bytes, n_elems),
QuantScheme::GgufIQ1S => rlx_gguf::iq_dequant::dequant_iq1_s(w_bytes, n_elems),
QuantScheme::GgufIQ1M => rlx_gguf::iq_dequant::dequant_iq1_m(w_bytes, n_elems),
QuantScheme::GgufTQ1_0 => rlx_gguf::tq_dequant::dequant_tq1_0(w_bytes, n_elems),
QuantScheme::GgufTQ2_0 => rlx_gguf::tq_dequant::dequant_tq2_0(w_bytes, n_elems),
QuantScheme::GgufMXFP4 => rlx_gguf::mx_dequant::dequant_mxfp4(w_bytes, n_elems),
QuantScheme::GgufNVFP4 => rlx_gguf::mx_dequant::dequant_nvfp4(w_bytes, n_elems),
QuantScheme::GgufQ1_0 => rlx_gguf::q1_dequant::dequant_q1_0(w_bytes, n_elems),
QuantScheme::GgufQ2_0 => rlx_gguf::q2_dequant::dequant_q2_0(w_bytes, n_elems),
QuantScheme::GgufFV5 => rlx_gguf::fv5_dequant::dequant_fv5(w_bytes, n_elems),
QuantScheme::GgufFV5B => rlx_gguf::fv5_dequant::dequant_fv5b(w_bytes, n_elems),
QuantScheme::GgufG8_0 => rlx_gguf::g8_dequant::dequant_g8_0(w_bytes, n_elems),
QuantScheme::GgufPtq1_0 => rlx_gguf::ptq1_dequant::dequant_ptq1_0(w_bytes, n_elems),
other => panic!("dequant_cache: unsupported GGUF scheme {other:?}"),
}
.expect("GGUF dequant failed")
}
fn dequant_gguf(w_bytes: &[u8], k: usize, n: usize, scheme: QuantScheme) -> Vec<f32> {
use rayon::prelude::*;
const QK_K: usize = 256;
let n_elems = k * n;
let parallel = n_elems >= QK_K * 512
&& n_elems.is_multiple_of(QK_K)
&& matches!(scheme, QuantScheme::GgufQ4K | QuantScheme::GgufQ6K)
&& rlx_ir::env::var("RLX_DEQUANT_PARALLEL").as_deref() != Some("0");
if parallel {
let block_bytes = scheme.gguf_block_bytes() as usize;
let n_blocks = n_elems / QK_K;
if w_bytes.len() >= n_blocks * block_bytes {
let wb = &w_bytes[..n_blocks * block_bytes];
let mut out = vec![0f32; n_elems];
let threads = rayon::current_num_threads().max(1);
let blocks_per_chunk = n_blocks.div_ceil(threads).max(1);
out.par_chunks_mut(blocks_per_chunk * QK_K)
.enumerate()
.for_each(|(ci, out_chunk)| {
let base = ci * blocks_per_chunk;
for j in 0..(out_chunk.len() / QK_K) {
let bb = &wb[(base + j) * block_bytes..(base + j + 1) * block_bytes];
let arr: &mut [f32; QK_K] = (&mut out_chunk[j * QK_K..(j + 1) * QK_K])
.try_into()
.expect("256-elem block");
match scheme {
QuantScheme::GgufQ4K => rlx_gguf::dequant_q4_k_block(bb, arr),
QuantScheme::GgufQ6K => rlx_gguf::dequant_q6_k_block(bb, arr),
_ => unreachable!(),
}
}
});
return out;
}
}
dequant_gguf_serial(w_bytes, n_elems, scheme)
}
struct Entry {
data: Arc<[f32]>,
bytes: usize,
last: AtomicU64,
}
#[derive(Default)]
struct DequantCache {
map: HashMap<DequantKey, Entry>,
total_bytes: usize,
}
impl DequantCache {
fn insert_evict(
&mut self,
key: DequantKey,
arc: &Arc<[f32]>,
bytes: usize,
max: Option<usize>,
) {
let tick = TICK.fetch_add(1, Ordering::Relaxed);
if let Some(prev) = self.map.insert(
key,
Entry {
data: Arc::clone(arc),
bytes,
last: AtomicU64::new(tick),
},
) {
self.total_bytes -= prev.bytes;
}
self.total_bytes += bytes;
let Some(max) = max else { return };
while self.total_bytes > max && self.map.len() > 1 {
let victim = self
.map
.iter()
.filter(|(k, _)| **k != key)
.min_by_key(|(_, e)| e.last.load(Ordering::Relaxed))
.map(|(k, _)| *k);
match victim {
Some(v) => {
if let Some(e) = self.map.remove(&v) {
self.total_bytes -= e.bytes;
EVICTED_BYTES.fetch_add(e.bytes as u64, Ordering::Relaxed);
}
}
None => break,
}
}
}
}
static CACHE: OnceLock<RwLock<DequantCache>> = OnceLock::new();
static TICK: AtomicU64 = AtomicU64::new(0);
static EVICTED_BYTES: AtomicU64 = AtomicU64::new(0);
static THRASH_PURGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub fn cache_thrashing() -> bool {
match max_cache_bytes() {
Some(max) => EVICTED_BYTES.load(Ordering::Relaxed) > (max as u64) / 4,
None => false,
}
}
fn cache_enabled() -> bool {
!matches!(
rlx_ir::env::var("RLX_DEQUANT_CACHE").as_deref(),
Some("0") | Some("false") | Some("off")
)
}
fn physical_ram_bytes() -> Option<usize> {
static RAM: OnceLock<Option<usize>> = OnceLock::new();
*RAM.get_or_init(physical_ram_bytes_uncached)
}
fn physical_ram_bytes_uncached() -> Option<usize> {
#[cfg(target_os = "linux")]
{
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let kb: usize = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb * 1024);
}
}
None
}
#[cfg(target_vendor = "apple")]
{
let out = std::process::Command::new("/usr/sbin/sysctl")
.args(["-n", "hw.memsize"])
.output()
.ok()?;
String::from_utf8(out.stdout).ok()?.trim().parse().ok()
}
#[cfg(not(any(target_os = "linux", target_vendor = "apple")))]
{
None
}
}
const DEFAULT_CACHE_RAM_FRACTION: f64 = 0.25;
fn max_cache_bytes() -> Option<usize> {
match rlx_ir::env::var("RLX_DEQUANT_CACHE_MAX_BYTES") {
Some(s) => {
let s = s.trim().to_ascii_lowercase();
if s == "0" || s == "unbounded" || s == "off" {
return None;
}
s.parse::<usize>().ok().filter(|&b| b > 0)
}
None => physical_ram_bytes()
.map(|ram| (ram as f64 * DEFAULT_CACHE_RAM_FRACTION) as usize)
.filter(|&b| b > 0),
}
}
pub fn gguf_weight_f32(
w_off: usize,
w_bytes: &[u8],
k: usize,
n: usize,
scheme: QuantScheme,
) -> Arc<[f32]> {
if !cache_enabled() {
return Arc::from(dequant_gguf(w_bytes, k, n, scheme).into_boxed_slice());
}
let key = DequantKey {
w_off: w_off as u64,
len: w_bytes.len() as u64,
k: k as u32,
n: n as u32,
scheme: scheme_tag(scheme),
sample: weight_bytes_sample(w_bytes),
};
let cache = CACHE.get_or_init(|| RwLock::new(DequantCache::default()));
if let Some(hit) = {
let r = cache.read().expect("dequant cache poisoned");
r.map.get(&key).map(|e| {
e.last
.store(TICK.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
Arc::clone(&e.data)
})
} {
return hit;
}
let dense = dequant_gguf(w_bytes, k, n, scheme);
const MAX_CACHED_ELEMS: usize = 256 * 1024 * 1024; if dense.len() > MAX_CACHED_ELEMS {
return Arc::from(dense.into_boxed_slice());
}
let bytes = std::mem::size_of_val(&dense[..]);
let arc: Arc<[f32]> = Arc::from(dense.into_boxed_slice());
if cache_thrashing() {
if !THRASH_PURGED.swap(true, Ordering::Relaxed) {
let mut w = cache.write().expect("dequant cache poisoned");
w.map.clear();
w.total_bytes = 0;
}
return arc;
}
{
let mut w = cache.write().expect("dequant cache poisoned");
if let Some(e) = w.map.get(&key) {
e.last
.store(TICK.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
return Arc::clone(&e.data);
}
w.insert_evict(key, &arc, bytes, max_cache_bytes());
}
arc
}
pub fn clear_dequant_cache() {
if let Some(c) = CACHE.get() {
let mut w = c.write().expect("dequant cache poisoned");
w.map.clear();
w.total_bytes = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gguf_dequant_cache_hits_on_second_lookup() {
clear_dequant_cache();
const QK_K: usize = 256;
let mut packed = Vec::new();
packed.extend_from_slice(&half::f16::from_f32(1.0).to_le_bytes());
packed.extend_from_slice(&half::f16::from_f32(1.0).to_le_bytes());
let mut scales = [0u8; 12];
for s in &mut scales[0..4] {
*s = 0x01;
}
packed.extend_from_slice(&scales);
packed.extend(std::iter::repeat_n(0x77u8, QK_K / 2));
let k = 256;
let n = 1;
let w_off = 4096;
let a = gguf_weight_f32(w_off, &packed, k, n, QuantScheme::GgufQ4K);
let b = gguf_weight_f32(w_off, &packed, k, n, QuantScheme::GgufQ4K);
assert!(Arc::ptr_eq(&a, &b), "same offset + bytes should hit");
let other_off = gguf_weight_f32(w_off + 999, &packed, k, n, QuantScheme::GgufQ4K);
assert!(
!Arc::ptr_eq(&a, &other_off),
"different w_off should miss even with identical bytes"
);
let mut other = packed.clone();
other[0] ^= 0x01;
let c = gguf_weight_f32(w_off, &other, k, n, QuantScheme::GgufQ4K);
assert!(
!Arc::ptr_eq(&a, &c),
"different bytes at same offset should miss"
);
}
#[test]
fn gguf_dequant_cache_q4_1_q5_hits() {
clear_dequant_cache();
let w: Vec<f32> = (0..512).map(|i| (i as f32 * 0.01).sin()).collect();
for (scheme, ggml) in [
(QuantScheme::GgufQ4_1, rlx_gguf::GgmlType::Q4_1),
(QuantScheme::GgufQ5_0, rlx_gguf::GgmlType::Q5_0),
(QuantScheme::GgufQ5_1, rlx_gguf::GgmlType::Q5_1),
] {
let packed = rlx_gguf::quantize(&w, ggml).unwrap();
let a = gguf_weight_f32(0, &packed, 32, 16, scheme);
let b = gguf_weight_f32(0, &packed, 32, 16, scheme);
assert!(Arc::ptr_eq(&a, &b), "cache hit expected for {scheme:?}");
}
}
fn key(off: u64) -> DequantKey {
DequantKey {
w_off: off,
len: 100,
k: 10,
n: 10,
scheme: 1,
sample: off,
}
}
#[test]
fn lru_evicts_to_budget() {
let mut c = DequantCache::default();
let data: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
let bytes = 400; c.insert_evict(key(1), &data, bytes, Some(1000));
c.insert_evict(key(2), &data, bytes, Some(1000));
assert_eq!(c.map.len(), 2);
assert_eq!(c.total_bytes, 800);
c.insert_evict(key(3), &data, bytes, Some(1000));
assert!(
c.total_bytes <= 1000,
"total {} exceeds budget",
c.total_bytes
);
assert_eq!(c.map.len(), 2);
assert!(
!c.map.contains_key(&key(1)),
"LRU (key 1) should be evicted"
);
assert!(
c.map.contains_key(&key(3)),
"just-inserted key must be kept"
);
}
#[test]
fn recent_hit_survives_eviction() {
let mut c = DequantCache::default();
let data: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
let bytes = 400;
c.insert_evict(key(1), &data, bytes, Some(1000));
c.insert_evict(key(2), &data, bytes, Some(1000));
c.map
.get(&key(1))
.unwrap()
.last
.store(TICK.fetch_add(1, Ordering::Relaxed), Ordering::Relaxed);
c.insert_evict(key(3), &data, bytes, Some(1000));
assert!(
c.map.contains_key(&key(1)),
"recently touched key must survive"
);
assert!(
!c.map.contains_key(&key(2)),
"untouched LRU (key 2) evicted"
);
}
#[test]
fn entry_larger_than_budget_still_lands() {
let mut c = DequantCache::default();
let big: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
c.insert_evict(key(1), &big, 400, Some(100)); assert_eq!(c.map.len(), 1);
assert!(c.map.contains_key(&key(1)));
}
#[test]
fn unbounded_when_no_budget() {
let mut c = DequantCache::default();
let data: Arc<[f32]> = Arc::from(vec![0f32; 100].into_boxed_slice());
for i in 0..10 {
c.insert_evict(key(i), &data, 400, None);
}
assert_eq!(c.map.len(), 10, "no budget → nothing evicted");
assert_eq!(c.total_bytes, 4000);
}
}