spark_runtime/weights/
deferred.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Tensors the checkpoint loader deliberately does NOT upload, and the hook a
4//! model's own weight loader uses to say so.
5//!
6//! # Two ways a tensor becomes deferred
7//!
8//! 1. **`is_ngram_table`** — a NAME rule baked into the runtime, for the
9//!    LongCat / Qwen3.8-Flash-Next embedding tables. Always on, because those
10//!    tables (63 GB to 102 GB) cannot be uploaded on any box we serve.
11//! 2. **[`DeferHook`]** — a predicate the MODEL's loader supplies, keyed on
12//!    `(name, on-disk dtype)`. Off unless a loader asks for it, and plumbed
13//!    exactly like `skip_vision`: `serve_phases/weights.rs` asks
14//!    `ModelWeightLoader::defer_predicate` once and hands the answer to
15//!    whichever checkpoint loader runs.
16//!
17//! Both end in the same place — a [`DeferredTensor`] in the store's
18//! `deferred` map — so a consumer never has to know which rule withheld it.
19//!
20//! # Why a loader would ask for this
21//!
22//! The rule is NOT "this tensor is huge". It is "the bytes that reach the
23//! device are not the bytes on disk, and the model's loader is the only thing
24//! that knows how to make them".
25//!
26//! `nvidia/GLM-5.3-Flash-NVFP4` ships the MTP block's 288 routed experts as
27//! full-width BF16 while the forward is w4a16 end to end. Every one of those
28//! tensors is read once, by `glm5_next_load::bind_expert`, quantised, and
29//! replaced by an NVFP4 triple ~3.6x smaller. Uploading the BF16 first is
30//! ~7.25 GB per EP=2 rank of device residency that lasts from the sweep until
31//! `prune_after_load` — i.e. across the WHOLE 45-layer build, which on a
32//! unified-memory box is where the host-memory guard fires. Measured
33//! 2026-09-21 on a 2-rank boot: survived at a 2048-token shape (host floor
34//! 1.74 GB), killed rank 0 at layer 38 at the 32K shape (935 MB against a
35//! 1200 MB floor) BEFORE the MTP bind could free anything.
36//!
37//! Deferring makes that transient structurally impossible: the BF16 never
38//! leaves the page cache, and the only expert bytes that reach the device are
39//! the NVFP4 ones, for the experts this rank owns.
40//!
41//! # Contract, both halves of it
42//!
43//! * The loader that defers a tensor MUST read it back — nothing else will.
44//!   A predicate that over-matches withholds a tensor a binder expects, and
45//!   `WeightStore::get` then fails by NAME at bind time rather than producing
46//!   a subtly wrong model. That is the intended failure mode; it is still the
47//!   loader author's job not to reach it.
48//! * The OOM pre-flight MUST agree with the predicate. A deferred tensor is
49//!   never part of the sweep peak, so counting it at its on-disk size refuses
50//!   loads that fit — the same reasoning that already excludes the n-gram
51//!   tables. The pre-flight and the retain loop are handed the SAME closure
52//!   for exactly this reason.
53//!
54//! # 🪤 Keyed on the ON-DISK dtype, and F16 is never offered
55//!
56//! A deferred tensor is served from its own bytes in the shard, so the
57//! predicate is asked about the width those bytes actually have. F16 is the
58//! one width both disk loaders REWRITE on the way to the store
59//! (`f16_to_bf16_bytes`), so a (path, offset) locator would hand its reader
60//! F16 where the rest of the engine expects BF16. F16 tensors are therefore
61//! never deferred — by the retain loop and by the pre-flight alike, which is
62//! the only way the two can stay the same rule.
63
64use anyhow::{Context, Result};
65
66use super::WeightDtype;
67
68/// A model loader's answer to "which tensors will I read from disk myself?".
69///
70/// `(tensor name, store dtype) -> defer`. `Arc` because both checkpoint
71/// loaders hold it for the length of a load and the fast loader's pre-flight
72/// and retain loop both call it; `Send + Sync` because the fast loader's
73/// pipeline is threaded.
74pub type DeferHook = std::sync::Arc<dyn Fn(&str, WeightDtype) -> bool + Send + Sync>;
75
76/// Where a skipped tensor lives, so a consumer can read it in place.
77#[derive(Clone, Debug)]
78pub struct DeferredTensor {
79    /// Shard file containing the tensor.
80    pub path: std::path::PathBuf,
81    /// ABSOLUTE byte offset of the tensor's first element in that file
82    /// (safetensors header length + the tensor's `data_offsets[0]`).
83    pub offset: u64,
84    pub shape: Vec<usize>,
85    pub dtype: WeightDtype,
86}
87
88impl DeferredTensor {
89    /// The tensor's on-disk footprint.
90    ///
91    /// Only the fixed-width dtypes a safetensors header can carry reach here,
92    /// so `numel * byte_size` is exact — the block-based GGUF dtypes
93    /// ([`WeightDtype::PackedQ2_0`] and the K-quants) report 0 bytes/element
94    /// and are never deferred by either rule.
95    pub fn byte_size(&self) -> usize {
96        let numel: usize = self.shape.iter().product();
97        numel * self.dtype.byte_size()
98    }
99
100    /// Read the whole tensor's raw bytes into host memory.
101    ///
102    /// The consumer for a deferred tensor is usually a row cache that faults
103    /// single rows off NVMe; this is the other shape of consumer — a loader
104    /// that needs the tensor ONCE, at bind time, to derive the buffer it will
105    /// actually upload.
106    ///
107    /// 🪤 `read_exact` rather than `read_to_end`: a short read means the shard
108    /// is truncated or the offset is wrong, and both must be an error here
109    /// rather than a shorter tensor whose element count is rejected later with
110    /// a message pointing at the wrong thing. Seek-then-read rather than
111    /// `pread`, because `WeightStore` builds on Windows too.
112    pub fn read_host_bytes(&self) -> Result<Vec<u8>> {
113        use std::io::{Read, Seek, SeekFrom};
114        let n = self.byte_size();
115        let mut f = std::fs::File::open(&self.path)
116            .with_context(|| format!("deferred tensor: opening {}", self.path.display()))?;
117        f.seek(SeekFrom::Start(self.offset))?;
118        let mut buf = vec![0u8; n];
119        f.read_exact(&mut buf).with_context(|| {
120            format!(
121                "deferred tensor: reading {n} B at offset {} of {}",
122                self.offset,
123                self.path.display()
124            )
125        })?;
126        Ok(buf)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    fn write_shard(bytes: &[u8]) -> (tempfile::TempDir, std::path::PathBuf) {
135        let dir = tempfile::tempdir().unwrap();
136        let path = dir.path().join("shard.safetensors");
137        std::fs::write(&path, bytes).unwrap();
138        (dir, path)
139    }
140
141    #[test]
142    fn byte_size_is_the_on_disk_footprint() {
143        let d = DeferredTensor {
144            path: std::path::PathBuf::from("x"),
145            offset: 0,
146            shape: vec![2048, 4096],
147            dtype: WeightDtype::BF16,
148        };
149        assert_eq!(d.byte_size(), 2048 * 4096 * 2);
150    }
151
152    #[test]
153    fn host_read_returns_exactly_the_tensor_at_its_offset() {
154        // 8 filler bytes stand in for a header, then four BF16 values.
155        let payload: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8];
156        let (_d, path) = write_shard(&[vec![0xFFu8; 8], payload.clone()].concat());
157        let d = DeferredTensor {
158            path,
159            offset: 8,
160            shape: vec![2, 2],
161            dtype: WeightDtype::BF16,
162        };
163        assert_eq!(d.read_host_bytes().unwrap(), payload);
164    }
165
166    /// Write a one-tensor-per-entry safetensors file and return its path.
167    fn write_safetensors(
168        dir: &std::path::Path,
169        entries: &[(&str, &str, Vec<usize>, usize)],
170    ) -> std::path::PathBuf {
171        let mut header = serde_json::Map::new();
172        let mut end = 0usize;
173        for (name, dtype, shape, len) in entries {
174            header.insert(
175                (*name).to_string(),
176                serde_json::json!({
177                    "dtype": dtype,
178                    "shape": shape,
179                    "data_offsets": [end, end + len],
180                }),
181            );
182            end += len;
183        }
184        let header = serde_json::to_vec(&serde_json::Value::Object(header)).unwrap();
185        let mut blob = (header.len() as u64).to_le_bytes().to_vec();
186        blob.extend_from_slice(&header);
187        blob.extend_from_slice(&vec![0u8; end]);
188        let path = dir.join("model.safetensors");
189        std::fs::write(&path, blob).unwrap();
190        path
191    }
192
193    /// 🔴 The other half of the contract: a deferred tensor is never swept, so
194    /// the OOM pre-flight must not count it. Before this, the official GLM-5.3
195    /// export's ~7.25 GB of full-width MTP experts were charged to a peak they
196    /// never joined.
197    #[test]
198    fn the_preflight_estimate_drops_deferred_tensors() {
199        let dir = tempfile::tempdir().unwrap();
200        let path = write_safetensors(
201            dir.path(),
202            &[
203                ("big.weight", "BF16", vec![8, 8], 128),
204                ("small.weight", "BF16", vec![4], 8),
205            ],
206        );
207        let files = [path];
208
209        let all = super::super::estimate_load_bytes(&files, &|_| false, &|_, _| false).unwrap();
210        assert_eq!(all, 136);
211
212        let without = super::super::estimate_load_bytes(&files, &|_| false, &|n, d| {
213            n == "big.weight" && d == WeightDtype::BF16
214        })
215        .unwrap();
216        assert_eq!(without, 8, "the deferred tensor must leave the peak");
217
218        // 🪤 Keyed on the dtype too: the same NAME at a width the predicate
219        // does not claim is still counted, because it is still uploaded.
220        let wrong_dtype = super::super::estimate_load_bytes(&files, &|_| false, &|n, d| {
221            n == "big.weight" && d == WeightDtype::UInt8
222        })
223        .unwrap();
224        assert_eq!(wrong_dtype, 136);
225    }
226
227    /// F16 is rewritten on the way to the store, so it can never be served
228    /// from its on-disk bytes — the estimate counts it however loudly the
229    /// predicate claims it, which is exactly what the loaders then do.
230    #[test]
231    fn an_f16_tensor_is_counted_even_when_the_predicate_claims_everything() {
232        let dir = tempfile::tempdir().unwrap();
233        let path = write_safetensors(dir.path(), &[("h.weight", "F16", vec![8, 8], 128)]);
234        let got = super::super::estimate_load_bytes(&[path], &|_| false, &|_, _| true).unwrap();
235        assert_eq!(got, 128);
236    }
237
238    #[test]
239    fn a_truncated_shard_is_an_error_not_a_short_tensor() {
240        let (_d, path) = write_shard(&[0u8; 8]);
241        let d = DeferredTensor {
242            path,
243            offset: 0,
244            shape: vec![64, 64],
245            dtype: WeightDtype::BF16,
246        };
247        assert!(d.read_host_bytes().is_err());
248    }
249}