metrale_model_arch/weight_loader/qwen35_dense/
predicted_residency.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The derived-weight bytes of the native-FP8 dense route,
4//! predicted from the config and the environment before the checkpoint loads.
5//!
6//! Owner: model-arch weight loader (Qwen3.5 dense).
7//! Invariants:
8//! - `predicted_derived_bytes` reads no environment and allocates nothing;
9//!   `Fp8RouteInputs::from_env` does the reads.
10//! - A route whose plan keeps an NVFP4 copy (`METRALE_DENSE_FP8_KEEP_NVFP4`,
11//!   a CUTLASS NVFP4 attention lever, `METRALE_ATTN_W4A4`) gets
12//!   [`DerivedBytesEstimate::Unavailable`].
13//!
14//! The server's preflight (`post_load_yardstick` in
15//! `serve_phases/preflight/headroom.rs`) adds the prediction to the
16//! checkpoint's on-disk size to estimate the memory in use before the KV
17//! cache. When the prediction is unavailable, it uses pre-load free memory.
18//! Which copies are built is [`fp8_residency::DenseFp8Plan::resolve`]; their
19//! sizes are shape arithmetic over the config. The loader tallies NVFP4
20//! copies through `DerivedResidency::skip`, never `keep`, so a route that
21//! builds them is not predicted.
22
23use metrale_config::ModelConfig;
24
25use super::fp8_residency::{self, RouteEnv, TwinsBuilt};
26use metrale_model_layers::layers::qwen3_attention::Fp8TwinSet;
27use metrale_model_layers::weight_map::Nvfp4Variant;
28
29/// 2026-09-25: The derived bytes the native-FP8 dense loader will keep, by
30/// term.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub struct PredictedDerived {
33    /// 2026-09-25: `Fp8Weight::transpose_for_gemm` twins for the projections
34    /// [`fp8_residency::DenseFp8Plan`] selects, summed over full-attention
35    /// layers.
36    pub attn_fp8_twins: u64,
37    /// 2026-09-25: The fused `[QKV|Z]` FP8 weight, its block-scale grids, the
38    /// `out_proj` block-scale grid and the interleaved `in_proj_ba`, summed
39    /// over linear-attention layers (`ssm_concat_bytes`).
40    pub ssm_fp8_concat: u64,
41    /// 2026-09-25: The E4M3 bytes of the fused `[2*inter, hidden]` gate+up
42    /// weight, summed over layers; its scale grid is not counted. 0 when the
43    /// fusion is not selected.
44    pub ffn_gateup_fused: u64,
45    /// 2026-09-25: The `mlp.gate_proj.weight` and `mlp.up_proj.weight` bytes
46    /// that `prune_after_load` releases on the same layers. Equal to
47    /// [`Self::ffn_gateup_fused`]: the fused weight is those two tensors side
48    /// by side.
49    pub ffn_gateup_pruned: u64,
50    /// 2026-09-25: Which derived families the prediction expects.
51    pub twins: TwinsBuilt,
52    /// 2026-09-25: The twin set the attention term was priced at.
53    pub attn_twin_set: Fp8TwinSet,
54}
55
56impl PredictedDerived {
57    /// 2026-09-25: Derived bytes above the on-disk checkpoint size, which
58    /// `headroom.rs` adds to that size. The fusion enters as a difference,
59    /// which is zero: the on-disk size still counts the gate and up tensors
60    /// that `prune_after_load` releases once the fused copy exists
61    /// (`the_gateup_fusion_is_residency_neutral`).
62    pub fn total(&self) -> u64 {
63        self.attn_fp8_twins
64            + self.ssm_fp8_concat
65            + self.ffn_gateup_fused.saturating_sub(self.ffn_gateup_pruned)
66    }
67}
68
69/// 2026-09-25: A prediction, or the reason there is none.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum DerivedBytesEstimate {
72    /// 2026-09-25: The native-FP8 dense route will run, with these bytes.
73    NativeFp8Dense(PredictedDerived),
74    /// 2026-09-25: No prediction; the string says why, and the preflight
75    /// carries it in `Yardstick::PreLoadFree`.
76    Unavailable(&'static str),
77}
78
79impl DerivedBytesEstimate {
80    /// 2026-09-25: [`PredictedDerived::total`], or `None` when unavailable.
81    pub fn bytes(&self) -> Option<u64> {
82        match self {
83            Self::NativeFp8Dense(p) => Some(p.total()),
84            Self::Unavailable(_) => None,
85        }
86    }
87
88    pub fn reason(&self) -> Option<&'static str> {
89        match self {
90            Self::NativeFp8Dense(_) => None,
91            Self::Unavailable(why) => Some(*why),
92        }
93    }
94}
95
96/// 2026-09-25: The route conditions of `load_layers` that are known before the
97/// checkpoint loads.
98#[derive(Clone, Copy, Debug)]
99pub struct Fp8RouteInputs {
100    /// 2026-09-25: `METRALE_DENSE_FP8=1` (`qwen35_dense::dense_fp8_enabled`).
101    pub dense_fp8: bool,
102    /// 2026-09-25: `config.tp_world_size.max(1)`; the FP8 dense route needs 1.
103    pub tp_size: usize,
104    /// 2026-09-25: The variant the config declares (`config_declared_variant`),
105    /// or `None` when it declares none.
106    pub declared_variant: Option<Nvfp4Variant>,
107    /// 2026-09-25: `METRALE_NO_GDN_FP8` is unset, a clause of
108    /// `qwen35_dense::gdn_fp8_arm_selected`.
109    pub gdn_fp8: bool,
110    /// 2026-09-25: Both `qwen3_attention::W8A8_PREFILL_KERNELS` are loaded for
111    /// this target; it decides whether the Q and O FP8 twins are planned.
112    pub w8a8_prefill_kernels: bool,
113    /// 2026-09-25: The environment half of the loader's plan.
114    pub route: RouteEnv,
115    /// 2026-09-25: `gateup_fused::ffn_gateup_fused()`, which
116    /// `qwen35_dense::ffn_gateup_fused_selected` also requires.
117    pub ffn_gateup_fused: bool,
118}
119
120impl Fp8RouteInputs {
121    /// 2026-09-25: Resolve each condition from the environment and the config.
122    /// `w8a8_prefill_kernels` comes from the caller, which asks
123    /// `qwen3_attention::w8a8_prefill_kernels_loaded` of the loaded kernel set.
124    pub fn from_env(config: &ModelConfig, w8a8_prefill_kernels: bool) -> Self {
125        Self {
126            // 2026-09-25: The predicate of `qwen35_dense::dense_fp8_enabled`.
127            dense_fp8: std::env::var("METRALE_DENSE_FP8").as_deref() == Ok("1"),
128            tp_size: config.tp_world_size.max(1),
129            declared_variant: metrale_model_layers::weight_map::config_declared_variant(config),
130            // 2026-09-25: The predicate of `gdn_fp8_arm_selected`.
131            gdn_fp8: std::env::var_os("METRALE_NO_GDN_FP8").is_none(),
132            w8a8_prefill_kernels,
133            route: RouteEnv::from_env(),
134            ffn_gateup_fused:
135                metrale_model_layers::layers::dense_ffn::gateup_fused::ffn_gateup_fused(),
136        }
137    }
138}
139
140/// 2026-09-25: Bytes the native-FP8 dense loader will derive on top of the
141/// checkpoint, or why that is not predicted. Reads only its arguments.
142pub fn predicted_derived_bytes(
143    config: &ModelConfig,
144    route: &Fp8RouteInputs,
145) -> DerivedBytesEstimate {
146    // 2026-09-25: `factory::loader_for_config` picks `Qwen35DenseWeightLoader`
147    // when `is_qwen35_dense()` holds.
148    if !config.is_qwen35_dense() {
149        return DerivedBytesEstimate::Unavailable("not the Qwen3.5-dense loader");
150    }
151    if !route.dense_fp8 {
152        return DerivedBytesEstimate::Unavailable("METRALE_DENSE_FP8 is not 1");
153    }
154    if route.tp_size != 1 {
155        return DerivedBytesEstimate::Unavailable("--tp-size > 1 takes the NVFP4 route");
156    }
157    if route.declared_variant != Some(Nvfp4Variant::Fp8Dequanted) {
158        return DerivedBytesEstimate::Unavailable(
159            "config.json does not declare a block-scaled FP8 checkpoint",
160        );
161    }
162    if route.route.keep_nvfp4 {
163        return DerivedBytesEstimate::Unavailable(
164            "METRALE_DENSE_FP8_KEEP_NVFP4 restores the pre-#915 fallback copies",
165        );
166    }
167
168    // 2026-09-25: Decline when the plan keeps an NVFP4 copy (module docs).
169    // With `ffn_fp8` true, `ffn_nvfp4` is false, so only `attn_nvfp4` can hold.
170    let plan = fp8_residency::DenseFp8Plan::resolve(fp8_residency::DenseFp8Inputs {
171        ffn_fp8: true,
172        attn_fp8: true,
173        keep_nvfp4: false,
174        dispatch: route.route.dispatch,
175        w8a8_kernels: route.w8a8_prefill_kernels,
176        attn_w4a4: route.route.attn_w4a4,
177        attn_prefill_q_t: route.route.attn_prefill_q_t,
178    });
179    if plan.ffn_nvfp4 || plan.attn_nvfp4 {
180        return DerivedBytesEstimate::Unavailable(
181            "an NVFP4 fallback lever (METRALE_CUTLASS_NVFP4_* / METRALE_ATTN_W4A4) is set",
182        );
183    }
184
185    let hidden = config.hidden_size;
186    let (nh, hd) = (config.num_attention_heads, config.head_dim);
187    let nkv = config.num_key_value_heads;
188    let q_n = nh * hd * if config.attn_gated { 2 } else { 1 };
189    let attn_layers = config.num_attention_layers() as u64;
190    let attn_fp8_twins = attn_layers
191        * fp8_residency::attn_fp8_twin_bytes(plan.attn_fp8_twins, q_n, nkv * hd, nh * hd, hidden)
192            as u64;
193
194    let ssm_layers = config.num_ssm_layers() as u64;
195    let ssm_fp8_concat = if route.gdn_fp8 && ssm_layers > 0 {
196        ssm_layers * ssm_concat_bytes(config) as u64
197    } else {
198        0
199    };
200
201    // 2026-09-25: The clauses of `qwen35_dense::ffn_gateup_fused_selected` that
202    // need no checkpoint: the switch, a dense model, and both widths whole
203    // 128-blocks. `inter` falls back as `qwen35_dense::ffn_inter` does.
204    let inter = if config.intermediate_size > 0 {
205        config.intermediate_size
206    } else {
207        config.moe_intermediate_size
208    };
209    let ffn_layers = config.num_hidden_layers as u64;
210    let fused_here = route.ffn_gateup_fused
211        && config.num_experts == 0
212        && inter > 0
213        && inter.is_multiple_of(128)
214        && hidden.is_multiple_of(128);
215    let (ffn_gateup_fused, ffn_gateup_pruned) = if fused_here {
216        let (w, _scales) = fp8_residency::ffn_gateup_fused_parts(hidden, inter);
217        (ffn_layers * w as u64, ffn_layers * w as u64)
218    } else {
219        (0, 0)
220    };
221
222    DerivedBytesEstimate::NativeFp8Dense(PredictedDerived {
223        attn_fp8_twins,
224        ssm_fp8_concat,
225        ffn_gateup_fused,
226        ffn_gateup_pruned,
227        twins: TwinsBuilt {
228            ffn_nvfp4: false,
229            attn_nvfp4: false,
230            attn_fp8: plan.attn_fp8_twins.any() && attn_layers > 0,
231            ssm_fp8_concat: ssm_fp8_concat > 0,
232            ffn_gateup_fused: ffn_gateup_fused > 0,
233        },
234        attn_twin_set: plan.attn_fp8_twins,
235    })
236}
237
238/// 2026-09-25: What one native-FP8 GDN layer keeps beyond the checkpoint
239/// bytes: the four buffers the loader's native FP8 GDN arm adopts and counts
240/// with `residency.keep`.
241///
242/// - The fused `[QKV|Z]` E4M3 weight from `concat_fp8_block_scaled`.
243/// - Its FP32 block-scale grid: the two source grids side by side, so the sum
244///   of two grids, not one grid over the fused N.
245/// - The `out_proj` block-scale grid.
246/// - `in_proj_ba`, the `[2*nv, hidden]` BF16 interleave of `in_proj_a` and
247///   `in_proj_b`.
248///
249/// The per-projection source grids are freed after the concat, so they are
250/// not counted.
251fn ssm_concat_bytes(config: &ModelConfig) -> usize {
252    let hidden = config.hidden_size;
253    let block_grid = |n: usize, k: usize| n.div_ceil(128) * k.div_ceil(128) * 4;
254    let qkv_n = config.ssm_qkv_size();
255    let z_n = config.ssm_z_size();
256    let qkvz_bytes = config.ssm_qkvz_size() * hidden;
257    let qkvz_scale_bytes = block_grid(qkv_n, hidden) + block_grid(z_n, hidden);
258    let out_scale_bytes = block_grid(hidden, config.ssm_z_size());
259    let ba_bytes = config.linear_num_value_heads * 2 * hidden * 2;
260    qkvz_bytes + qkvz_scale_bytes + out_scale_bytes + ba_bytes
261}
262
263#[cfg(test)]
264#[path = "predicted_residency_tests.rs"]
265mod tests;