1use anyhow::{Context, Result};
21use metrale_cache::kv_cache::KvCacheDtype;
22use metrale_config::{LayerType, ModelConfig};
23use metrale_gpu_runtime::gpu::GpuBackend;
24use metrale_model_weights::weights::WeightStore;
25
26use crate::weight_loader::ModelWeightLoader;
27use metrale_model_layers::layer::TransformerLayer;
28use metrale_model_layers::weight_map::{DenseWeight, MtpWeights, dense};
29
30#[path = "qwen4_exp/aux_sites.rs"]
33mod aux;
34mod ffn;
35mod hc;
36mod ple;
37mod probe;
38
39pub use probe::audit_namespace;
40
41#[cfg(test)]
46pub fn ple_shard_layout(snapshot: &str) -> Result<(Vec<(std::path::PathBuf, u64)>, u64)> {
47 use std::io::{Read, Seek, SeekFrom};
48 let idx: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(
49 std::path::Path::new(snapshot).join("model.safetensors.index.json"),
50 )?)?;
51 let map = idx["weight_map"].as_object().context("weight_map")?;
52 let mut names: Vec<(usize, &String)> = map
53 .keys()
54 .filter(|k| k.contains(".ngram_embedding.shard_"))
55 .map(|k| {
56 let n = k
57 .rsplit("shard_")
58 .next()
59 .and_then(|r| r.split('.').next())
60 .and_then(|r| r.parse().ok())
61 .unwrap_or(usize::MAX);
62 (n, k)
63 })
64 .collect();
65 names.sort();
66 anyhow::ensure!(!names.is_empty(), "no PLE shards in {snapshot}");
67
68 let mut headers: std::collections::HashMap<String, (serde_json::Value, u64)> =
72 std::collections::HashMap::new();
73 let mut shards = Vec::with_capacity(names.len());
74 let mut rows_per = 0u64;
75 for (i, name) in &names {
76 let file = map[name.as_str()].as_str().context("shard file")?;
77 if !headers.contains_key(file) {
78 let path = std::path::Path::new(snapshot).join(file);
79 let mut fh = std::fs::File::open(&path)?;
80 let mut len = [0u8; 8];
81 fh.read_exact(&mut len)?;
82 let hlen = u64::from_le_bytes(len);
83 let mut hdr = vec![0u8; hlen as usize];
84 fh.seek(SeekFrom::Start(8))?;
85 fh.read_exact(&mut hdr)?;
86 headers.insert(file.to_owned(), (serde_json::from_slice(&hdr)?, 8 + hlen));
87 }
88 let (hdr, data_start) = &headers[file];
89 let e = &hdr[name.as_str()];
90 let off = e["data_offsets"][0].as_u64().context("data_offsets")?;
91 let rows = e["shape"][0].as_u64().context("shape")?;
92 if *i == 0 {
93 rows_per = rows;
94 }
95 anyhow::ensure!(
96 rows == rows_per,
97 "shard {i} has {rows} rows, not {rows_per}"
98 );
99 shards.push((std::path::Path::new(snapshot).join(file), data_start + off));
100 }
101 Ok((shards, rows_per))
102}
103
104pub struct Qwen4ExpWeightLoader;
105
106impl ModelWeightLoader for Qwen4ExpWeightLoader {
107 fn supports_tp(&self) -> bool {
108 false
109 }
110
111 fn load_layers(
112 &self,
113 store: &WeightStore,
114 config: &ModelConfig,
115 gpu: &dyn GpuBackend,
116 layer_kv_dtypes: &[KvCacheDtype],
117 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
118 let report = audit_namespace(store, config);
119 report.log();
120 report.ensure_loadable()?;
121
122 let h = config.hidden_size;
123 let variant = metrale_model_layers::weight_map::detect_nvfp4_variant(store, config);
124 let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
125 let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
126 let stream = gpu.default_stream();
127
128 tracing::info!(
129 "Qwen3.8-Flash-Next: {} layers ({} GDN + {} full attention), \
130 {} experts top-{}, hc {} streams x rank {}, indexer budget {}, \
131 PLE at {:?}; NVFP4 variant {:?}",
132 config.num_hidden_layers,
133 config
134 .layer_types
135 .iter()
136 .filter(|t| **t == LayerType::LinearAttention)
137 .count(),
138 config
139 .layer_types
140 .iter()
141 .filter(|t| **t == LayerType::FullAttention)
142 .count(),
143 config.num_experts,
144 config.num_experts_per_tok,
145 config.hc_mult,
146 config.hc_lowrank,
147 config.index_topk,
148 config.ple_layer_ids,
149 variant,
150 );
151
152 let hc_head = if config.hc_mult > 0 {
155 Some(hc::load_head(store, config)?)
156 } else {
157 None
158 };
159
160 let max_ple_tokens: usize = std::env::var("METRALE_PLE_MAX_TOKENS")
165 .ok()
166 .and_then(|v| v.parse().ok())
167 .unwrap_or(2048);
168 let ple_off = std::env::var("METRALE_QWEN4EXP_NO_PLE").as_deref() == Ok("1");
171 let bf16_gdn = std::env::var("METRALE_QWEN4EXP_BF16_GDN").as_deref() != Ok("0");
175 tracing::info!(
176 "GDN projections: {} on the {} linear-attention layers",
177 if bf16_gdn {
178 "BF16 as shipped (no runtime NVFP4 requantization)"
179 } else {
180 "requantized to NVFP4 (METRALE_QWEN4EXP_BF16_GDN=0)"
181 },
182 config
183 .layer_types
184 .iter()
185 .filter(|t| **t == LayerType::LinearAttention)
186 .count(),
187 );
188
189 let mut layers: Vec<Box<dyn TransformerLayer>> =
190 Vec::with_capacity(config.num_hidden_layers);
191 let mut attn_idx = 0usize;
192
193 let (mut moe_bytes, mut arm_bytes, mut hc_bytes) = (0u64, 0u64, 0u64);
196 let free_now = |g: &dyn GpuBackend| g.free_memory().unwrap_or(0) as u64;
197
198 for i in 0..config.num_hidden_layers {
199 let lp = config.layer_prefix(i);
200 let f0 = free_now(gpu);
201 let ffn = ffn::build_moe(store, &lp, config, gpu, variant)?;
202 let f1 = free_now(gpu);
203 moe_bytes += f0.saturating_sub(f1);
204
205 let input_norm = ones_norm(h, gpu)?;
211 let post_attn_norm = ones_norm(h, gpu)?;
212
213 let layer = match config.layer_types[i] {
214 LayerType::LinearAttention if bf16_gdn => {
215 crate::weight_loader::qwen35::load_layers::linear_attn_arms::build_linear_attention_dense_bf16(
216 i, store, &lp, gpu, variant, config, h,
217 input_norm, post_attn_norm, ffn,
218 )
219 .with_context(|| format!("qwen4_exp: GDN layer {i} (BF16)"))?
220 }
221 LayerType::LinearAttention => {
222 crate::weight_loader::qwen35::load_layers::linear_attn_arms::build_linear_attention_nvfp4(
223 store, &lp, gpu, variant, config, h, absmax_k, quantize_k, stream,
224 input_norm, post_attn_norm, ffn,
225 )
226 .with_context(|| format!("qwen4_exp: GDN layer {i}"))?
227 }
228 LayerType::FullAttention => {
229 let kv_dtype = layer_kv_dtypes
230 .get(attn_idx)
231 .copied()
232 .unwrap_or(KvCacheDtype::Bf16);
233 let l = crate::weight_loader::qwen35::load_layers::attention_arms::build_full_attention_nvfp4(
234 i, store, &lp, gpu, variant, config, h, absmax_k, quantize_k, stream,
235 kv_dtype, attn_idx, input_norm, post_attn_norm, ffn,
236 )
237 .with_context(|| format!("qwen4_exp: full-attention layer {i}"))?;
238 attn_idx += 1;
239 l
240 }
241 other => anyhow::bail!(
242 "qwen4_exp layer {i} has type {other:?}; this architecture is \
243 only linear_attention / full_attention"
244 ),
245 };
246 let f2 = free_now(gpu);
247 arm_bytes += f1.saturating_sub(f2);
248
249 let mut layer = layer;
252 if config.hc_mult > 0 {
253 let (attn, ffn) = hc::load_layer_sites(store, &lp, config)?;
254 aux::attach_hc(&mut layer, i, attn, ffn, hc_head.clone(), config)?;
255 }
256 aux::attach_qsa(&mut layer, i, &lp, store, config, gpu)?;
257 let ple_layer = if ple_off {
260 None
261 } else {
262 ple::load(store, config, i, max_ple_tokens, gpu)?
263 };
264 if let Some(p) = ple_layer {
265 aux::attach_ple(&mut layer, i, p)?;
266 }
267 layers.push(layer);
268 hc_bytes += f2.saturating_sub(free_now(gpu));
269 }
270 tracing::info!(
271 "qwen4_exp layer construction: MoE {:.2} GB ({:.1} MB/layer), \
272 attn/GDN arms {:.2} GB ({:.1} MB/layer), mHC+PLE {:.2} GB",
273 moe_bytes as f64 / 1e9,
274 moe_bytes as f64 / 1e6 / config.num_hidden_layers as f64,
275 arm_bytes as f64 / 1e9,
276 arm_bytes as f64 / 1e6 / config.num_hidden_layers as f64,
277 hc_bytes as f64 / 1e9,
278 );
279
280 if !config.ple_layer_ids.is_empty()
281 && std::env::var("METRALE_QWEN4EXP_NO_PLE").as_deref() == Ok("1")
282 {
283 tracing::warn!(
284 "METRALE_QWEN4EXP_NO_PLE=1: PLE n-gram injection at model layer {} \
285 is DISABLED. Output is wrong by construction — this arm exists \
286 to bisect the mHC spine, nothing else.",
287 config.ple_layer_ids[0].saturating_sub(1),
288 );
289 }
290 tracing::info!(
291 "Qwen3.8-Flash-Next loaded {} layers with the mHC highway live on \
292 all of them ({} GDN + {} full-attention).",
293 layers.len(),
294 layers.len()
295 - config
296 .layer_types
297 .iter()
298 .filter(|t| **t == LayerType::FullAttention)
299 .count(),
300 config
301 .layer_types
302 .iter()
303 .filter(|t| **t == LayerType::FullAttention)
304 .count(),
305 );
306 Ok(layers)
307 }
308
309 fn load_embedding(
310 &self,
311 store: &WeightStore,
312 config: &ModelConfig,
313 _gpu: &dyn GpuBackend,
314 ) -> Result<DenseWeight> {
315 let pfx = embed_prefix(config);
316 dense(store, &format!("{pfx}.embed_tokens.weight")).context("qwen4_exp: embedding")
317 }
318
319 fn load_final_norm(
325 &self,
326 store: &WeightStore,
327 config: &ModelConfig,
328 gpu: &dyn GpuBackend,
329 ) -> Result<DenseWeight> {
330 aux::final_norm_placeholder(store, config, gpu)
331 }
332
333 fn load_lm_head(
334 &self,
335 store: &WeightStore,
336 config: &ModelConfig,
337 _gpu: &dyn GpuBackend,
338 ) -> Result<DenseWeight> {
339 if store.contains("lm_head.weight") {
340 return dense(store, "lm_head.weight");
341 }
342 anyhow::ensure!(
343 config.tie_word_embeddings,
344 "qwen4_exp: no lm_head.weight and tie_word_embeddings is false"
345 );
346 let pfx = embed_prefix(config);
347 dense(store, &format!("{pfx}.embed_tokens.weight")).context("qwen4_exp: tied lm_head")
348 }
349
350 fn load_vision_encoder(
351 &self,
352 store: &WeightStore,
353 config: &ModelConfig,
354 gpu: &dyn GpuBackend,
355 ) -> Result<Option<metrale_model_layers::layers::VisionTower>> {
356 crate::weight_loader::qwen35::Qwen35WeightLoader.load_vision_encoder(store, config, gpu)
357 }
358
359 fn load_mtp_weights(
360 &self,
361 _store: &WeightStore,
362 _config: &ModelConfig,
363 _gpu: &dyn GpuBackend,
364 ) -> Result<Option<MtpWeights>> {
365 Ok(None)
366 }
367}
368
369fn ones_norm(n: usize, gpu: &dyn GpuBackend) -> Result<DenseWeight> {
372 let host: Vec<u8> = std::iter::repeat_n([0x80u8, 0x3Fu8], n).flatten().collect();
373 let ptr = gpu.alloc(host.len())?;
374 gpu.copy_h2d(&host, ptr)?;
375 Ok(DenseWeight { weight: ptr })
376}
377
378fn embed_prefix(config: &ModelConfig) -> String {
380 if config.weight_prefix.is_empty() {
381 "model".to_string()
382 } else {
383 config.weight_prefix.clone()
384 }
385}
386
387fn mixer_prefix(config: &ModelConfig) -> String {
389 format!("{}.hyper_connection_mixer", embed_prefix(config))
390}