metrale_model_engine/model/types/
teardown.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `TransformerModel` teardown: `adopt_weight_store` and `release_pools`.
4//!
5//! Owner: model-engine.
6//! Invariants:
7//! - `release_pools` attempts every release even after an earlier one fails, and returns the first error.
8
9use metrale_gpu_runtime::gpu::GpuBackend;
10
11use super::TransformerModel;
12
13impl TransformerModel {
14    /// 2026-09-25: Hand the model the ledger of its own weights, for teardown.
15    pub fn adopt_weight_store(&mut self, store: metrale_model_weights::weights::WeightStore) {
16        self.weight_store = Some(store);
17    }
18
19    /// 2026-09-25: Release every GPU resource this model owns, in this order:
20    /// derived weights, SSM snapshots, the SSM state pool, the KV cache, the
21    /// buffer arena, the weight store, then whatever the backend still tracks
22    /// (`GpuBackend::sweep_unreleased`). Every step is attempted even after an
23    /// earlier one fails; the first error is returned, with its label as
24    /// context.
25    pub(in crate::model) fn release_pools(&mut self) -> anyhow::Result<()> {
26        use metrale_core::scope::ModelResource;
27
28        let gpu: &dyn GpuBackend = self.gpu.as_ref();
29        let mut first_error: Option<anyhow::Error> = None;
30        let mut attempt = |label: &'static str, r: anyhow::Result<()>| {
31            if let Err(e) = r
32                && first_error.is_none()
33            {
34                first_error = Some(e.context(label));
35            }
36        };
37
38        attempt("derived weights", self.derived.release(gpu));
39        attempt("ssm snapshots", self.ssm_snapshots.release(gpu));
40        // 2026-09-25: The pool is Arc'd because sequences hold slots. Another
41        // live handle means a sequence was not released before teardown; that
42        // is reported as an error rather than forced.
43        match std::sync::Arc::get_mut(&mut self.ssm_pool) {
44            Some(pool) => attempt("ssm state pool", pool.release(gpu)),
45            None => attempt(
46                "ssm state pool",
47                Err(anyhow::anyhow!(
48                    "{} handle(s) still hold the SSM pool — a sequence was not \
49                     released before teardown",
50                    std::sync::Arc::strong_count(&self.ssm_pool) - 1
51                )),
52            ),
53        }
54        attempt("kv cache", self.kv_cache.lock().release(gpu));
55        attempt("buffer arena", self.buffers.release(gpu));
56        // 2026-09-25: The weight store goes after every owner above, because
57        // the layers hold pointers into it.
58        if let Some(mut store) = self.weight_store.take() {
59            attempt("weight store", store.release(gpu));
60        }
61        // 2026-09-25: Last: allocations no owner above released, such as
62        // weights the loaders fused into layer-owned buffers.
63        let swept = gpu.sweep_unreleased();
64        if swept > 0 {
65            tracing::warn!(target: "metrale_model_engine::model::types", "teardown swept {swept} allocation(s) that no ModelResource \
66                 released — they are reclaimed, but each one is memory whose \
67                 owner is unaccounted for"
68            );
69        }
70
71        match first_error {
72            Some(e) => Err(e),
73            None => Ok(()),
74        }
75    }
76}