qualia_core_db/inference/cuda_lane/
weight_cache.rs1use std::alloc::Layout;
9use std::collections::HashMap;
10use std::sync::{Mutex, OnceLock};
11
12use crate::wgsl_forge::dispatch::{ensure_cuda_runtime_path, gemm_f32_tc_reduced};
13
14const MAX_WEIGHT_ENTRIES: usize = 24;
16pub const MAX_DENSE_ELEMS: usize = 48 * 1024 * 1024;
18
19const HUGE_PAGE_ALIGN: usize = 2 * 1024 * 1024;
21
22struct HugePageF32 {
25 ptr: *mut f32,
26 len: usize,
27 cap_bytes: usize,
29}
30
31unsafe impl Send for HugePageF32 {}
35unsafe impl Sync for HugePageF32 {}
36
37impl HugePageF32 {
38 fn new(len: usize) -> Self {
39 let byte_len = len * std::mem::size_of::<f32>();
40 let cap_bytes = byte_len;
41 let layout = Layout::from_size_align(cap_bytes, HUGE_PAGE_ALIGN).expect("huge-page layout");
42 let ptr = unsafe { std::alloc::alloc(layout) as *mut f32 };
43 if ptr.is_null() {
44 std::alloc::handle_alloc_error(layout);
45 }
46 #[cfg(target_os = "linux")]
48 unsafe {
49 let ret = libc::madvise(ptr as *mut libc::c_void, cap_bytes, libc::MADV_HUGEPAGE);
50 if ret != 0 {
51 log::debug!("huge_page|madvise_failed|errno={ret}");
52 }
53 }
54 Self {
55 ptr,
56 len,
57 cap_bytes,
58 }
59 }
60
61 fn as_slice(&self) -> &[f32] {
62 unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
63 }
64
65 fn as_mut_slice(&mut self) -> &mut [f32] {
66 unsafe { std::slice::from_raw_parts_mut(self.ptr, self.len) }
67 }
68}
69
70impl Clone for HugePageF32 {
71 fn clone(&self) -> Self {
72 let buf = Self::new(self.len);
73 unsafe {
74 std::ptr::copy_nonoverlapping(self.ptr, buf.ptr, self.len);
75 }
76 buf
77 }
78}
79
80impl Drop for HugePageF32 {
81 fn drop(&mut self) {
82 let layout =
83 Layout::from_size_align(self.cap_bytes, HUGE_PAGE_ALIGN).expect("huge-page layout");
84 unsafe {
85 std::alloc::dealloc(self.ptr as *mut u8, layout);
86 }
87 }
88}
89
90#[derive(Clone)]
91struct WeightEntry {
92 n_in: usize,
93 n_out: usize,
94 data: HugePageF32,
96 last_use: u64,
97}
98
99struct WeightCache {
100 entries: HashMap<u64, WeightEntry>,
101 clock: u64,
102}
103
104fn cache() -> &'static Mutex<WeightCache> {
105 static C: OnceLock<Mutex<WeightCache>> = OnceLock::new();
106 C.get_or_init(|| {
107 Mutex::new(WeightCache {
108 entries: HashMap::new(),
109 clock: 0,
110 })
111 })
112}
113
114pub fn weight_fingerprint(raw: &[u8], n_in: usize, n_out: usize) -> u64 {
116 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
117 for &b in raw {
118 h ^= b as u64;
119 h = h.wrapping_mul(0x0100_0000_01b3);
120 }
121 h ^= n_in as u64;
122 h = h.wrapping_mul(0x0100_0000_01b3);
123 h ^= n_out as u64;
124 h.wrapping_mul(0x0100_0000_01b3)
125}
126
127pub fn dense_weight_cached(key: u64) -> bool {
129 cache()
130 .lock()
131 .ok()
132 .map(|g| g.entries.contains_key(&key))
133 .unwrap_or(false)
134}
135
136pub fn cache_dense_weight(key: u64, n_in: usize, n_out: usize, data: Vec<f32>) {
138 if data.len() != n_in.saturating_mul(n_out) {
139 return;
140 }
141 let len = data.len();
142 let buf = HugePageF32::new(len);
143 unsafe {
144 std::ptr::copy_nonoverlapping(data.as_ptr(), buf.ptr, len);
145 }
146 let Ok(mut g) = cache().lock() else {
147 return;
148 };
149 g.clock = g.clock.wrapping_add(1);
150 if g.entries.len() >= MAX_WEIGHT_ENTRIES && !g.entries.contains_key(&key) {
151 if let Some(old_k) = g
153 .entries
154 .iter()
155 .min_by_key(|(_, e)| e.last_use)
156 .map(|(k, _)| *k)
157 {
158 g.entries.remove(&old_k);
159 log::info!("cuda_lane|weight_evict|key={old_k:#x}");
160 }
161 }
162 let clock = g.clock;
163 g.entries.insert(
164 key,
165 WeightEntry {
166 n_in,
167 n_out,
168 data: buf,
169 last_use: clock,
170 },
171 );
172 let n_ent = g.entries.len();
173 log::debug!("cuda_lane|weight_cache|key={key:#x}|n_in={n_in}|n_out={n_out}|entries={n_ent}");
174}
175
176pub fn cache_dense_weight_direct<F>(key: u64, n_in: usize, n_out: usize, fill: F) -> bool
180where
181 F: FnOnce(&mut [f32]) -> bool,
182{
183 let total = n_in.saturating_mul(n_out);
184 if total == 0 || total > MAX_DENSE_ELEMS {
185 return false;
186 }
187 let mut buf = HugePageF32::new(total);
188 let ok = fill(buf.as_mut_slice());
189 if !ok {
190 return false;
191 }
192 let Ok(mut g) = cache().lock() else {
193 return false;
194 };
195 g.clock = g.clock.wrapping_add(1);
196 if g.entries.len() >= MAX_WEIGHT_ENTRIES && !g.entries.contains_key(&key) {
197 if let Some(old_k) = g
198 .entries
199 .iter()
200 .min_by_key(|(_, e)| e.last_use)
201 .map(|(k, _)| *k)
202 {
203 g.entries.remove(&old_k);
204 log::info!("cuda_lane|weight_evict|key={old_k:#x}");
205 }
206 }
207 let clock = g.clock;
208 g.entries.insert(
209 key,
210 WeightEntry {
211 n_in,
212 n_out,
213 data: buf,
214 last_use: clock,
215 },
216 );
217 let n_ent = g.entries.len();
218 log::debug!(
219 "cuda_lane|weight_cache_direct|key={key:#x}|n_in={n_in}|n_out={n_out}|entries={n_ent}"
220 );
221 true
222}
223
224fn pad16(x: usize) -> usize {
225 ((x + 15) / 16) * 16
226}
227
228pub fn try_cuda_batch_gemv(
236 hidden: &[f32],
237 batch: usize,
238 n_in: usize,
239 n_out: usize,
240 weight: &[f32],
241 out: &mut [f32],
242) -> bool {
243 if !crate::inference_modes::prefer_tensor_core_gemm() {
244 return false;
245 }
246 if batch == 0 || n_in == 0 || n_out == 0 {
247 return false;
248 }
249 if hidden.len() < batch * n_in || weight.len() < n_out * n_in || out.len() < batch * n_out {
250 return false;
251 }
252 ensure_cuda_runtime_path();
253
254 let m = pad16(batch);
257 let k = pad16(n_in);
258 let n = pad16(n_out);
259
260 let mut a = vec![0.0f32; m * k];
261 for b in 0..batch {
262 a[b * k..b * k + n_in].copy_from_slice(&hidden[b * n_in..b * n_in + n_in]);
263 }
264 let mut bmat = vec![0.0f32; k * n];
266 for j in 0..n_out {
267 for i in 0..n_in {
268 bmat[i * n + j] = weight[j * n_in + i];
269 }
270 }
271
272 let use_reduced = matches!(
276 std::env::var("QUALIA_LLM_CUDA_TC_REDUCED").ok().as_deref(),
277 Some("1") | Some("true")
278 );
279 let c = if use_reduced {
280 match gemm_f32_tc_reduced(m, k, n, &a, &bmat) {
281 Ok(v) => v,
282 Err(e) => {
283 log::warn!("cuda_lane|batch_gemv|tc_reduced_fail|{e:?}");
284 return false;
285 }
286 }
287 } else {
288 match crate::wgsl_forge::dispatch::gemm_f32_tc(m, k, n, &a, &bmat) {
289 Ok(v) => v,
290 Err(e) => {
291 log::warn!("cuda_lane|batch_gemv|tc_fail|{e:?}");
292 return false;
293 }
294 }
295 };
296 for b in 0..batch {
297 out[b * n_out..b * n_out + n_out].copy_from_slice(&c[b * n..b * n + n_out]);
298 }
299 true
300}
301
302pub fn try_cuda_batch_gemv_cached_only(
304 key: u64,
305 hidden: &[f32],
306 batch: usize,
307 out: &mut [f32],
308) -> bool {
309 if !crate::inference_modes::prefer_tensor_core_gemm() {
310 return false;
311 }
312 let Ok(mut g) = cache().lock() else {
313 return false;
314 };
315 g.clock = g.clock.wrapping_add(1);
316 let clock = g.clock;
317 let Some(e) = g.entries.get_mut(&key) else {
318 return false;
319 };
320 e.last_use = clock;
321 let (wi, wo) = (e.n_in, e.n_out);
322 let w = e.data.clone();
323 drop(g);
324 try_cuda_batch_gemv(hidden, batch, wi, wo, w.as_slice(), out)
325}
326
327pub fn try_cuda_batch_gemv_cached(
330 key: u64,
331 hidden: &[f32],
332 batch: usize,
333 n_in: usize,
334 n_out: usize,
335 weight: &[f32],
336 out: &mut [f32],
337) -> bool {
338 if try_cuda_batch_gemv_cached_only(key, hidden, batch, out) {
339 return true;
340 }
341 if weight.len() == n_in.saturating_mul(n_out) {
342 cache_dense_weight(key, n_in, n_out, weight.to_vec());
343 return try_cuda_batch_gemv(hidden, batch, n_in, n_out, weight, out);
344 }
345 false
346}
347
348pub fn weight_cache_len() -> usize {
350 cache().lock().map(|g| g.entries.len()).unwrap_or(0)
351}
352
353pub fn clear_weight_cache() {
355 if let Ok(mut g) = cache().lock() {
356 g.entries.clear();
357 g.clock = 0;
358 }
359}