Skip to main content

ferrox_core/
activation_tap.rs

1//! A process-wide observer of the f32 activations fed to every
2//! [`WeightMatrix`] projection: the seam `ferrox imatrix` collects an
3//! importance matrix through.
4//!
5//! llama.cpp collects its imatrix with a scheduler callback
6//! (`cb_eval`) that sees every `MUL_MAT` node and its `src1` input
7//! (`tools/imatrix/imatrix.cpp:219-237`). ferrox has no graph, so the
8//! equivalent seam is the function every projection goes through:
9//! [`WeightMatrix::apply`] for one activation and
10//! [`WeightMatrix::apply_batch_with_acts`] for a batch. Both call
11//! [`observe`] before they touch the weights.
12//!
13//! **A `WeightMatrix` carries no name**, and the loader that knows the
14//! names lives in `ferrox-models`, so the observer is handed the matrix
15//! by reference and it is the installer's job to know which one it is
16//! -- `ferrox imatrix` walks the decoder's public weight fields and
17//! keys on the address. That is why the callback takes `&WeightMatrix`
18//! and not a string: adding a name field would have to be threaded
19//! through thirty construction sites across seven loaders to change
20//! nothing for inference.
21//!
22//! Cost when nothing is installed: one relaxed atomic load per
23//! projection call, not per element. A decode step is a few dozen
24//! calls, so this is not a hot-path concern.
25//!
26//! The tap sees the f32 input EXACTLY as the projection receives it,
27//! before any activation quantization the CPU INT_DOT path does. That
28//! is what llama.cpp's callback sees too (`src1->type == F32` is a
29//! precondition for collection).
30//!
31//! What it does NOT see: a GPU batch that fails to launch and degrades
32//! to per-row [`WeightMatrix::apply`] would fire the observer twice for
33//! the same rows. `ferrox imatrix` pins the CPU backend, where no such
34//! degradation path exists, and checks every dense entry's row count
35//! against the token count so a double observation is a refusal rather
36//! than a silently doubled matrix.
37
38use std::sync::atomic::{AtomicBool, Ordering};
39use std::sync::{Arc, RwLock};
40
41use crate::weight_matrix::WeightMatrix;
42
43/// The observer: the matrix being applied, the activation rows laid
44/// out `[n_rows][cols]`, and `n_rows`.
45pub type Observer = dyn Fn(&WeightMatrix, &[f32], usize) + Send + Sync;
46
47static INSTALLED: AtomicBool = AtomicBool::new(false);
48static OBSERVER: RwLock<Option<Arc<Observer>>> = RwLock::new(None);
49
50/// Installs `observer` for the life of the returned guard. Only one
51/// may be installed at a time: a second install while one is live is
52/// refused, because two observers would each see every row and neither
53/// would know the other was counting.
54pub fn install(observer: Arc<Observer>) -> Result<TapGuard, AlreadyInstalled> {
55    let mut slot = OBSERVER.write().unwrap_or_else(|e| e.into_inner());
56    if slot.is_some() {
57        return Err(AlreadyInstalled);
58    }
59    *slot = Some(observer);
60    INSTALLED.store(true, Ordering::Release);
61    Ok(TapGuard(()))
62}
63
64/// Uninstalls the observer on drop.
65#[must_use = "dropping the guard uninstalls the tap immediately"]
66pub struct TapGuard(());
67
68impl Drop for TapGuard {
69    fn drop(&mut self) {
70        INSTALLED.store(false, Ordering::Release);
71        *OBSERVER.write().unwrap_or_else(|e| e.into_inner()) = None;
72    }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct AlreadyInstalled;
77
78impl std::fmt::Display for AlreadyInstalled {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        f.write_str("an activation tap is already installed in this process")
81    }
82}
83
84impl std::error::Error for AlreadyInstalled {}
85
86/// Called by the projection entry points. The fast path is the
87/// `INSTALLED` load; the observer is cloned out of the lock so a slow
88/// observer never holds it across the call.
89#[inline]
90pub(crate) fn observe(matrix: &WeightMatrix, rows: &[f32], n_rows: usize) {
91    if !INSTALLED.load(Ordering::Acquire) {
92        return;
93    }
94    let observer = OBSERVER
95        .read()
96        .unwrap_or_else(|e| e.into_inner())
97        .as_ref()
98        .cloned();
99    if let Some(observer) = observer {
100        observer(matrix, rows, n_rows);
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::tensor::Tensor;
108    use std::sync::Mutex;
109
110    // Tests that install a tap share the one process-wide slot, so
111    // they serialise on this rather than racing each other.
112    static SERIAL: Mutex<()> = Mutex::new(());
113
114    fn small_matrix() -> WeightMatrix {
115        WeightMatrix::F32(Tensor::new(vec![1.0; 8], vec![2, 4]))
116    }
117
118    /// The tap sees each `apply_batch` call once, with every row, and
119    /// can tell which matrix was applied by address. Both halves are
120    /// what `ferrox imatrix` relies on.
121    #[test]
122    fn the_observer_sees_every_batch_row_once_and_the_matrix_identity() {
123        let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
124        let m = small_matrix();
125        let addr = &m as *const WeightMatrix as usize;
126        type Seen = Mutex<Vec<(usize, Vec<f32>, usize)>>;
127        let seen: Arc<Seen> = Arc::new(Mutex::new(Vec::new()));
128        let sink = seen.clone();
129        let guard = install(Arc::new(move |w: &WeightMatrix, rows: &[f32], n: usize| {
130            sink.lock()
131                .unwrap()
132                .push((w as *const WeightMatrix as usize, rows.to_vec(), n));
133        }))
134        .unwrap();
135        let x: Vec<f32> = (0..12).map(|i| i as f32).collect();
136        let _ = m.apply_batch(&x, 3);
137        drop(guard);
138        let seen = seen.lock().unwrap();
139        assert_eq!(seen.len(), 1, "one batch call, one observation");
140        assert_eq!(seen[0].0, addr);
141        assert_eq!(seen[0].1, x);
142        assert_eq!(seen[0].2, 3);
143    }
144
145    /// After the guard drops nothing is observed, and a second install
146    /// while one is live is refused rather than replacing it.
147    #[test]
148    fn the_tap_is_gone_after_the_guard_drops_and_cannot_be_installed_twice() {
149        let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
150        let m = small_matrix();
151        let calls = Arc::new(Mutex::new(0usize));
152        let c = calls.clone();
153        let guard = install(Arc::new(move |_: &WeightMatrix, _: &[f32], _| {
154            *c.lock().unwrap() += 1;
155        }))
156        .unwrap();
157        assert_eq!(
158            install(Arc::new(|_: &WeightMatrix, _: &[f32], _| {}))
159                .err()
160                .map(|_| ()),
161            Some(()),
162            "a second install must be refused while the first is live"
163        );
164        let _ = m.apply(&[1.0; 4]);
165        drop(guard);
166        let _ = m.apply(&[1.0; 4]);
167        assert_eq!(*calls.lock().unwrap(), 1);
168    }
169}