/home/noah/src/realizar/src/gguf/inference/cached/weights.rs
Line | Count | Source |
1 | | //! Dequantized weight cache for GPU GEMM operations |
2 | | //! |
3 | | //! Stores pre-dequantized f32 weights for GPU GEMM to avoid |
4 | | //! repeated dequantization on every forward pass. |
5 | | |
6 | | |
7 | | /// Dequantized FFN weights for a single transformer layer |
8 | | /// |
9 | | /// Stores pre-dequantized f32 weights for GPU GEMM operations. |
10 | | /// Cache these to avoid repeated dequantization on every forward pass. |
11 | | #[derive(Clone)] |
12 | | pub struct DequantizedFFNWeights { |
13 | | /// Up projection weights [hidden_dim, intermediate_dim] |
14 | | pub up: Vec<f32>, |
15 | | /// Down projection weights [intermediate_dim, hidden_dim] |
16 | | pub down: Vec<f32>, |
17 | | /// Optional up bias [intermediate_dim] |
18 | | pub up_bias: Option<Vec<f32>>, |
19 | | /// Optional down bias [hidden_dim] |
20 | | pub down_bias: Option<Vec<f32>>, |
21 | | } |
22 | | |
23 | | /// Cache for dequantized FFN weights (PARITY-019) |
24 | | /// |
25 | | /// Uses RwLock for concurrent read access during batch inference. |
26 | | /// Weights are dequantized once during warmup and reused for GPU GEMM. |
27 | | /// |
28 | | /// # Performance Impact |
29 | | /// - Eliminates per-forward dequantization overhead |
30 | | /// - Enables GPU GEMM with f32 weights |
31 | | /// - Memory tradeoff: ~6.4 GB for phi-2 32 layers |
32 | | /// |
33 | | /// # Thread Safety |
34 | | /// - RwLock allows multiple concurrent readers during inference |
35 | | /// - Single writer during warmup phase |
36 | | #[cfg(feature = "gpu")] |
37 | | pub struct DequantizedWeightCache { |
38 | | /// Per-layer dequantized weights |
39 | | layers: std::sync::RwLock<std::collections::HashMap<usize, DequantizedFFNWeights>>, |
40 | | /// Hidden dimension for validation |
41 | | hidden_dim: usize, |
42 | | /// Intermediate FFN dimension |
43 | | intermediate_dim: usize, |
44 | | /// Number of layers to cache |
45 | | num_layers: usize, |
46 | | } |
47 | | |
48 | | #[cfg(feature = "gpu")] |
49 | | impl DequantizedWeightCache { |
50 | | /// Create a new weight cache with specified dimensions |
51 | | /// |
52 | | /// # Arguments |
53 | | /// * `hidden_dim` - Model hidden dimension (e.g., 2560 for phi-2) |
54 | | /// * `intermediate_dim` - FFN intermediate dimension (e.g., 10240 for phi-2) |
55 | | /// * `num_layers` - Number of transformer layers to cache |
56 | | #[must_use] |
57 | 6 | pub fn new(hidden_dim: usize, intermediate_dim: usize, num_layers: usize) -> Self { |
58 | 6 | Self { |
59 | 6 | layers: std::sync::RwLock::new(std::collections::HashMap::with_capacity(num_layers)), |
60 | 6 | hidden_dim, |
61 | 6 | intermediate_dim, |
62 | 6 | num_layers, |
63 | 6 | } |
64 | 6 | } |
65 | | |
66 | | /// Pre-warmup all layers with dequantized weights |
67 | | /// |
68 | | /// Call this once at startup to avoid dequantization during inference. |
69 | | /// The closure receives layer index and returns (up_weights, down_weights). |
70 | | /// |
71 | | /// # Arguments |
72 | | /// * `dequant_fn` - Closure that dequantizes weights for a given layer index |
73 | | /// |
74 | | /// # Panics |
75 | | /// Panics if the RwLock is poisoned |
76 | 4 | pub fn warmup<F>(&self, dequant_fn: F) |
77 | 4 | where |
78 | 4 | F: Fn(usize) -> (Vec<f32>, Vec<f32>), |
79 | | { |
80 | 4 | let mut cache = self.layers.write().expect("Cache lock poisoned"); |
81 | 11 | for layer_idx in 0..self.num_layers4 { |
82 | 11 | cache.entry(layer_idx).or_insert_with(|| { |
83 | 11 | let (up, down) = dequant_fn(layer_idx); |
84 | 11 | DequantizedFFNWeights { |
85 | 11 | up, |
86 | 11 | down, |
87 | 11 | up_bias: None, |
88 | 11 | down_bias: None, |
89 | 11 | } |
90 | 11 | }); |
91 | | } |
92 | 4 | } |
93 | | |
94 | | /// Warmup with biases |
95 | | /// |
96 | | /// Same as `warmup` but also caches bias vectors. |
97 | 1 | pub fn warmup_with_bias<F>(&self, dequant_fn: F) |
98 | 1 | where |
99 | 1 | F: Fn(usize) -> (Vec<f32>, Vec<f32>, Option<Vec<f32>>, Option<Vec<f32>>), |
100 | | { |
101 | 1 | let mut cache = self.layers.write().expect("Cache lock poisoned"); |
102 | 2 | for layer_idx in 0..self.num_layers1 { |
103 | 2 | cache.entry(layer_idx).or_insert_with(|| { |
104 | 2 | let (up, down, up_bias, down_bias) = dequant_fn(layer_idx); |
105 | 2 | DequantizedFFNWeights { |
106 | 2 | up, |
107 | 2 | down, |
108 | 2 | up_bias, |
109 | 2 | down_bias, |
110 | 2 | } |
111 | 2 | }); |
112 | | } |
113 | 1 | } |
114 | | |
115 | | /// Get cached weights for a layer (read-only access) |
116 | | /// |
117 | | /// Returns None if the layer hasn't been warmed up. |
118 | | /// Uses read lock for concurrent access during batch inference. |
119 | 21 | pub fn get(&self, layer_idx: usize) -> Option<DequantizedFFNWeights> { |
120 | 21 | let cache = self.layers.read().expect("Cache lock poisoned"); |
121 | 21 | cache.get(&layer_idx).cloned() |
122 | 21 | } |
123 | | |
124 | | /// Check if a layer is cached |
125 | 8 | pub fn is_cached(&self, layer_idx: usize) -> bool { |
126 | 8 | let cache = self.layers.read().expect("Cache lock poisoned"); |
127 | 8 | cache.contains_key(&layer_idx) |
128 | 8 | } |
129 | | |
130 | | /// Get number of cached layers |
131 | 14 | pub fn cached_count(&self) -> usize { |
132 | 14 | let cache = self.layers.read().expect("Cache lock poisoned"); |
133 | 14 | cache.len() |
134 | 14 | } |
135 | | |
136 | | /// Get total memory usage in bytes |
137 | 5 | pub fn memory_bytes(&self) -> usize { |
138 | | // Each layer: up + down weights |
139 | | // up: hidden_dim × intermediate_dim × 4 bytes |
140 | | // down: intermediate_dim × hidden_dim × 4 bytes |
141 | 5 | let per_layer = 2 * self.hidden_dim * self.intermediate_dim * 4; |
142 | 5 | self.cached_count() * per_layer |
143 | 5 | } |
144 | | |
145 | | /// Get model dimensions |
146 | | #[must_use] |
147 | 2 | pub fn dimensions(&self) -> (usize, usize, usize) { |
148 | 2 | (self.hidden_dim, self.intermediate_dim, self.num_layers) |
149 | 2 | } |
150 | | } |