metrale_model_arch/weight_loader/mod.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The [`ModelWeightLoader`] trait, and one loader per architecture that turns a
4//! flat [`WeightStore`] into typed [`TransformerLayer`]s.
5//!
6//! Owner: model-arch weight loader.
7//! Invariants: none beyond the types.
8
9pub mod deepseek_v4;
10pub mod deepseek_v41;
11pub mod dflash_loader;
12mod gemma4;
13/// 2026-09-25: GLM-5.3 checkpoint tensor classification (`classify`).
14pub mod glm5_next;
15mod laguna;
16mod longcat;
17mod minimax;
18mod nemotron;
19mod nllb;
20mod qwen3;
21mod qwen35;
22mod qwen35_dense;
23mod qwen3_vl;
24#[cfg_attr(test, allow(unreachable_pub))]
25pub(crate) mod qwen4_exp;
26mod step3p7;
27
28pub use deepseek_v4::DeepSeekV4WeightLoader;
29pub use dflash_loader::{
30 DflashConfig, DflashLayerWeights, DflashSubConfig, DflashWeights, load_dflash_weights,
31 store_has_dflash_weights,
32};
33pub mod glm5_next_load;
34pub mod glm5_next_mtp;
35pub mod glm5_next_vision;
36pub use gemma4::Gemma4WeightLoader;
37pub use glm5_next_load::Glm5NextWeightLoader;
38pub(crate) use glm5_next_mtp::Glm5NextMtpModule;
39
40pub use laguna::LagunaWeightLoader;
41pub use longcat::LongcatWeightLoader;
42pub use minimax::MinimaxM2WeightLoader;
43pub use nemotron::NemotronHWeightLoader;
44pub use nllb::NllbWeightLoader;
45pub use qwen3::Qwen3WeightLoader;
46pub use qwen3_vl::Qwen3VLWeightLoader;
47pub use qwen4_exp::Qwen4ExpWeightLoader;
48pub use qwen35::Qwen35WeightLoader;
49pub use qwen35_dense::Qwen35DenseWeightLoader;
50/// 2026-09-25: The native-FP8 dense loader's derived-copy decision table and the
51/// shape arithmetic that prices it.
52pub use qwen35_dense::fp8_residency;
53/// 2026-09-25: That table evaluated from the config before the checkpoint
54/// loads; preflight's headroom check reads it.
55pub use qwen35_dense::predicted_residency;
56pub use step3p7::Step3p7WeightLoader;
57
58use anyhow::Result;
59use metrale_cache::kv_cache::KvCacheDtype;
60use metrale_config::ModelConfig;
61use metrale_gpu_runtime::gpu::GpuBackend;
62use metrale_model_weights::weights::{DeferHook, WeightStore};
63
64use metrale_model_layers::layer::TransformerLayer;
65use metrale_model_layers::layers::VisionTower;
66use metrale_model_layers::weight_map::{
67 DenseWeight, MtpWeights, Nvfp4Variant, detect_nvfp4_variant,
68};
69
70/// 2026-09-25: Can this box hold the transposed MoE prefill copies for every
71/// layer, and does the operator want them? Without them MoE prefill uses the
72/// fallback grouped GEMM. The qwen3, qwen3_vl and gemma4 loaders ask here.
73///
74/// `METRALE_MOE_PREFILL_COPIES=0` forces the fallback. Any other value, or
75/// unset, builds them when their NVFP4 bytes for all `num_hidden_layers` fit in
76/// the measured free memory less 2 GiB; if free memory cannot be read, they are
77/// not built.
78pub(crate) fn moe_prefill_copies_fit(config: &ModelConfig, gpu: &dyn GpuBackend) -> bool {
79 if std::env::var("METRALE_MOE_PREFILL_COPIES").ok().as_deref() == Some("0") {
80 tracing::info!("METRALE_MOE_PREFILL_COPIES=0: MoE prefill uses the fallback grouped GEMM");
81 return false;
82 }
83 let inter = config.moe_intermediate_size;
84 let h = config.hidden_size;
85 // 2026-09-25: NVFP4: one scale byte per 16 elements beside the packed
86 // pairs (half a byte per element).
87 let group_size = 16usize;
88 let gu_bytes = inter * h / 2 + inter * h / group_size;
89 let d_bytes = h * inter / 2 + h * inter / group_size;
90 let per_layer = config.num_experts * (2 * gu_bytes + d_bytes);
91 let total = per_layer * config.num_hidden_layers;
92 let available = gpu.free_memory().unwrap_or(0);
93 let headroom = 2 * 1024 * 1024 * 1024;
94 let fits = total <= available.saturating_sub(headroom);
95 if !fits {
96 tracing::warn!(
97 "Skipping MoE weight transposition ({:.1} GB needed, {:.1} GB available). \
98 Prefill will use fallback grouped GEMM.",
99 total as f64 / (1024.0 * 1024.0 * 1024.0),
100 available as f64 / (1024.0 * 1024.0 * 1024.0),
101 );
102 }
103 fits
104}
105
106/// 2026-09-25: Runtime quantization format of a loader's weights, which the
107/// qwen3 and qwen35 loaders resolve from the checkpoint's NVFP4 variant.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum QuantFormat {
110 /// 2026-09-25: NVFP4 E2M1; every variant other than `Fp8Dequanted`.
111 Nvfp4,
112 /// 2026-09-25: FP8 E4M3 block-scaled, served natively (`Fp8Dequanted`).
113 Fp8,
114}
115
116impl QuantFormat {
117 /// 2026-09-25: Peak GPU memory as a multiple of on-disk weight bytes, for an
118 /// OOM pre-flight estimate.
119 pub fn peak_memory_multiplier(&self) -> f64 {
120 match self {
121 Self::Nvfp4 => 1.15,
122 Self::Fp8 => 1.5,
123 }
124 }
125}
126
127/// 2026-09-25: On-disk weight format, derived from `detect_nvfp4_variant`.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum WeightFormat {
130 /// 2026-09-25: ModelOpt (`Standard`) or compressed-tensors NVFP4 on disk,
131 /// and `Bf16Raw` checkpoints, which the loader quantises to NVFP4.
132 Nvfp4,
133 /// 2026-09-25: Block-scaled FP8 E4M3 on disk (`Fp8Dequanted`).
134 Fp8BlockScaled,
135 /// 2026-09-25: BF16 on disk. `WeightFormat::detect` never returns it.
136 Bf16Dense,
137}
138
139impl WeightFormat {
140 /// 2026-09-25: Detect the weight format from a [`WeightStore`] by probing
141 /// tensor names.
142 pub fn detect(store: &WeightStore, config: &ModelConfig) -> Self {
143 match detect_nvfp4_variant(store, config) {
144 Nvfp4Variant::Fp8Dequanted => Self::Fp8BlockScaled,
145 Nvfp4Variant::CompressedTensors | Nvfp4Variant::Standard => Self::Nvfp4,
146 // 2026-09-25: A `Bf16Raw` weight is quantized to NVFP4 at load.
147 Nvfp4Variant::Bf16Raw => Self::Nvfp4,
148 }
149 }
150
151 pub fn is_fp8(&self) -> bool {
152 matches!(self, Self::Fp8BlockScaled)
153 }
154}
155
156/// 2026-09-25: Loads weights from a [`WeightStore`] into typed layer objects.
157pub trait ModelWeightLoader {
158 /// 2026-09-25: Whether this loader's weight slicing honours
159 /// `config.tp_world_size` / `config.tp_rank`. There is no default, so every
160 /// loader declares it.
161 ///
162 /// With `--tp-size > 1`, startup (`serve_phases/topology.rs`) refuses a
163 /// loader that returns `false`. `weight_loader/minimax.rs` is a loader
164 /// that returns `true`.
165 fn supports_tp(&self) -> bool;
166
167 /// 2026-09-25: Load all transformer layers from the weight store.
168 ///
169 /// `layer_kv_dtypes` holds one KV cache dtype per attention layer, indexed
170 /// by the layer's 0-based ordinal among the attention layers.
171 fn load_layers(
172 &self,
173 store: &WeightStore,
174 config: &ModelConfig,
175 gpu: &dyn GpuBackend,
176 layer_kv_dtypes: &[KvCacheDtype],
177 ) -> Result<Vec<Box<dyn TransformerLayer>>>;
178
179 /// 2026-09-25: Drop store tensors this loader has finished with. `factory::build`
180 /// calls it after every `load_*` reader and before the KV budget is
181 /// computed, so the freed memory counts.
182 ///
183 /// The default keeps everything, which is right for a loader that binds
184 /// the store's device pointers zero-copy. A loader that uploads its own
185 /// copies (a TP shard, a host round-trip, a dtype conversion) overrides it
186 /// to free the originals.
187 fn prune_after_load(
188 &self,
189 _store: &mut WeightStore,
190 _config: &ModelConfig,
191 _gpu: &dyn GpuBackend,
192 ) -> Result<()> {
193 Ok(())
194 }
195
196 /// 2026-09-25: Per-(layer, role) weight precision schedule. The default is
197 /// `PrecisionSchedule::default()`, in which every role is `Dtype::Inherit`.
198 fn precision_schedule(
199 &self,
200 _config: &ModelConfig,
201 ) -> crate::precision_schedule::PrecisionSchedule {
202 crate::precision_schedule::PrecisionSchedule::default()
203 }
204
205 fn load_embedding(
206 &self,
207 store: &WeightStore,
208 config: &ModelConfig,
209 gpu: &dyn GpuBackend,
210 ) -> Result<DenseWeight>;
211
212 /// 2026-09-25: Build the n-gram embedding, for an architecture that fuses
213 /// hashed n-gram lookups into the input embedding (the LongCat loader).
214 /// The default `None` leaves the plain `embed_tokens` gather in place.
215 fn load_ngram_embedding(
216 &self,
217 _store: &WeightStore,
218 _config: &ModelConfig,
219 _gpu: &dyn GpuBackend,
220 _max_tokens: usize,
221 ) -> Result<Option<metrale_model_layers::layers::ngram_embed::NgramEmbedding>> {
222 Ok(None)
223 }
224 /// 2026-09-25: Load the final RMSNorm weight used before the LM head.
225 fn load_final_norm(
226 &self,
227 store: &WeightStore,
228 config: &ModelConfig,
229 gpu: &dyn GpuBackend,
230 ) -> Result<DenseWeight>;
231 fn load_lm_head(
232 &self,
233 store: &WeightStore,
234 config: &ModelConfig,
235 gpu: &dyn GpuBackend,
236 ) -> Result<DenseWeight>;
237
238 /// 2026-09-25: Load single-module MTP head weights, or `None`.
239 fn load_mtp_weights(
240 &self,
241 store: &WeightStore,
242 config: &ModelConfig,
243 gpu: &dyn GpuBackend,
244 ) -> Result<Option<MtpWeights>>;
245
246 /// 2026-09-25: Load every MTP module; `factory::build` calls this one.
247 /// The default wraps `load_mtp_weights` (zero or one module). The MiniMax
248 /// and Step3p7 loaders override it.
249 fn load_mtp_weights_multi(
250 &self,
251 store: &WeightStore,
252 config: &ModelConfig,
253 gpu: &dyn GpuBackend,
254 ) -> Result<Vec<MtpWeights>> {
255 Ok(self
256 .load_mtp_weights(store, config, gpu)?
257 .into_iter()
258 .collect())
259 }
260
261 /// 2026-09-25: Per-attention-layer `(num_kv_heads, head_dim)`, indexed like
262 /// `layer_kv_dtypes`, for models whose attention geometry differs per layer
263 /// (the Gemma-4 loader). The default is empty: every layer uses the global
264 /// dims.
265 fn kv_layer_dims(&self, _config: &ModelConfig) -> Vec<(usize, usize)> {
266 Vec::new()
267 }
268
269 /// 2026-09-25: Load DFlash drafter weights from the drafter's own
270 /// `WeightStore`. The default returns `None`, and no loader overrides it:
271 /// `factory::build` loads the drafter with the free function
272 /// `dflash_loader::load_dflash_weights`.
273 fn load_dflash_weights(
274 &self,
275 _drafter_store: &WeightStore,
276 _config: &ModelConfig,
277 _gpu: &dyn GpuBackend,
278 _tp_size: usize,
279 ) -> Result<Option<DflashWeights>> {
280 Ok(None)
281 }
282
283 /// 2026-09-25: Load startup-static PEFT LoRA adapters into the LoRA pool,
284 /// through `metrale_model_layers::lora::load_lora_adapters_multi`.
285 /// `factory::build` calls it before the buffer arena and the KV budget, so
286 /// the pool counts as used memory.
287 fn load_lora_adapters(
288 &self,
289 adapters: &[metrale_model_layers::lora::LoraAdapterInput<'_>],
290 config: &ModelConfig,
291 gpu: &dyn GpuBackend,
292 max_loras: usize,
293 max_lora_rank: usize,
294 ) -> Result<Option<metrale_model_layers::lora::LoraWeights>> {
295 metrale_model_layers::lora::load_lora_adapters_multi(
296 adapters,
297 config,
298 gpu,
299 max_loras,
300 max_lora_rank,
301 )
302 .map(Some)
303 }
304
305 /// 2026-09-25: Will this loader bind a vision encoder for a multimodal
306 /// checkpoint?
307 ///
308 /// Default `true`, so a loader that does not override it never loses a
309 /// weight. On `false` the checkpoint loader skips the tower's tensors
310 /// (`serve_phases/weights.rs`). `factory::build` frees an unbound tower
311 /// afterwards either way, keyed off the bind result, so this only lowers
312 /// peak memory.
313 fn binds_vision_encoder(&self) -> bool {
314 true
315 }
316
317 /// 2026-10-02: Does this loader read the serve's MoE expert-table decision
318 /// (`metrale_model_layers::layers::MoeExpertTables`)? The serve plans and publishes it before
319 /// load only for a loader that does.
320 fn reads_expert_table_plan(&self) -> bool {
321 false
322 }
323
324 /// 2026-09-25: Which checkpoint tensors will this loader read from the host
325 /// at bind time, so the checkpoint loader records their location instead
326 /// of uploading them?
327 ///
328 /// Default `None`: upload everything. `serve_phases/weights.rs` asks once,
329 /// before the load. A deferred tensor is not skipped: it is recorded in
330 /// `WeightStore::deferred`, and the loader that asked reads it. See
331 /// [`metrale_model_weights::weights::DeferHook`] for the contract.
332 fn defer_predicate(&self, _config: &ModelConfig) -> Option<DeferHook> {
333 None
334 }
335
336 /// 2026-09-25: Load vision encoder weights; the default returns `None`.
337 fn load_vision_encoder(
338 &self,
339 _store: &WeightStore,
340 _config: &ModelConfig,
341 _gpu: &dyn GpuBackend,
342 ) -> Result<Option<VisionTower>> {
343 Ok(None)
344 }
345}