1use anyhow::Result;
15use metrale_cache::kv_cache::KvCacheDtype;
16use metrale_config::{LayerType, ModelConfig};
17use metrale_gpu_runtime::gpu::GpuBackend;
18use metrale_model_weights::weights::{WeightDtype, WeightStore};
19
20use super::{ModelWeightLoader, WeightFormat};
21use metrale_model_layers::layer::TransformerLayer;
22use metrale_model_layers::weight_map::loaders_moe::load_mtp;
23use metrale_model_layers::weight_map::{
24 DenseWeight, Fp8Weight, MtpWeights, Nvfp4Variant, PackedQ2Weight, dense, detect_nvfp4_variant,
25};
26
27fn proj_q2_group(store: &WeightStore, prefix: &str) -> Option<u16> {
31 store
32 .get(&format!("{prefix}.weight"))
33 .ok()
34 .and_then(|w| w.q2_group())
35}
36
37fn packed_q2_from_store(store: &WeightStore, prefix: &str) -> Result<PackedQ2Weight> {
41 let w = store.get(&format!("{prefix}.weight"))?;
42 let group = w
43 .q2_group()
44 .ok_or_else(|| anyhow::anyhow!("{prefix}.weight is not keep-packed Q2_0"))?;
45 anyhow::ensure!(
46 w.shape.len() == 2,
47 "packed Q2_0 {prefix}.weight must be 2D, got {:?}",
48 w.shape
49 );
50 Ok(PackedQ2Weight {
51 weight: w.ptr,
52 n: w.shape[0] as u32,
53 k: w.shape[1] as u32,
54 group,
55 })
56}
57
58fn proj_is_native_fp8(store: &WeightStore, prefix: &str) -> bool {
62 let is_fp8_weight = store
63 .get(&format!("{prefix}.weight"))
64 .map(|w| w.dtype == WeightDtype::FP8E4M3)
65 .unwrap_or(false);
66 let has_block_scale = store.contains(&format!("{prefix}.weight_scale_inv"))
67 || store
68 .get(&format!("{prefix}.weight_scale"))
69 .map(|s| s.shape.len() == 2)
70 .unwrap_or(false);
71 is_fp8_weight && has_block_scale
72}
73
74pub(super) fn proj_is_fp8_any_scale(store: &WeightStore, prefix: &str) -> bool {
82 let Ok(w) = store.get(&format!("{prefix}.weight")) else {
83 return false;
84 };
85 if w.dtype != WeightDtype::FP8E4M3 || w.shape.len() != 2 {
86 return false;
87 }
88 let (n, k) = (w.shape[0], w.shape[1]);
89
90 for key in [
91 format!("{prefix}.weight_scale_inv"),
92 format!("{prefix}.weight_scale"),
93 ] {
94 let Ok(s) = store.get(&key) else { continue };
95 if s.num_elements() == 1 {
96 return true;
97 }
98 if s.shape.len() == 2 && s.shape[0] == n.div_ceil(128) && s.shape[1] == k.div_ceil(128) {
99 return true;
100 }
101 }
102 false
103}
104
105fn concat_fp8_block_scaled(
110 a: &Fp8Weight,
111 b: &Fp8Weight,
112 k: usize,
113 gpu: &dyn GpuBackend,
114) -> Result<Fp8Weight> {
115 let kb = k.div_ceil(128);
116 let a_w = a.n as usize * k;
117 let b_w = b.n as usize * k;
118 let weight = gpu.alloc(a_w + b_w)?;
119 gpu.copy_d2d(a.weight, weight, a_w)?;
120 gpu.copy_d2d(b.weight, weight.offset(a_w), b_w)?;
121 let a_s = (a.n as usize).div_ceil(128) * kb * 4;
122 let b_s = (b.n as usize).div_ceil(128) * kb * 4;
123 let row_scale = gpu.alloc(a_s + b_s)?;
124 gpu.copy_d2d(a.row_scale, row_scale, a_s)?;
125 gpu.copy_d2d(b.row_scale, row_scale.offset(a_s), b_s)?;
126 Ok(Fp8Weight {
127 weight,
128 row_scale,
129 n: a.n + b.n,
130 k: k as u32,
131 scale_format: metrale_model_layers::weight_map::WeightQuantFormat::Fp8BlockScaled,
132 })
133}
134
135fn dense_fp8_enabled() -> bool {
139 std::env::var("METRALE_DENSE_FP8").as_deref() == Ok("1")
140}
141
142fn gdn_fp8_arm_selected(store: &WeightStore, la: &str, tp_size: usize) -> bool {
147 let q2 = tp_size.max(1) == 1
148 && std::env::var_os("METRALE_NO_Q2_GDN").is_none()
149 && proj_q2_group(store, &format!("{la}.in_proj_qkv")).is_some()
150 && proj_q2_group(store, &format!("{la}.in_proj_z")).is_some();
151 !q2 && std::env::var_os("METRALE_NO_GDN_FP8").is_none()
152 && proj_is_fp8_any_scale(store, &format!("{la}.in_proj_qkv"))
153 && proj_is_fp8_any_scale(store, &format!("{la}.in_proj_z"))
154 && proj_is_fp8_any_scale(store, &format!("{la}.out_proj"))
155}
156
157fn ffn_inter(config: &ModelConfig) -> usize {
160 if config.intermediate_size > 0 {
161 config.intermediate_size
162 } else {
163 config.moe_intermediate_size
164 }
165}
166
167fn ffn_fp8_arm_selected(
172 store: &WeightStore,
173 config: &ModelConfig,
174 variant: Nvfp4Variant,
175 lp: &str,
176) -> bool {
177 dense_fp8_enabled()
178 && config.tp_world_size.max(1) == 1
179 && matches!(variant, Nvfp4Variant::Fp8Dequanted)
180 && proj_is_native_fp8(store, &format!("{lp}.mlp.gate_proj"))
181}
182
183fn ffn_gateup_fused_selected(
190 store: &WeightStore,
191 config: &ModelConfig,
192 variant: Nvfp4Variant,
193 lp: &str,
194) -> bool {
195 let inter = ffn_inter(config);
196 let hidden = config.hidden_size;
197 let on_disk = |name: &str| {
201 store
202 .get(&format!("{lp}.mlp.{name}.weight"))
203 .is_ok_and(|w| w.shape == [inter, hidden])
204 };
205 ffn_fp8_arm_selected(store, config, variant, lp)
206 && metrale_model_layers::layers::dense_ffn::gateup_fused::ffn_gateup_fused()
207 && config.num_experts == 0
208 && inter > 0
209 && inter.is_multiple_of(128)
210 && hidden.is_multiple_of(128)
211 && on_disk("gate_proj")
212 && on_disk("up_proj")
213}
214
215pub mod fp8_residency;
219mod loaders_b;
220pub mod predicted_residency;
221mod rowwise_fp8;
222
223mod attn_arms;
224mod attn_layer;
225mod ffn_arm;
226mod gdn_dequant;
227mod gdn_layer;
228mod load_cx;
229mod prune;
230mod served_formats;
231mod w8a8_install;
232
233use fp8_residency::{DerivedResidency, RouteEnv};
234use load_cx::{Flow, LayerIn, LoadCx};
235
236pub struct Qwen35DenseWeightLoader;
237
238impl ModelWeightLoader for Qwen35DenseWeightLoader {
239 fn supports_tp(&self) -> bool {
240 true
245 }
246
247 fn load_layers(
248 &self,
249 store: &WeightStore,
250 config: &ModelConfig,
251 gpu: &dyn GpuBackend,
252 layer_kv_dtypes: &[KvCacheDtype],
253 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
254 let layer_types = if config.layer_types.is_empty() {
255 (0..config.num_hidden_layers)
256 .map(|i| config.layer_type(i))
257 .collect::<Vec<_>>()
258 } else {
259 config.layer_types.clone()
260 };
261
262 let mut layers: Vec<Box<dyn TransformerLayer>> =
263 Vec::with_capacity(config.num_hidden_layers);
264 let mut attn_idx = 0usize;
265
266 let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
267 let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
268 let stream = gpu.default_stream();
269 let h = config.hidden_size;
270
271 let variant = detect_nvfp4_variant(store, config);
272 let weight_format = WeightFormat::detect(store, config);
273 tracing::info!(
274 "Weight format: {:?}, NVFP4 variant: {:?}",
275 weight_format,
276 variant
277 );
278
279 let fp8_ssm_prefill = std::env::var_os("METRALE_NO_GDN_FP8_PREFILL").is_none();
282 let bf16_to_fp8_k = if fp8_ssm_prefill {
283 tracing::info!(
284 "SSM in_proj_qkv + out_proj via native FP8 prefill GEMM \
285 (BF16 act × FP8 weight via fp8_gemm_n128). PREFILL ONLY: \
286 decode + batched verify read the NVFP4 copy — weight-streaming \
287 GEMV at M<=8, tile GEMM above it (trait_decode_batched.rs). \
288 The FP8 copy reaches decode only at M<=8 on a build whose \
289 batched NVFP4 GEMVs are absent"
290 );
291 Some(gpu.kernel("w4a16", "bf16_to_fp8")?)
292 } else {
293 None
294 };
295
296 let mem_profile = std::env::var("METRALE_MEM_PROFILE").is_ok();
299 let log_free = |tag: &str| {
300 if mem_profile && let Ok(free) = gpu.free_memory() {
301 tracing::info!("MEM_PROFILE[{tag}]: {:.2} GB GPU-free", free as f64 / 1e9);
302 }
303 };
304 log_free("dense-load-start");
305
306 let route_env = RouteEnv::from_env();
309 let mut residency = DerivedResidency::default();
310 let policy = load_cx::weight_quant_policy(config);
311 let served = std::cell::RefCell::new(served_formats::ServedFormats::default());
312 let cx = LoadCx {
313 store,
314 config,
315 gpu,
316 layer_kv_dtypes,
317 variant,
318 absmax_k,
319 quantize_k,
320 stream,
321 h,
322 bf16_to_fp8_k,
323 route_env: &route_env,
324 policy,
325 served: &served,
326 };
327
328 for (i, lt) in layer_types.iter().enumerate() {
329 if i % 8 == 0 {
330 log_free(&format!("layer-{i}"));
331 }
332 let lp = config.layer_prefix(i);
333 let input_norm = dense(store, &format!("{lp}.input_layernorm.weight"))?;
334 let post_attn_norm = dense(store, &format!("{lp}.post_attention_layernorm.weight"))?;
335 let ffn = ffn_arm::build_dense_ffn(&cx, &mut residency, i, &lp)?;
336
337 match lt {
338 LayerType::FullAttention => {
339 let l = LayerIn {
340 i,
341 lp: &lp,
342 input_norm,
343 post_attn_norm,
344 ffn,
345 };
346 if let Flow::Continue = attn_layer::load_full_attention(
347 &cx,
348 &mut residency,
349 &mut layers,
350 &mut attn_idx,
351 l,
352 )? {
353 continue;
354 }
355 }
356 LayerType::LinearAttention => {
357 let l = LayerIn {
358 i,
359 lp: &lp,
360 input_norm,
361 post_attn_norm,
362 ffn,
363 };
364 if let Flow::Continue =
365 gdn_layer::load_linear_attention(&cx, &mut residency, &mut layers, l)?
366 {
367 continue;
368 }
369 }
370 LayerType::SlidingAttention => {
371 unreachable!("unexpected SlidingAttention in this loader")
372 }
373 LayerType::Moe => unreachable!("Qwen3.5 dense has no standalone MoE layers"),
374 LayerType::SparseAttention => anyhow::bail!(
378 "layer {i}: SparseAttention needs a DSA indexer and per-query top-k; Qwen3.5 dense has neither"
379 ),
380 }
381
382 if (i + 1) % 10 == 0 {
383 tracing::info!("Loaded layers 0..{}", i + 1);
384 metrale_telemetry::progress::layer(i + 1, config.num_hidden_layers);
385 }
386 }
387
388 cx.install_w8a8_and_log_served(&layer_types, &mut layers)?;
389 tracing::info!(
390 "Qwen3.5 dense weight loader: {} layers ({} attention, {} SSM, dense FFN)",
391 layers.len(),
392 attn_idx,
393 layers.len() - attn_idx,
394 );
395 tracing::info!("{}", residency.summary(store.resident_bytes()));
399 for (label, bytes, count) in store.derived().by_label() {
400 tracing::debug!(
401 " derived-weight owner: {:>9.1} MB x{:<5} {label}",
402 bytes as f64 / (1024.0 * 1024.0),
403 count,
404 );
405 }
406
407 Ok(layers)
408 }
409
410 fn prune_after_load(
416 &self,
417 store: &mut WeightStore,
418 config: &ModelConfig,
419 gpu: &dyn GpuBackend,
420 ) -> Result<()> {
421 let doomed = prune::doomed_after_load(store, config);
422 if doomed.is_empty() {
423 return Ok(());
424 }
425 let (count, bytes) = store.free_matching(gpu, |name| doomed.contains(name))?;
426 tracing::info!(
427 "native FP8: released {count} store tensors ({:.2} GB) consumed by the fused [QKV|Z] SSM concat, the BA interleave and the dense-FFN gate+up fusion; out_proj/conv1d/A_log/dt_bias/norm/down_proj kept (still aliased)",
428 bytes as f64 / 1e9,
429 );
430 Ok(())
431 }
432
433 fn load_embedding(
434 &self,
435 store: &WeightStore,
436 config: &ModelConfig,
437 _gpu: &dyn GpuBackend,
438 ) -> Result<DenseWeight> {
439 loaders_b::load_embedding(store, config)
440 }
441
442 fn load_final_norm(
443 &self,
444 store: &WeightStore,
445 config: &ModelConfig,
446 _gpu: &dyn GpuBackend,
447 ) -> Result<DenseWeight> {
448 loaders_b::load_final_norm(store, config)
449 }
450
451 fn load_lm_head(
452 &self,
453 store: &WeightStore,
454 config: &ModelConfig,
455 gpu: &dyn GpuBackend,
456 ) -> Result<DenseWeight> {
457 loaders_b::load_lm_head(store, config, gpu)
458 }
459
460 fn load_mtp_weights(
461 &self,
462 store: &WeightStore,
463 config: &ModelConfig,
464 gpu: &dyn GpuBackend,
465 ) -> Result<Option<MtpWeights>> {
466 if !store.contains("mtp.fc.weight") {
467 return Ok(None);
468 }
469 let variant = detect_nvfp4_variant(store, config);
470 tracing::info!(
471 "Loading dense MTP weights (variant={:?}, hidden={}, inter={})",
472 variant,
473 config.hidden_size,
474 config.intermediate_size,
475 );
476 let mtp = load_mtp(store, config.num_experts, gpu, variant)?;
479 if mtp.dense_ffn.is_some() {
480 tracing::info!("Dense MTP head ready (FP8 e4m3 projections + dense gate/up/down MLP)");
481 } else {
482 tracing::info!(
483 "MoE MTP head ready ({} experts) — dense loader sees MoE bundle",
484 mtp.experts.len(),
485 );
486 }
487 Ok(Some(mtp))
488 }
489
490 fn load_vision_encoder(
491 &self,
492 store: &WeightStore,
493 config: &ModelConfig,
494 gpu: &dyn GpuBackend,
495 ) -> Result<Option<metrale_model_layers::layers::VisionTower>> {
496 super::qwen35::Qwen35WeightLoader.load_vision_encoder(store, config, gpu)
499 }
500}