metrale_model_weights/
weights.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Weight loading from safetensors files (SBIO IORouter for filesystem I/O).
4
5use anyhow::{Result, bail};
6use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
7use std::collections::HashMap;
8use std::path::Path;
9
10/// Advise the OS to evict a file's pages from the page cache.
11///
12/// On GB10 (unified memory), mmap'd safetensors share the GPU memory pool.
13/// After copying tensors to GPU, the mmap pages linger in the page cache,
14/// consuming memory that should be available for KV cache and inference buffers.
15/// This function tells the kernel those pages are no longer needed.
16#[cfg(target_os = "linux")]
17pub(crate) fn evict_page_cache(file: &std::fs::File) {
18    use std::os::unix::io::AsRawFd;
19    // POSIX_FADV_DONTNEED = 4 on Linux (POSIX standard).
20    // macOS lacks posix_fadvise — see the non-linux branch below.
21    const POSIX_FADV_DONTNEED: libc::c_int = 4;
22    unsafe {
23        libc::posix_fadvise(file.as_raw_fd(), 0, 0, POSIX_FADV_DONTNEED);
24    }
25}
26
27#[cfg(not(target_os = "linux"))]
28pub(crate) fn evict_page_cache(_file: &std::fs::File) {
29    // No-op: macOS/BSD have no posix_fadvise. Apple Silicon UMA already
30    // shares page cache with the GPU pool, so eviction is unnecessary.
31}
32
33/// Data type of a weight tensor.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum WeightDtype {
36    BF16,
37    FP32,
38    FP8E4M3,
39    FP8E8M0,
40    UInt8,
41    Int64,
42    /// Keep-packed PrismML ternary Q2_0 (ggml id 42): raw on-disk blocks stay
43    /// 2-bit in VRAM (fp16 scale + 2-bit codes per group of `group` elements),
44    /// dequantized in-kernel by the native `q2_0_gemv` decode path. Only
45    /// produced by the GGUF loader under `METRALE_GGUF_NATIVE_Q2=1`. Its byte
46    /// footprint is NOT a per-element size (2-bit codes + an inline scale per
47    /// group), so [`WeightDtype::byte_size`] returns 0 for this variant and the
48    /// real size is computed in [`WeightTensor::byte_size`] (shape + group).
49    PackedQ2_0 {
50        group: u16,
51    },
52    /// Raw GGUF `Q2_K` blocks kept on the device (84 bytes per 256 weights,
53    /// `[N, K]` row-major in blocks) for the K-quant GEMV/MMQ kernels.
54    Q2K,
55    /// Raw GGUF `Q3_K` blocks kept on the device (110 bytes per 256 weights).
56    Q3K,
57    /// Raw GGUF `Q6_K` blocks kept on the device (210 bytes per 256 weights):
58    /// the DeepSeek-V4.1 output head, run by the K-quant GEMV instead of a
59    /// bf16 expansion (0.82 instead of 2 bytes a weight read every token).
60    Q6K,
61}
62
63impl WeightDtype {
64    /// Bytes per element for the fixed-width dtypes. Returns 0 for the
65    /// block-based [`WeightDtype::PackedQ2_0`] — [`WeightTensor::byte_size`]
66    /// handles that variant directly, and no caller multiplies its numel by this.
67    pub fn byte_size(self) -> usize {
68        match self {
69            Self::BF16 => 2,
70            Self::FP32 => 4,
71            Self::FP8E4M3 => 1,
72            Self::FP8E8M0 => 1,
73            Self::UInt8 => 1,
74            Self::Int64 => 8,
75            Self::PackedQ2_0 { .. } => 0,
76            Self::Q2K => 0,
77            Self::Q3K => 0,
78            Self::Q6K => 0,
79        }
80    }
81
82    fn from_safetensors(dtype: safetensors::Dtype) -> Result<Self> {
83        match dtype {
84            safetensors::Dtype::BF16 => Ok(Self::BF16),
85            safetensors::Dtype::F32 => Ok(Self::FP32),
86            safetensors::Dtype::U8 => Ok(Self::UInt8),
87            // I8: raw 1-byte container for 4-bit-packed NVFP4 (DeepSeek-V4 MTP
88            // experts). Treat as UInt8 — signedness is irrelevant for packed FP4.
89            safetensors::Dtype::I8 => Ok(Self::UInt8),
90            safetensors::Dtype::F8_E4M3 => Ok(Self::FP8E4M3),
91            safetensors::Dtype::F8_E8M0 => Ok(Self::FP8E8M0),
92            safetensors::Dtype::I64 => Ok(Self::Int64),
93            other => bail!("Unsupported safetensors dtype: {other:?}"),
94        }
95    }
96
97    /// Map a raw safetensors header dtype STRING (as it appears in the JSON
98    /// header, e.g. `"BF16"`, `"F8_E4M3"`) to a [`WeightDtype`], factored out
99    /// so the RDMA weight loader (which receives dtype as a wire string in the
100    /// peer manifest, not a `safetensors::Dtype`) resolves it identically to
101    /// the disk loaders — byte-identity depends on the two ends agreeing.
102    pub fn from_safetensors_str(s: &str) -> Result<Self> {
103        Ok(match s {
104            "F32" => Self::FP32,
105            "BF16" => Self::BF16,
106            "U8" => Self::UInt8,
107            // I8 is a 1-byte raw container (packed NVFP4); signedness is
108            // irrelevant, treat as raw bytes exactly like the disk path.
109            "I8" => Self::UInt8,
110            "F8_E4M3" => Self::FP8E4M3,
111            "F8_E8M0" => Self::FP8E8M0,
112            "I64" => Self::Int64,
113            other => bail!("Unsupported safetensors dtype '{other}'"),
114        })
115    }
116}
117
118/// Convert a little-endian IEEE-754 half-precision (F16) tensor byte buffer
119/// to BF16 bytes. F16 and BF16 are both 2 bytes/element but have different
120/// bit layouts (5-bit vs 8-bit exponent), so the bytes cannot be
121/// reinterpreted — each value goes f16 → f32 (exact) → bf16
122/// (round-to-nearest-even). Shared by both disk loaders so F16 checkpoints
123/// (e.g. centml modelopt W4A4 exports, which ship all unquantized tensors as
124/// F16) land in the store as BF16; [`WeightDtype`] itself stays closed to
125/// store-legal dtypes and F16 can never appear on the RDMA wire.
126pub(crate) fn f16_to_bf16_bytes(src: &[u8]) -> Vec<u8> {
127    use half::{bf16, f16};
128    debug_assert_eq!(src.len() % 2, 0, "F16 tensor byte length must be even");
129    let mut out = Vec::with_capacity(src.len());
130    for pair in src.chunks_exact(2) {
131        let h = f16::from_le_bytes([pair[0], pair[1]]);
132        out.extend_from_slice(&bf16::from_f32(h.to_f32()).to_le_bytes());
133    }
134    out
135}
136
137/// A weight tensor on the GPU.
138pub struct WeightTensor {
139    pub ptr: DevicePtr,
140    pub shape: Vec<usize>,
141    pub dtype: WeightDtype,
142}
143
144impl WeightTensor {
145    pub fn num_elements(&self) -> usize {
146        self.shape.iter().product()
147    }
148
149    pub fn byte_size(&self) -> usize {
150        match self.dtype {
151            // Packed Q2_0: `n_blocks = numel / group` blocks of
152            // `2 + group/4` bytes (34 @ g128, 18 @ g64) — the on-disk footprint.
153            WeightDtype::PackedQ2_0 { group } => {
154                let g = group as usize;
155                debug_assert!(g == 128 || g == 64, "unexpected Q2_0 group {g}");
156                let n_blocks = self.num_elements() / g.max(1);
157                n_blocks * (2 + g / 4)
158            }
159            WeightDtype::Q2K => self.num_elements() / 256 * 84,
160            WeightDtype::Q3K => self.num_elements() / 256 * 110,
161            WeightDtype::Q6K => self.num_elements() / 256 * 210,
162            d => self.num_elements() * d.byte_size(),
163        }
164    }
165
166    /// The Q2_0 group size if this tensor is keep-packed ternary, else `None`.
167    pub fn q2_group(&self) -> Option<u16> {
168        match self.dtype {
169            WeightDtype::PackedQ2_0 { group } => Some(group),
170            _ => None,
171        }
172    }
173
174    /// True if this tensor holds keep-packed ternary Q2_0 blocks (id 42).
175    pub fn is_packed_q2(&self) -> bool {
176        matches!(self.dtype, WeightDtype::PackedQ2_0 { .. })
177    }
178}
179
180/// All model weights loaded onto the GPU, keyed by HuggingFace name.
181pub struct WeightStore {
182    weights: HashMap<String, WeightTensor>,
183    prepartitioned_tp: Option<(usize, usize)>,
184    /// Buffers a loader derived from these tensors — fused concats, transposed
185    /// twins, requants. Owned here so teardown RELEASES them instead of the
186    /// backend sweep reclaiming them unowned (#736, #915); see `derived.rs`.
187    derived: DerivedStore,
188    /// Tensors deliberately NOT uploaded, with where they live on disk.
189    ///
190    /// The n-gram embedding tables of the LongCat / Qwen3.8-Flash-Next family
191    /// are 63 GB (LongCat-Lite) to ~102 GB (Flash-Next) of BF16. Uploading
192    /// them through the generic path would exhaust a 121 GB unified box
193    /// before any quantization could run — and on GB10 the fallback is
194    /// `alloc_managed`, i.e. Linux swap, i.e. the documented kernel freeze.
195    /// They are skipped at load and served either by streaming per-table
196    /// quantize-on-load or straight off NVMe by `NgramRowCache`, both of
197    /// which need only this (path, offset) locator.
198    deferred: HashMap<String, DeferredTensor>,
199}
200
201mod deferred;
202pub use deferred::{DeferHook, DeferredTensor};
203mod kv_scale_keys;
204pub use kv_scale_keys::{
205    KvScaleCensus, KvScaleKeys, KvScaleSpelling, kv_scale_census, resolve_kv_scale_keys,
206};
207mod store;
208
209/// SBIO IORouter trait for weight loading.
210pub trait WeightLoader {
211    fn load(
212        &self,
213        model_dir: &Path,
214        gpu: &dyn GpuBackend,
215        oom_reserve_bytes: usize,
216    ) -> Result<WeightStore>;
217}
218
219/// Loads weights from safetensors files using mmap.
220pub struct SafetensorsLoader {
221    /// EP rank (0-based). Only used when ep_world_size > 1.
222    pub ep_rank: usize,
223    /// EP world size. When > 1, remote expert tensors are skipped.
224    pub ep_world_size: usize,
225    /// Total number of MoE experts in the model (for EP partitioning).
226    pub num_experts: usize,
227    /// Override for the peak memory multiplier in the pre-flight OOM check.
228    /// Set from QuantFormat::peak_memory_multiplier() in the caller.
229    /// When None, the pre-flight uses its own heuristic (1.3x NVFP4 / 1.5x FP8).
230    pub peak_memory_multiplier: Option<f64>,
231    /// Skip the W4A4 `*.input_scale` activation scales at load.
232    ///
233    /// ModelOpt NVFP4 checkpoints ship one 0-dim F32 scalar per quantized
234    /// projection. On a 512-expert model that is ~74k four-byte allocations,
235    /// each taking a full allocation granule — GBs of padding for values
236    /// Metrale Engine never reads, because it serves w4a16 (BF16 activations) and the
237    /// NVFP4 loader already treats the key as optional.
238    ///
239    /// OPT-IN: `step3p7` reads this key on its own path, so it must stay off
240    /// unless the model's loader is known not to need it.
241    pub skip_activation_scales: bool,
242    /// Skip `mtp.*` tensors at load.
243    ///
244    /// For models whose loader deliberately does not build an MTP head,
245    /// uploading its weights is pure waste — on Qwen3.8-Flash-Next that is a
246    /// 1.49 GB expert shard plus the MTP backbone, held resident while the KV
247    /// cache goes without.
248    ///
249    /// OPT-IN: a model that DOES build an MTP head must keep them, so this is
250    /// set only where `load_mtp_weights` is known to return `None`.
251    pub skip_mtp: bool,
252    /// Tensors the MODEL's weight loader will read from disk itself, so this
253    /// loader must record their location instead of uploading them. See
254    /// [`DeferHook`] for the contract and why a loader asks for it.
255    pub defer: Option<DeferHook>,
256}
257
258impl Default for SafetensorsLoader {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl SafetensorsLoader {
265    /// Create a loader with no expert parallelism (loads all tensors).
266    pub fn new() -> Self {
267        Self {
268            ep_rank: 0,
269            ep_world_size: 1,
270            num_experts: 0,
271            peak_memory_multiplier: None,
272            skip_activation_scales: false,
273            skip_mtp: false,
274            defer: None,
275        }
276    }
277
278    /// Create a loader with EP-aware filtering.
279    pub fn with_ep(ep_rank: usize, ep_world_size: usize, num_experts: usize) -> Self {
280        Self {
281            ep_rank,
282            ep_world_size,
283            num_experts,
284            peak_memory_multiplier: None,
285            skip_activation_scales: false,
286            skip_mtp: false,
287            defer: None,
288        }
289    }
290
291    /// Does the model's loader claim this tensor? `false` when no hook is set,
292    /// which is every model but the ones that opt in.
293    ///
294    /// 🪤 Consulted only for tensors `should_skip_tensor` KEPT — deferring a
295    /// tensor this rank was never going to load would put a remote expert's
296    /// location in the store and invite a binder to read it.
297    pub fn is_deferred(&self, name: &str, dtype: WeightDtype) -> bool {
298        self.defer.as_ref().is_some_and(|f| f(name, dtype))
299    }
300
301    /// Check if a tensor should be skipped under EP.
302    /// Skips `*.experts.{E}.*` tensors where E is not in local range.
303    /// MTP head experts are never skipped (small, fully replicated).
304    ///
305    /// 🪤 The MTP exemption keys on a leading `mtp.` — a DeepSeek-style name.
306    /// GLM-5.3 puts its MTP head at `model.language_model.layers.45.*` with no
307    /// `mtp.` prefix, so that layer's routed experts ARE sharded on GLM. Fine
308    /// while the MTP head is out of scope; revisit before enabling it.
309    ///
310    /// `pub` so residency can be PROVEN against a real checkpoint index
311    /// without collectives (see `metrale-model-layers/tests/glm53_ep_residency.rs`).
312    pub fn should_skip_tensor(&self, name: &str) -> bool {
313        // MTP head weights for a model whose loader does not build one.
314        if self.skip_mtp && name.starts_with("mtp.") {
315            return true;
316        }
317        // W4A4 activation scales: never read on the w4a16 path (the NVFP4
318        // loader falls back to `DevicePtr::NULL`), and 4-byte allocations are
319        // almost pure granule padding at expert scale.
320        if self.skip_activation_scales && name.ends_with(".input_scale") {
321            return true;
322        }
323        if self.ep_world_size <= 1 {
324            return false;
325        }
326        // MTP head experts are small — always replicate, never shard.
327        if name.starts_with("mtp.") {
328            return false;
329        }
330        // Parse expert index from patterns like "*.experts.42.gate_proj*"
331        if let Some(idx) = parse_expert_index(name) {
332            let per_rank = self.num_experts / self.ep_world_size;
333            let local_start = self.ep_rank * per_rank;
334            let local_end = if self.ep_rank == self.ep_world_size - 1 {
335                self.num_experts
336            } else {
337                local_start + per_rank
338            };
339            idx < local_start || idx >= local_end
340        } else {
341            false // Non-expert tensors are always loaded (replicated)
342        }
343    }
344}
345
346/// Split a tensor name into (everything but its last numeric path segment,
347/// that segment as a number) so names sort NUMERICALLY on the index.
348/// `embedders.2` must precede `embedders.10`; a plain lexicographic sort puts
349/// `10` first and silently mis-maps every table after the ninth.
350pub mod adapter;
351mod derived;
352pub use derived::DerivedStore;
353mod gguf;
354mod k3;
355mod loader;
356pub use gguf::dequant_cpu;
357pub use gguf::expert_stream;
358pub use gguf::{GgufLoader, GgufShardSet, config_from_gguf_dir, find_gguf, find_gguf_shards};
359pub use k3::K3SafetensorsLoader;
360pub(crate) use loader::estimate_load_bytes;
361// Platform-independent: consumed by the unix-only fast-weights (O_DIRECT) path
362// AND by the GGUF loader, which builds everywhere. Gating this on `unix` broke
363// the Windows CUDA build the moment `gguf.rs` started using it.
364pub(crate) use loader::check_oom_guard;
365// Consumed by the unix-only fast-weights (O_DIRECT) loader path.
366#[cfg(unix)]
367pub(crate) use loader::estimate_has_fp8;
368
369mod name_utils;
370pub(crate) use name_utils::split_trailing_index;
371pub use name_utils::{is_ngram_table, parse_expert_index};
372
373#[cfg(test)]
374mod packed_q2_tests;
375mod prefix_detect;
376pub use prefix_detect::auto_detect_weight_prefix;
377
378mod release;
379
380#[cfg(test)]
381mod teardown_tests;