spark_runtime/weights/
derived.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Load-time derived weight buffers, and who frees them.
4//!
5//! **WHY (#736, #915).** A loader that re-encodes a checkpoint tensor — a
6//! transposed twin, a row-concatenation of two projections, an NVFP4 requant —
7//! allocates a buffer that lives in a *layer struct*. Layer structs have no
8//! `Drop` that reaches the GPU and no `ModelResource`, so nothing frees them:
9//! `TransformerModel::release_pools` walks buffers, KV cache, SSM pools,
10//! `DerivedWeights` and the store, and every one of these falls through to
11//! `AtlasCudaBackend::sweep_unreleased`. On 1xH100, 2026-09-11,
12//! `Qwen/Qwen3.8-27B-FP8`, that sweep reclaimed **28.01 GB across 1,980
13//! allocations** and warned that each was "memory whose owner is unaccounted
14//! for". The sweep is a backstop, not an owner: it cannot run before the
15//! backend is torn down, so those bytes are unreclaimable for the life of the
16//! model even when the layer that holds them has been replaced.
17//!
18//! This is the owner. The [`WeightStore`](super::WeightStore) is the right home
19//! for it because these buffers are *derived from* store tensors, have exactly
20//! the store's lifetime (the layers read both until teardown), and the store is
21//! already the last `ModelResource` released before the sweep — so adopting a
22//! derived buffer here moves it from "swept" to "released", which is the
23//! difference the ledger reports.
24//!
25//! Interior mutability because loaders take `&WeightStore`: threading a `&mut`
26//! through `ModelWeightLoader::load_layers` would change the signature of every
27//! architecture's loader to fix a bookkeeping gap in one of them.
28
29use parking_lot::Mutex;
30
31use crate::gpu::{DevicePtr, GpuBackend};
32
33/// One adopted buffer: the pointer to free, its size, and a label for the
34/// residency report.
35#[derive(Clone, Copy, Debug)]
36struct Derived {
37    label: &'static str,
38    ptr: DevicePtr,
39    bytes: usize,
40}
41
42/// Device buffers a loader derived from this store's tensors.
43#[derive(Default)]
44pub struct DerivedStore {
45    // parking_lot: no poisoning, so a panic elsewhere cannot block teardown.
46    owned: Mutex<Vec<Derived>>,
47}
48
49impl DerivedStore {
50    /// Take ownership of a derived buffer.
51    ///
52    /// `label` is a static category ("ssm qkvz fp8 concat", "attn fp8 twin"),
53    /// not a per-tensor name: the report aggregates, and a `String` per buffer
54    /// would allocate once per layer per twin for text nobody reads per entry.
55    pub fn adopt(&self, label: &'static str, ptr: DevicePtr, bytes: usize) {
56        if ptr.0 == 0 {
57            return;
58        }
59        self.owned.lock().push(Derived { label, ptr, bytes });
60    }
61
62    /// Give up ownership of a buffer the caller is about to free itself.
63    ///
64    /// The transient half of a fused concat is adopted by the generic loader
65    /// that produced it and then freed by the loader that consumed it
66    /// (`qwen35_dense.rs`, the GDN `[QKV|Z]` arm). Without this the pointer
67    /// would be freed twice — once there, once at teardown. Returns the bytes
68    /// that left the ledger, or `None` if this store never held it.
69    pub fn disown(&self, ptr: DevicePtr) -> Option<usize> {
70        let mut owned = self.owned.lock();
71        let i = owned.iter().position(|d| d.ptr == ptr)?;
72        Some(owned.swap_remove(i).bytes)
73    }
74
75    /// Total adopted bytes still live.
76    pub fn bytes(&self) -> usize {
77        self.owned.lock().iter().map(|d| d.bytes).sum()
78    }
79
80    pub fn len(&self) -> usize {
81        self.owned.lock().len()
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.len() == 0
86    }
87
88    /// `label -> (bytes, count)`, biggest first, for the residency summary.
89    pub fn by_label(&self) -> Vec<(&'static str, usize, usize)> {
90        let mut rows: Vec<(&'static str, usize, usize)> = Vec::new();
91        for d in self.owned.lock().iter() {
92            match rows.iter_mut().find(|r| r.0 == d.label) {
93                Some(r) => {
94                    r.1 += d.bytes;
95                    r.2 += 1;
96                }
97                None => rows.push((d.label, d.bytes, 1)),
98            }
99        }
100        rows.sort_by(|a, b| b.1.cmp(&a.1));
101        rows
102    }
103
104    /// Free everything adopted. Drains first, so a failure part-way through
105    /// cannot leave a freed pointer in the list to be freed again.
106    pub fn release(&self, gpu: &dyn GpuBackend) -> anyhow::Result<()> {
107        let doomed: Vec<Derived> = self.owned.lock().drain(..).collect();
108        let mut first_error = None;
109        for d in doomed {
110            if let Err(e) = gpu.free(d.ptr)
111                && first_error.is_none()
112            {
113                first_error = Some(e.context(format!("freeing derived weight ({})", d.label)));
114            }
115        }
116        match first_error {
117            Some(e) => Err(e),
118            None => Ok(()),
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::gpu::mock::MockGpuBackend;
127
128    #[test]
129    fn an_adopted_buffer_is_freed_by_release() {
130        let gpu = MockGpuBackend::new();
131        let d = DerivedStore::default();
132        let a = gpu.alloc(1024).unwrap();
133        let b = gpu.alloc(2048).unwrap();
134        d.adopt("twin", a, 1024);
135        d.adopt("twin", b, 2048);
136        assert_eq!(d.len(), 2);
137        assert_eq!(d.bytes(), 3072);
138        d.release(&gpu).unwrap();
139        assert!(d.is_empty(), "release must drain, not merely free");
140        assert_eq!(d.bytes(), 0);
141    }
142
143    #[test]
144    fn release_is_idempotent() {
145        let gpu = MockGpuBackend::new();
146        let d = DerivedStore::default();
147        d.adopt("twin", gpu.alloc(64).unwrap(), 64);
148        d.release(&gpu).unwrap();
149        // A second release must not double-free: the list is already drained.
150        d.release(&gpu).unwrap();
151    }
152
153    #[test]
154    fn a_null_pointer_is_not_adopted() {
155        let d = DerivedStore::default();
156        d.adopt("twin", DevicePtr::NULL, 0);
157        assert!(d.is_empty(), "a NULL twin is 'not built', not 'owned'");
158    }
159
160    #[test]
161    fn disown_removes_the_buffer_so_release_cannot_double_free() {
162        let gpu = MockGpuBackend::new();
163        let d = DerivedStore::default();
164        let transient = gpu.alloc(512).unwrap();
165        d.adopt("transient", transient, 512);
166        assert_eq!(d.disown(transient), Some(512));
167        assert!(d.is_empty());
168        // A pointer this store never held is not silently "disowned".
169        assert_eq!(d.disown(transient), None);
170    }
171
172    #[test]
173    fn by_label_aggregates_biggest_first() {
174        let gpu = MockGpuBackend::new();
175        let d = DerivedStore::default();
176        d.adopt("small", gpu.alloc(16).unwrap(), 16);
177        d.adopt("big", gpu.alloc(1000).unwrap(), 1000);
178        d.adopt("big", gpu.alloc(1000).unwrap(), 1000);
179        assert_eq!(d.by_label(), vec![("big", 2000, 2), ("small", 16, 1)]);
180    }
181}