spark_model/layers/ops/
model_stats.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! [`ModelStats`] — diagnostic counters and one-shot latches owned by the model.
4//!
5//! Sibling to [`ModelLevers`](super::ModelLevers): the levers say what a model's
6//! kernels *do*, this records what they *did*. Both are owned by
7//! `TransformerModel` and lent to every `ForwardContext`.
8//!
9//! Telemetry is not exempt from scoping. A counter that spans a model swap
10//! averages two models together and describes neither, and a one-shot dump
11//! latch that has already fired suppresses the *next* model's dump — the exact
12//! artifact someone asked for by setting the flag. Nothing here changes
13//! generation, so the failure is a wrong measurement rather than a wrong
14//! answer; for a diagnostic those are the same kind of defect.
15//!
16//! Counters are atomics because they are mutated through the shared `&` that
17//! `ForwardContext` hands out. The change from the statics they replace is
18//! where they live, not how they are written.
19
20use std::sync::atomic::AtomicU64;
21
22/// Per-model diagnostic state.
23#[derive(Debug, Default)]
24pub struct ModelStats {
25    /// MoE expert-union sampling (`ModelLevers::moe_union_stats`): calls seen,
26    /// calls sampled, and the running unique-expert / slot totals behind the
27    /// periodic aggregate line.
28    pub moe_union: MoeUnionStats,
29    /// One-shot latches for the `ATLAS_*_DUMP` diagnostics. A latch is per
30    /// model so a swap re-arms the dump instead of silently swallowing it.
31    pub dumped: DumpLatches,
32}
33
34/// Expert-union sampling counters for one model.
35#[derive(Debug, Default)]
36pub struct MoeUnionStats {
37    pub calls: AtomicU64,
38    pub samples: AtomicU64,
39    pub unique_sum: AtomicU64,
40    pub slots_sum: AtomicU64,
41}
42
43/// One-shot diagnostic latches for one model.
44///
45/// The named fields are the latches with a caller that already holds the
46/// struct; [`keyed`](DumpLatches::keyed) covers the long tail of
47/// `ATLAS_*_DUMP` gates, which are numerous, scattered, and identical in
48/// shape — a field each would be noise, and a static each is the bug.
49#[derive(Debug, Default)]
50pub struct DumpLatches {
51    /// Ad-hoc latches, keyed by a `&'static str` naming the dump.
52    fired: std::sync::Mutex<std::collections::BTreeSet<&'static str>>,
53}
54
55impl ModelStats {
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// `true` the first time THIS model reaches `key`, `false` after.
61    ///
62    /// The general per-model latch. Log-dedup gates used a `static Once` each,
63    /// which is correct for "print this line once" and wrong for "print this
64    /// line once per model": after a swap the new model's kernel-route and
65    /// fallback lines — the ones that say which path a model actually took —
66    /// were suppressed by the previous model's shot, and those lines exist to
67    /// be read when a model behaves unexpectedly.
68    ///
69    /// Namespace keys by purpose (`"log:..."`, `"dump:..."`) so two unrelated
70    /// sites cannot collide.
71    pub fn once(&self, key: &'static str) -> bool {
72        self.dumped.keyed(key)
73    }
74}
75
76impl DumpLatches {
77    /// `true` exactly once per model for `key`. Use for the `ATLAS_*_DUMP`
78    /// gates that would otherwise each grow a `static AtomicBool`.
79    ///
80    /// Call it only when the dump is actually wanted — it consumes the shot,
81    /// so gating on the env var FIRST keeps a disabled dump from burning it.
82    pub fn keyed(&self, key: &'static str) -> bool {
83        self.fired
84            .lock()
85            .expect("dump latches poisoned")
86            .insert(key)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use std::sync::atomic::Ordering;
94
95    #[test]
96    fn a_keyed_latch_fires_once_per_key_per_model() {
97        let a = ModelStats::new();
98        assert!(a.dumped.keyed("dflash_block"));
99        assert!(!a.dumped.keyed("dflash_block"), "and only once");
100        assert!(a.dumped.keyed("dflash_ctx"), "a different dump is separate");
101        assert!(
102            ModelStats::new().dumped.keyed("dflash_block"),
103            "and a new model re-arms every key"
104        );
105    }
106
107    #[test]
108    fn two_models_count_expert_unions_independently() {
109        let a = ModelStats::new();
110        let b = ModelStats::new();
111        a.moe_union.calls.fetch_add(9, Ordering::Relaxed);
112        assert_eq!(a.moe_union.calls.load(Ordering::Relaxed), 9);
113        assert_eq!(
114            b.moe_union.calls.load(Ordering::Relaxed),
115            0,
116            "a second model starts clean rather than inheriting a mean"
117        );
118    }
119}