1#![allow(unused_imports)]
9
10use anyhow::{Context, Result, bail, ensure};
11use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
12use metrale_model_weights::weights::{WeightDtype, WeightStore};
13
14use super::*;
15
16pub struct NemotronSsmWeights {
20 pub in_proj: QuantizedWeight,
22 pub out_proj: QuantizedWeight,
24 pub conv1d_weight: DenseWeight,
25 pub conv1d_bias: DenseWeight,
27 pub a_log: DenseWeight,
29 pub d_param: DenseWeight,
31 pub dt_bias: DenseWeight,
33 pub ssm_norm: DenseWeight,
34}
35
36#[derive(Debug, Clone, Copy)]
38pub struct NemotronExpertWeight {
39 pub up_proj: QuantizedWeight,
40 pub down_proj: QuantizedWeight,
41}
42
43impl NemotronExpertWeight {
44 pub fn null() -> Self {
45 Self {
46 up_proj: QuantizedWeight::null(),
47 down_proj: QuantizedWeight::null(),
48 }
49 }
50}
51
52pub struct NemotronMoeWeights {
54 pub gate: DenseWeight,
56 pub e_score_correction_bias: DenseWeight,
59 pub experts: Vec<NemotronExpertWeight>,
61 pub shared_up: QuantizedWeight,
63 pub shared_up_fp8: Option<Fp8Weight>,
68 pub shared_down: QuantizedWeight,
70 pub shared_down_fp8: Option<Fp8Weight>,
75 pub fc1_latent_proj: Option<DenseWeight>,
78 pub fc2_latent_proj: Option<DenseWeight>,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum NemotronSsmQuant {
86 Nvfp4,
88 Fp8,
90 Bf16,
92}
93
94pub fn load_nemotron_ssm(
100 store: &WeightStore,
101 _layer: usize,
102 gpu: &dyn GpuBackend,
103 layer_prefix: &str,
104) -> Result<(NemotronSsmWeights, NemotronSsmQuant)> {
105 let p = format!("{layer_prefix}.mixer");
106 let has_scale = store.contains(&format!("{p}.in_proj.weight_scale"));
107 let has_scale2 = store.contains(&format!("{p}.in_proj.weight_scale_2"));
108 let quant = if has_scale && has_scale2 {
109 NemotronSsmQuant::Nvfp4
110 } else if has_scale {
111 NemotronSsmQuant::Fp8
112 } else {
113 NemotronSsmQuant::Bf16
114 };
115 let in_proj = if quant == NemotronSsmQuant::Nvfp4 {
116 quantized(store, &format!("{p}.in_proj"), gpu)?
117 } else {
118 QuantizedWeight::null()
119 };
120 let out_proj = if quant == NemotronSsmQuant::Nvfp4 {
121 quantized(store, &format!("{p}.out_proj"), gpu)?
122 } else {
123 QuantizedWeight::null()
124 };
125 Ok((
126 NemotronSsmWeights {
127 in_proj,
128 out_proj,
129 conv1d_weight: dense(store, &format!("{p}.conv1d.weight"))?,
130 conv1d_bias: dense_bf16_as_f32(store, &format!("{p}.conv1d.bias"), gpu)?,
131 a_log: dense_bf16_as_f32(store, &format!("{p}.A_log"), gpu)?,
132 d_param: dense_bf16_as_f32(store, &format!("{p}.D"), gpu)?,
133 dt_bias: dense_bf16_as_f32(store, &format!("{p}.dt_bias"), gpu)?,
134 ssm_norm: dense(store, &format!("{p}.norm.weight"))?,
135 },
136 quant,
137 ))
138}
139
140pub fn load_nemotron_attention(
148 store: &WeightStore,
149 layer: usize,
150 gpu: &dyn GpuBackend,
151 layer_prefix: &str,
152) -> Result<(
153 AttentionWeights,
154 Option<QuantizedWeight>,
155 Option<QuantizedWeight>,
156 Option<QuantizedWeight>,
157 DenseWeight,
158 bool,
159)> {
160 let p = format!("{layer_prefix}.mixer");
161 let is_nvfp4 = store.contains(&format!("{p}.q_proj.weight_scale_2"));
162 let is_fp8 = !is_nvfp4 && store.contains(&format!("{p}.q_proj.weight_scale"));
163 let dummy = DenseWeight {
164 weight: DevicePtr::NULL,
165 };
166
167 let (q_dense, k_dense, v_dense, o_dense, o_proj, q_nvfp4, k_nvfp4, v_nvfp4) = if is_nvfp4 {
168 let q = quantized(store, &format!("{p}.q_proj"), gpu)?;
169 let k = quantized(store, &format!("{p}.k_proj"), gpu)?;
170 let v = quantized(store, &format!("{p}.v_proj"), gpu)?;
171 let o = quantized(store, &format!("{p}.o_proj"), gpu)?;
172 (dummy, dummy, dummy, dummy, o, Some(q), Some(k), Some(v))
173 } else {
174 let load_proj = |name: &str| -> Result<DenseWeight> {
175 let prefix = format!("{p}.{name}");
176 if store.contains(&format!("{prefix}.weight_scale")) {
177 dequant_fp8_to_bf16(store, &prefix, gpu)
178 } else {
179 dense(store, &format!("{prefix}.weight"))
180 }
181 };
182 let q = load_proj("q_proj")?;
183 let k = load_proj("k_proj")?;
184 let v = load_proj("v_proj")?;
185 let o = load_proj("o_proj")?;
186 if is_fp8 && layer < 2 {
187 tracing::info!("L{layer} Attention: FP8 → BF16 (runtime quantization to NVFP4)");
188 }
189 (q, k, v, o, QuantizedWeight::null(), None, None, None)
190 };
191
192 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu)?;
193 let attn = AttentionWeights {
194 q_proj: q_dense,
195 k_proj: k_dense,
196 v_proj: v_dense,
197 o_proj,
198 q_norm: dummy,
199 k_norm: dummy,
200 q_norm_full: None,
201 k_norm_full: None,
202 k_scale,
203 v_scale,
204 };
205 Ok((attn, q_nvfp4, k_nvfp4, v_nvfp4, o_dense, is_nvfp4))
206}
207
208pub fn load_nemotron_moe(
215 store: &WeightStore,
216 layer: usize,
217 num_experts: usize,
218 gpu: &dyn GpuBackend,
219 config: &metrale_config::ModelConfig,
220 absmax_k: Option<metrale_gpu_runtime::gpu::KernelHandle>,
221 quantize_k: Option<metrale_gpu_runtime::gpu::KernelHandle>,
222 stream: u64,
223 scratch: Option<DevicePtr>,
224 layer_prefix: &str,
225) -> Result<NemotronMoeWeights> {
226 let p = format!("{layer_prefix}.mixer");
227 let gate_name = format!("{p}.gate.weight");
228 let gate_w = store.get(&gate_name)?;
229 let gate = if gate_w.dtype == WeightDtype::FP32 {
230 dense_f32_as_bf16(store, &gate_name, gpu)?
231 } else {
232 DenseWeight { weight: gate_w.ptr }
233 };
234 let e_score_correction_bias = dense(store, &format!("{p}.gate.e_score_correction_bias"))?;
235
236 let shared_up_prefix = format!("{p}.shared_experts.up_proj");
238 let shared_up_has_s2 = store.contains(&format!("{shared_up_prefix}.weight_scale_2"));
239 let shared_up_has_s = store.contains(&format!("{shared_up_prefix}.weight_scale"));
240 let native_fp8_mode =
244 std::env::var("METRALE_NEMOTRON_NATIVE_FP8_SSM").unwrap_or_else(|_| "1".to_string());
245 let want_native_fp8 = matches!(native_fp8_mode.as_str(), "1" | "both" | "decode");
246 let shared_up_fp8 = if want_native_fp8 && !shared_up_has_s2 && shared_up_has_s {
247 match load_fp8_block_scaled_as_fp8weight(store, &shared_up_prefix, gpu) {
248 Ok(mut w) => {
249 let bytes = (w.n as usize) * (w.k as usize);
250 let owned = gpu.alloc(bytes)?;
251 gpu.copy_d2d(w.weight, owned, bytes)?;
252 w.weight = owned;
253 Some(w)
254 }
255 Err(e) => {
256 tracing::warn!("shared_up native FP8 unavailable ({e}) — using NVFP4 requant");
257 None
258 }
259 }
260 } else {
261 None
262 };
263 let shared_up = if shared_up_fp8.is_some() {
264 QuantizedWeight::null()
265 } else if shared_up_has_s2 {
266 quantized(store, &shared_up_prefix, gpu)?
267 } else {
268 let bf16 = if shared_up_has_s {
269 if let Some(s) = scratch {
270 dequant_fp8_to_bf16_into(store, &shared_up_prefix, gpu, s)?
271 } else {
272 dequant_fp8_to_bf16(store, &shared_up_prefix, gpu)?
273 }
274 } else {
275 dense(store, &format!("{shared_up_prefix}.weight"))?
276 };
277 quantize_to_nvfp4(
278 &bf16,
279 config.shared_expert_intermediate_size,
280 config.hidden_size,
281 gpu,
282 absmax_k.unwrap(),
283 quantize_k.unwrap(),
284 stream,
285 )?
286 };
287
288 let shared_down_prefix = format!("{p}.shared_experts.down_proj");
289 let shared_down_has_s2 = store.contains(&format!("{shared_down_prefix}.weight_scale_2"));
290 let shared_down_has_s = store.contains(&format!("{shared_down_prefix}.weight_scale"));
291 let shared_down_fp8 = if want_native_fp8 && !shared_down_has_s2 && shared_down_has_s {
292 match load_fp8_block_scaled_as_fp8weight(store, &shared_down_prefix, gpu) {
293 Ok(mut w) => {
294 let bytes = (w.n as usize) * (w.k as usize);
295 let owned = gpu.alloc(bytes)?;
296 gpu.copy_d2d(w.weight, owned, bytes)?;
297 w.weight = owned;
298 Some(w)
299 }
300 Err(e) => {
301 tracing::warn!("shared_down native FP8 unavailable ({e}) — using NVFP4 requant");
302 None
303 }
304 }
305 } else {
306 None
307 };
308 let shared_down = if shared_down_fp8.is_some() {
309 QuantizedWeight::null()
310 } else if shared_down_has_s2 {
311 quantized(store, &shared_down_prefix, gpu)?
312 } else {
313 let bf16 = if shared_down_has_s {
314 if let Some(s) = scratch {
315 dequant_fp8_to_bf16_into(store, &shared_down_prefix, gpu, s)?
316 } else {
317 dequant_fp8_to_bf16(store, &shared_down_prefix, gpu)?
318 }
319 } else {
320 dense(store, &format!("{shared_down_prefix}.weight"))?
321 };
322 quantize_to_nvfp4(
323 &bf16,
324 config.hidden_size,
325 config.shared_expert_intermediate_size,
326 gpu,
327 absmax_k.unwrap(),
328 quantize_k.unwrap(),
329 stream,
330 )?
331 };
332
333 let (fc1_latent_proj, fc2_latent_proj) = if config.moe_latent_size > 0 {
336 let fc1_prefix = format!("{p}.fc1_latent_proj");
337 let fc1 = if store.contains(&format!("{fc1_prefix}.weight_scale")) {
338 dequant_fp8_to_bf16(store, &fc1_prefix, gpu)?
339 } else {
340 dense(store, &format!("{fc1_prefix}.weight"))?
341 };
342 let fc2_prefix = format!("{p}.fc2_latent_proj");
343 let fc2 = if store.contains(&format!("{fc2_prefix}.weight_scale")) {
344 dequant_fp8_to_bf16(store, &fc2_prefix, gpu)?
345 } else {
346 dense(store, &format!("{fc2_prefix}.weight"))?
347 };
348 (Some(fc1), Some(fc2))
349 } else {
350 (None, None)
351 };
352
353 let moe_input = config.moe_input_size();
357 let moe_inter = config.moe_intermediate_size_for(layer);
358 let first_local = (0..num_experts).find(|e| config.is_local_expert(*e));
359 let experts_are_nvfp4 = first_local
360 .is_none_or(|e| store.contains(&format!("{p}.experts.{e}.up_proj.weight_scale_2")));
361 let experts_are_fp8 = !experts_are_nvfp4
362 && first_local
363 .is_some_and(|e| store.contains(&format!("{p}.experts.{e}.up_proj.weight_scale")));
364 if !experts_are_nvfp4 && layer < 2 {
365 tracing::info!(
366 "L{layer} MoE experts: {} → NVFP4 (runtime quantization, {} experts)",
367 if experts_are_fp8 { "FP8" } else { "BF16" },
368 num_experts,
369 );
370 }
371
372 let mut experts = Vec::with_capacity(num_experts);
373 for e in 0..num_experts {
374 if config.is_local_expert(e) {
375 let up_prefix = format!("{p}.experts.{e}.up_proj");
376 let down_prefix = format!("{p}.experts.{e}.down_proj");
377 let (up_proj, down_proj) = if experts_are_nvfp4 {
378 (
379 quantized(store, &up_prefix, gpu)?,
380 quantized(store, &down_prefix, gpu)?,
381 )
382 } else {
383 let up_bf16 = if experts_are_fp8 {
384 if let Some(s) = scratch {
385 dequant_fp8_to_bf16_into(store, &up_prefix, gpu, s)?
386 } else {
387 dequant_fp8_to_bf16(store, &up_prefix, gpu)?
388 }
389 } else {
390 dense(store, &format!("{up_prefix}.weight"))?
391 };
392 let up = quantize_to_nvfp4(
393 &up_bf16,
394 moe_inter,
395 moe_input,
396 gpu,
397 absmax_k.unwrap(),
398 quantize_k.unwrap(),
399 stream,
400 )?;
401 let down_bf16 = if experts_are_fp8 {
402 if let Some(s) = scratch {
403 dequant_fp8_to_bf16_into(store, &down_prefix, gpu, s)?
404 } else {
405 dequant_fp8_to_bf16(store, &down_prefix, gpu)?
406 }
407 } else {
408 dense(store, &format!("{down_prefix}.weight"))?
409 };
410 let down = quantize_to_nvfp4(
411 &down_bf16,
412 moe_input,
413 moe_inter,
414 gpu,
415 absmax_k.unwrap(),
416 quantize_k.unwrap(),
417 stream,
418 )?;
419 (up, down)
420 };
421 experts.push(NemotronExpertWeight { up_proj, down_proj });
422 } else {
423 experts.push(NemotronExpertWeight::null());
424 }
425 }
426
427 Ok(NemotronMoeWeights {
428 gate,
429 e_score_correction_bias,
430 experts,
431 shared_up,
432 shared_up_fp8,
433 shared_down_fp8,
434 shared_down,
435 fc1_latent_proj,
436 fc2_latent_proj,
437 })
438}