Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}