spark_model/weight_loader/qwen35_dense.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3use anyhow::Result;
4use atlas_core::config::{LayerType, ModelConfig};
5use spark_runtime::gpu::GpuBackend;
6use spark_runtime::kv_cache::KvCacheDtype;
7use spark_runtime::weights::{WeightDtype, WeightStore};
8
9use super::{ModelWeightLoader, WeightFormat};
10use crate::layer::TransformerLayer;
11use crate::layers::{DenseFfnLayer, FfnComponent, Qwen3AttentionLayer, Qwen3SsmLayer};
12use crate::tp_shard::{
13 TpGdnDims, TpShardKind, load_qkvo_tp, shard_dense_bf16, shard_gdn_ba_rows, shard_gdn_conv_rows,
14 shard_gdn_out_proj_row_parallel, shard_gdn_qkvz_rows, shard_gdn_value_vector,
15 shard_quantized_nvfp4,
16};
17use crate::weight_map::{
18 AttentionWeights, DenseWeight, Fp8Weight, MtpWeights, Nvfp4Variant, PackedQ2Weight, SsmWeights,
19 dense, dense_auto, dense_f32_safe, dense_keep_f32, dequant_nvfp4_to_bf16, detect_nvfp4_variant,
20 gpu_concat_rows, interleave_ba, load_dense_ffn, load_fp8_block_scaled_as_fp8weight,
21 load_kv_scales, load_mtp, quantize_to_nvfp4, quantized_auto,
22};
23
24/// True when `{prefix}.weight` is FP8 E4M3 on disk with a 2D block scale
25/// (`weight_scale_inv` or 2D `weight_scale`) — i.e. a native FP8 checkpoint
26/// projection that should load as `Fp8Weight` rather than be requantized to
27/// NVFP4. Mirrors `qwen35::load_layers::proj_is_native_fp8`.
28/// The Q2_0 group size if `{prefix}.weight` is a keep-packed ternary tensor
29/// (`WeightDtype::PackedQ2_0`, produced by the GGUF loader under
30/// `ATLAS_GGUF_NATIVE_Q2=1`), else `None`. When `None` for every projection the
31/// FFN takes the unchanged BF16→NVFP4 path, so the default (flag-off) behavior
32/// is byte-identical.
33fn proj_q2_group(store: &WeightStore, prefix: &str) -> Option<u16> {
34 store
35 .get(&format!("{prefix}.weight"))
36 .ok()
37 .and_then(|w| w.q2_group())
38}
39
40/// Build a [`PackedQ2Weight`] borrowing the store's packed `block_q2_0` buffer.
41/// The buffer is owned by the `WeightStore` (freed with it), so this only wraps
42/// the pointer + `[n, k]` + group; no dequant, no allocation.
43fn packed_q2_from_store(store: &WeightStore, prefix: &str) -> Result<PackedQ2Weight> {
44 let w = store.get(&format!("{prefix}.weight"))?;
45 let group = w
46 .q2_group()
47 .ok_or_else(|| anyhow::anyhow!("{prefix}.weight is not keep-packed Q2_0"))?;
48 anyhow::ensure!(
49 w.shape.len() == 2,
50 "packed Q2_0 {prefix}.weight must be 2D, got {:?}",
51 w.shape
52 );
53 Ok(PackedQ2Weight {
54 weight: w.ptr,
55 n: w.shape[0] as u32,
56 k: w.shape[1] as u32,
57 group,
58 })
59}
60
61fn 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
74/// True when `{prefix}.weight` is on-disk FP8 with a scale the native-FP8
75/// `w8a16` path can actually consume — i.e. one that is (or broadcasts to) the
76/// `[ceil(N/128), ceil(K/128)]` FP32 block grid the kernel indexes as
77/// `block_scale[n/128, k/128]`:
78///
79/// * `weight_scale_inv` / 2-D `weight_scale` shaped as that block grid
80/// (DeepSeek-V3 / Qwen-native convention), or
81/// * a per-tensor SCALAR `weight_scale` (ModelOpt; e.g. the nvidia
82/// Qwen3.6-27B-NVFP4 GDN projections) — `load_fp8_block_scaled_as_fp8weight`
83/// broadcasts it into a uniform grid, which is exact.
84///
85/// A **per-row** `weight_scale` (`[N,1]`, e.g. unsloth's re-quantized
86/// Qwen3.6-*-NVFP4, 2026-07-10) is deliberately REJECTED. It is not a block
87/// grid: the kernel would read row `n`'s multiplier from grid cell `n/128`, so
88/// 127 of every 128 rows get some other row's scale. That is in-bounds — the
89/// widened `[N]` buffer is *larger* than the `[N/128, K/128]` grid — so it does
90/// not fault; it silently produces garbage logits. Returning false here drops
91/// the projection to the default `dequant_fp8_blockscaled_to_bf16` →
92/// `quantize_to_nvfp4` path, which reads a `[N,1]` scale correctly
93/// (`block_n = N/N = 1`, i.e. one multiplier per row).
94fn proj_is_fp8_any_scale(store: &WeightStore, prefix: &str) -> bool {
95 let Ok(w) = store.get(&format!("{prefix}.weight")) else {
96 return false;
97 };
98 if w.dtype != WeightDtype::FP8E4M3 || w.shape.len() != 2 {
99 return false;
100 }
101 let (n, k) = (w.shape[0], w.shape[1]);
102
103 for key in [
104 format!("{prefix}.weight_scale_inv"),
105 format!("{prefix}.weight_scale"),
106 ] {
107 let Ok(s) = store.get(&key) else { continue };
108 // Per-tensor scalar → broadcast to a uniform grid: exact.
109 if s.num_elements() == 1 {
110 return true;
111 }
112 // 2-D scale: only a genuine 128×128 block grid is consumable.
113 if s.shape.len() == 2 && s.shape[0] == n.div_ceil(128) && s.shape[1] == k.div_ceil(128) {
114 return true;
115 }
116 }
117 false
118}
119
120/// Concatenate two block-scaled FP8 weights along rows (dim 0):
121/// `[n_a, k] ++ [n_b, k] -> [n_a+n_b, k]`. FP8 bytes (1 B/elem) and the
122/// `[n/128, k/128]` FP32 block-scale grids are contiguous row-major, so each is
123/// a straight device-to-device append. Requires `n_a % 128 == 0` so the
124/// scale-grid rows meet at a block boundary (GDN qkv rows are a multiple of
125/// 128). Produces the `[Q|K|V|Z]` sequential order the SSM layer expects.
126fn concat_fp8_block_scaled(
127 a: &Fp8Weight,
128 b: &Fp8Weight,
129 k: usize,
130 gpu: &dyn GpuBackend,
131) -> Result<Fp8Weight> {
132 let kb = k.div_ceil(128);
133 let a_w = a.n as usize * k;
134 let b_w = b.n as usize * k;
135 let weight = gpu.alloc(a_w + b_w)?;
136 gpu.copy_d2d(a.weight, weight, a_w)?;
137 gpu.copy_d2d(b.weight, weight.offset(a_w), b_w)?;
138 let a_s = (a.n as usize).div_ceil(128) * kb * 4;
139 let b_s = (b.n as usize).div_ceil(128) * kb * 4;
140 let row_scale = gpu.alloc(a_s + b_s)?;
141 gpu.copy_d2d(a.row_scale, row_scale, a_s)?;
142 gpu.copy_d2d(b.row_scale, row_scale.offset(a_s), b_s)?;
143 Ok(Fp8Weight {
144 weight,
145 row_scale,
146 n: a.n + b.n,
147 k: k as u32,
148 scale_format: crate::weight_map::WeightQuantFormat::Fp8BlockScaled,
149 })
150}
151
152/// Opt-in gate for native dense-FP8 attention + FFN dispatch (Qwythos / dense
153/// Ornith-FP8). Default OFF.
154///
155/// VERIFIED 2026-06-29 on Qwythos-9B-FP8 (gb10/ornith-1.0-9b): with the flag
156/// on, the FP8 arms fire for all 32 FFN + 8 full-attn layers and text is
157/// correct (coherence/fib/tools 3/3). BUT it is NOT a perf win — ~30 tok/s vs
158/// ~40 for the NVFP4 fallback — because this target's NVFP4 W4A16 kernels
159/// (fused dual-GEMV decode, transposed m128 prefill) are more optimized than
160/// its FP8 W8A16 kernels (unfused per-projection GEMV, non-transposed
161/// `w8a16_gemm` prefill; the attention FP8 prefill transpose also does not
162/// engage). Vision prefill additionally hits a CUDA-700. Making FP8 pay off
163/// here needs dedicated dense-FP8 kernels (fused FP8 dual-GEMV + fast
164/// transposed FP8 prefill GEMM), not loader wiring. Until then NVFP4 autoquant
165/// is the better dense runtime. `ATLAS_DENSE_FP8=1` opts in for that kernel work.
166fn dense_fp8_enabled() -> bool {
167 std::env::var("ATLAS_DENSE_FP8").as_deref() == Ok("1")
168}
169
170mod loaders_b;
171mod rowwise_fp8;
172
173pub struct Qwen35DenseWeightLoader;
174
175impl ModelWeightLoader for Qwen35DenseWeightLoader {
176 fn supports_tp(&self) -> bool {
177 // FullAttention layers are TP-sharded (NVFP4-from-disk and BF16
178 // → NVFP4 paths). LinearAttention (GDN SSM) layers run
179 // full-replica per rank — see qwen35.rs for the rationale.
180 true
181 }
182
183 fn load_layers(
184 &self,
185 store: &WeightStore,
186 config: &ModelConfig,
187 gpu: &dyn GpuBackend,
188 layer_kv_dtypes: &[KvCacheDtype],
189 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
190 let layer_types = if config.layer_types.is_empty() {
191 (0..config.num_hidden_layers)
192 .map(|i| config.layer_type(i))
193 .collect::<Vec<_>>()
194 } else {
195 config.layer_types.clone()
196 };
197
198 let mut layers: Vec<Box<dyn TransformerLayer>> =
199 Vec::with_capacity(config.num_hidden_layers);
200 let mut attn_idx = 0usize;
201
202 let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
203 let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
204 let stream = gpu.default_stream();
205 let h = config.hidden_size;
206
207 let variant = detect_nvfp4_variant(store, config);
208 let weight_format = WeightFormat::detect(store, config);
209 tracing::info!(
210 "Weight format: {:?}, NVFP4 variant: {:?}",
211 weight_format,
212 variant
213 );
214
215 // Native FP8 SSM prefill GEMM (Qwen3.6-27B-FP8 root-cause fix,
216 // commit 3ebc08a). Atlas's prior SSM in_proj_qkv path was
217 // FP8 → BF16 → NVFP4 → BF16 (in `w4a16_gemm` dequant) → MMA — a
218 // double-quant chain whose NVFP4 hop's ~4-bit per-group precision
219 // is dominated by signal at q/v but attenuated into a k-direction
220 // error (HF conv-k ‖6.3‖ vs conv-v ‖117.2‖, ~18× smaller). For
221 // every FP8-on-disk checkpoint we install a single-scale FP8 copy
222 // of the stacked `[QKV|Z]` and `out_proj` weights for prefill,
223 // bypassing the NVFP4 intermediate. Prefill dispatches via the
224 // existing `fp8_gemm_n128` (BF16 act × FP8 weight) — same path
225 // the MoE shared-expert FP8 prefill uses. Decode/GEMV unchanged.
226 // Originally env-gated `ATLAS_FP8_SSM_PREFILL=1`; promoted to
227 // unconditional 2026-05-20 after live verification (commit
228 // dfb4e8a era, tokens_to_first_degeneration 1,196 → 16,968).
229 // 2026-07-03 precision policy: GDN projections stay ≥FP8 — the
230 // NVFP4 requant of qkvz/out_proj flattens tensors that both
231 // checkpoint toolchains deliberately keep high-precision (modelopt
232 // sensitivity analysis ships them FP8; unsloth ships BF16). Applies
233 // to ALL NVFP4 variants now, not just Fp8Dequanted. Kill-switch
234 // ATLAS_NO_GDN_FP8_PREFILL restores the pre-policy behavior for A/B.
235 let fp8_ssm_prefill = std::env::var_os("ATLAS_NO_GDN_FP8_PREFILL").is_none();
236 let bf16_to_fp8_k = if fp8_ssm_prefill {
237 tracing::info!(
238 // The old wording — "NVFP4 kept as structural fallback for
239 // decode batch paths" — was DISPROVEN by nsys at C=8
240 // (2026-08-17): the batched verify QKVZ was taking this FP8
241 // copy at every M>8, 26.3% of the step. It is true again only
242 // because that arm now reads NVFP4; say the actual rule so the
243 // log cannot re-drift from the dispatch.
244 "SSM in_proj_qkv + out_proj via native FP8 prefill GEMM \
245 (BF16 act × FP8 weight via fp8_gemm_n128). PREFILL ONLY: \
246 decode + batched verify read the NVFP4 copy — weight-streaming \
247 GEMV at M<=8, tile GEMM above it (trait_decode_batched.rs). \
248 The FP8 copy reaches decode only at M<=8 on a build whose \
249 batched NVFP4 GEMVs are absent"
250 );
251 Some(gpu.kernel("w4a16", "bf16_to_fp8")?)
252 } else {
253 None
254 };
255
256 // ATLAS_MEM_PROFILE: per-phase GPU-free trace to pin the strix/APU
257 // load-time footprint (FP8-source persistence vs NVFP4 steady-state vs
258 // BF16 requant transients). Gated env so it's a no-op in production.
259 let mem_profile = std::env::var("ATLAS_MEM_PROFILE").is_ok();
260 let log_free = |tag: &str| {
261 if mem_profile && let Ok(free) = gpu.free_memory() {
262 tracing::info!("MEM_PROFILE[{tag}]: {:.2} GB GPU-free", free as f64 / 1e9);
263 }
264 };
265 log_free("dense-load-start");
266
267 for (i, lt) in layer_types.iter().enumerate() {
268 if i % 8 == 0 {
269 log_free(&format!("layer-{i}"));
270 }
271 let lp = config.layer_prefix(i);
272 let input_norm = dense(store, &format!("{lp}.input_layernorm.weight"))?;
273 let post_attn_norm = dense(store, &format!("{lp}.post_attention_layernorm.weight"))?;
274
275 // Dense FFN instead of MoE. Native FP8 checkpoints (single-GPU)
276 // load gate/up/down directly as block-scaled `Fp8Weight` and
277 // dispatch w8a16 — no NVFP4 requant. TP>1 still uses the NVFP4
278 // path (FP8 FFN sharding is a follow-up).
279 let ffn_fp8 = dense_fp8_enabled()
280 && config.tp_world_size.max(1) == 1
281 && matches!(variant, Nvfp4Variant::Fp8Dequanted)
282 && proj_is_native_fp8(store, &format!("{lp}.mlp.gate_proj"));
283 // Always load the NVFP4 weights so every dispatch path (incl. the
284 // batched spec-decode forward_k2/k3 paths that have no FP8 branch)
285 // has a valid weight to fall back to — a null NVFP4 weight under a
286 // w4a16 dispatch is the CUDA-700-at-concurrency bug. When native
287 // FP8 is enabled we overlay the block-scaled FP8 weights on top;
288 // the hot forward / forward_prefill paths then use FP8, the rare
289 // batched paths fall back to real NVFP4.
290 // Native-BF16 dense-FFN prefill (Holo/Ornith Bf16Raw): the fast
291 // tensor-core `dense_gemm_tc` path (dense_ffn::forward_prefill) needs
292 // LIVE BF16 gate/up/down. `load_dense_ffn`'s Bf16Raw arm runtime-
293 // quantizes each proj via `quantized_any`, whose Bf16Raw branch
294 // FREES the store's cached BF16 buffer (`gpu.free(w.ptr)` in
295 // nvfp4_detect.rs:288-309, the Bf16Raw arm). `dense_auto` returns that
296 // SAME (now-freed) store ptr for a BF16 tensor — quant_helpers.rs:290
297 // hands back `w.ptr` uncopied — so overlaying it AFTER load_dense_ffn hands
298 // `set_bf16_weights` freed GPU memory -> dense_gemm_tc CUDA-700 at
299 // grid=[div_ceil(intermediate_size,64),..] on the first prefill.
300 // Snapshot fresh D2D copies BEFORE the free (the clone is an
301 // independent allocation the layer owns for its lifetime).
302 // Native keep-packed ternary Q2_0 (ATLAS_GGUF_NATIVE_Q2=1): when the
303 // GGUF loader tagged gate/up/down as `PackedQ2_0`, DON'T requant to
304 // NVFP4 — install the raw 2-bit blocks and dispatch `q2_0_gemv` at
305 // decode. Requires tp_size=1 (packed-block sharding is unimplemented).
306 // Flag off → tensors are BF16, `ffn_q2` is false, path unchanged.
307 let ffn_q2 = config.tp_world_size.max(1) == 1
308 && proj_q2_group(store, &format!("{lp}.mlp.gate_proj")).is_some()
309 && proj_q2_group(store, &format!("{lp}.mlp.up_proj")).is_some()
310 && proj_q2_group(store, &format!("{lp}.mlp.down_proj")).is_some();
311 // Keep-packed projections are 2-bit blocks, not BF16 — there is
312 // nothing to snapshot (dense_auto has no PackedQ2_0 arm and would
313 // abort the load), and the ffn_q2 arm below owns their compute.
314 let ffn_bf16_snapshot = if !ffn_q2 && matches!(variant, Nvfp4Variant::Bf16Raw) {
315 let inter = if config.intermediate_size > 0 {
316 config.intermediate_size
317 } else {
318 config.moe_intermediate_size
319 };
320 let clone_bf16 = |name: &str, rows: usize, cols: usize| -> Result<DenseWeight> {
321 let src = dense_auto(store, &format!("{lp}.mlp.{name}.weight"), gpu)?;
322 let bytes = rows * cols * 2; // BF16 = 2 bytes/elem
323 let dst = gpu.alloc(bytes)?;
324 // The whole point of this snapshot is an allocation
325 // INDEPENDENT of the store's cached ptr (which load_dense_ffn
326 // frees below); an aliased dst would silently re-create the
327 // freed-memory CUDA-700 this helper exists to prevent.
328 debug_assert_ne!(
329 dst, src.weight,
330 "ffn_bf16_snapshot must be a fresh allocation, not an alias of the store ptr"
331 );
332 gpu.copy_d2d(src.weight, dst, bytes)?;
333 Ok(DenseWeight { weight: dst })
334 };
335 Some((
336 clone_bf16("gate_proj", inter, h)?,
337 clone_bf16("up_proj", inter, h)?,
338 clone_bf16("down_proj", h, inter)?,
339 ))
340 } else {
341 None
342 };
343
344 let ffn_weights = if ffn_q2 {
345 // NULL NVFP4 fallback: decode uses the packed weights; prefill /
346 // batched paths bail (Tier-2). No NVFP4 allocation → memory win.
347 use crate::weight_map::QuantizedWeight;
348 crate::layers::dense_ffn::DenseFfnWeights {
349 gate_proj: QuantizedWeight::null(),
350 up_proj: QuantizedWeight::null(),
351 down_proj: QuantizedWeight::null(),
352 gate_proj_t: None,
353 up_proj_t: None,
354 down_proj_t: None,
355 }
356 } else {
357 load_dense_ffn(
358 store, &lp, gpu, variant, absmax_k, quantize_k, stream, config,
359 )?
360 };
361 let mut dffn = DenseFfnLayer::new(ffn_weights, gpu)?;
362 if ffn_q2 {
363 dffn.set_q2_weights(
364 packed_q2_from_store(store, &format!("{lp}.mlp.gate_proj"))?,
365 packed_q2_from_store(store, &format!("{lp}.mlp.up_proj"))?,
366 packed_q2_from_store(store, &format!("{lp}.mlp.down_proj"))?,
367 gpu,
368 );
369 }
370 if ffn_fp8 {
371 let load_ffn_fp8 = |name: &str| {
372 load_fp8_block_scaled_as_fp8weight(store, &format!("{lp}.mlp.{name}"), gpu)
373 };
374 dffn.set_fp8_weights(
375 load_ffn_fp8("gate_proj")?,
376 load_ffn_fp8("up_proj")?,
377 load_ffn_fp8("down_proj")?,
378 );
379 }
380 // ATLAS_FFN_MMQ: eagerly materialize Q4_K + free the dead `_t` copies at load,
381 // BEFORE KV cache sizing, so net FFN footprint == NVFP4 baseline (no decode OOM-throttle).
382 dffn.finalize_q4k_load(gpu, h as u32, config.intermediate_size as u32, stream)?;
383 // ATLAS_FFN_NVFP4_MMQ: same discipline for the W4A4 FP4-MMQ arm — repack
384 // gate/up to block_nvfp4 + free their `_t` copies (net ~0 footprint).
385 //
386 // SKIPPED when a LoRA adapter is pending. The forward-time FP4-MMQ
387 // arm is disabled while an adapter is installed (it leaves gate/up
388 // UNSCALED and folds weight_scale_2 inside the SiLU-mul, which
389 // would silently scale a true-valued delta). But this finalize
390 // FREES the transposed `_t` copies on the assumption that the MMQ
391 // arm will serve prefill — so running it and then disabling the arm
392 // left prefill on the slowest non-transposed GEMM with nothing to
393 // fall back to: 176 tok/s against 841 on a 2K prompt, and it had
394 // NOTHING to do with the cost of applying the deltas (measured with
395 // the deltas skipped entirely).
396 //
397 // `adapter_max_rank` is the load-time signal that `--lora-adapter`
398 // was given; the adapter itself is installed later (build step 8),
399 // so this is the only point where the decision can be made before
400 // the twins are freed.
401 if config.adapter_max_rank == 0 {
402 dffn.finalize_nvfp4_mmq_load(
403 gpu,
404 h as u32,
405 config.intermediate_size as u32,
406 stream,
407 )?;
408 }
409 // Native-BF16 dense-FFN overlay (Bf16Raw, no-metadata Holo dense): install the
410 // live BF16 gate/up/down snapshot so forward/forward_prefill's bf16 branch
411 // (preferred over the NVFP4 fallback) reads valid memory. The NVFP4 weights built
412 // by load_dense_ffn stay as the spec-decode/batched fallback (never null -> no
413 // CUDA-700 at concurrency). Snapshot was taken before load_dense_ffn freed the
414 // store's BF16 buffer (see ffn_bf16_snapshot above).
415 if let Some((g, u, d)) = ffn_bf16_snapshot {
416 dffn.set_bf16_weights(g, u, d);
417 }
418 let ffn = FfnComponent::Dense(dffn);
419
420 match lt {
421 LayerType::FullAttention => {
422 let p = format!("{lp}.self_attn");
423 let tp_rank = config.tp_rank;
424 let tp_size = config.tp_world_size.max(1);
425 // Bf16Raw installs a BF16 dense O-proj after the layer is
426 // built; other variants leave this None (NVFP4/FP8 dispatch).
427 let mut o_dense_bf16: Option<DenseWeight> = None;
428
429 // Native keep-packed ternary Q2_0 (Tier-1c): when the GGUF
430 // loader tagged q/k/v/o as `PackedQ2_0` (transform-free
431 // full-attention projections), install the raw 2-bit blocks
432 // and dispatch `q2_0_gemv_vec` at decode / transient-dequant
433 // at prefill — no NVFP4 requant, no `_t` copies. Requires
434 // tp_size=1 (packed-block sharding is unimplemented). Bonsai
435 // has no kill-switch here; the whole path is gated upstream
436 // by ATLAS_GGUF_NATIVE_Q2 (else these tensors are BF16 and
437 // `attn_q2` is false). ATLAS_NO_Q2_ATTN forces the BF16 path
438 // for A/B bisection.
439 let attn_q2 = tp_size == 1
440 && std::env::var_os("ATLAS_NO_Q2_ATTN").is_none()
441 && proj_q2_group(store, &format!("{p}.q_proj")).is_some()
442 && proj_q2_group(store, &format!("{p}.k_proj")).is_some()
443 && proj_q2_group(store, &format!("{p}.v_proj")).is_some()
444 && proj_q2_group(store, &format!("{p}.o_proj")).is_some();
445 if attn_q2 {
446 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu);
447 let attn = AttentionWeights {
448 q_proj: DenseWeight {
449 weight: spark_runtime::gpu::DevicePtr::NULL,
450 },
451 k_proj: DenseWeight {
452 weight: spark_runtime::gpu::DevicePtr::NULL,
453 },
454 v_proj: DenseWeight {
455 weight: spark_runtime::gpu::DevicePtr::NULL,
456 },
457 o_proj: crate::weight_map::QuantizedWeight::null(),
458 q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
459 k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
460 q_norm_full: None,
461 k_norm_full: None,
462 k_scale,
463 v_scale,
464 };
465 let mut attn_layer = Qwen3AttentionLayer::new(
466 input_norm,
467 attn,
468 post_attn_norm,
469 ffn,
470 attn_idx,
471 None,
472 None,
473 None,
474 gpu,
475 layer_kv_dtypes[attn_idx],
476 config.fp8_kv_calibration_tokens,
477 config,
478 )?;
479 attn_layer.set_packed_q2_weights(
480 packed_q2_from_store(store, &format!("{p}.q_proj"))?,
481 packed_q2_from_store(store, &format!("{p}.k_proj"))?,
482 packed_q2_from_store(store, &format!("{p}.v_proj"))?,
483 packed_q2_from_store(store, &format!("{p}.o_proj"))?,
484 gpu,
485 );
486 tracing::info!(
487 "ATTN[{lp}] native keep-packed Q2_0: q/k/v/o 2-bit \
488 (q2_0_gemv_vec decode; transient-dequant prefill)"
489 );
490 layers.push(Box::new(attn_layer));
491 attn_idx += 1;
492 if (i + 1) % 10 == 0 {
493 tracing::info!("Loaded layers 0..{}", i + 1);
494 }
495 continue;
496 }
497 let (attn, q_nvfp4, k_nvfp4, v_nvfp4) = match variant {
498 Nvfp4Variant::CompressedTensors => {
499 // NVFP4-from-disk path: column-parallel Q/K/V, row-parallel O.
500 let group_size = 16usize;
501 let load_nvfp4 = |name: &str,
502 full_n: usize,
503 full_k: usize,
504 kind: TpShardKind|
505 -> Result<crate::weight_map::QuantizedWeight> {
506 let prefix = format!("{p}.{name}");
507 // Mixed-precision compressed-tensors checkpoints
508 // (unsloth Qwen3.6-*-NVFP4, re-quantized 2026-07-10)
509 // NVFP4-pack most of the net but keep attention
510 // q/k/v/o as FP8 (`.weight` FP8E4M3 + a per-row
511 // `.weight_scale`, no `.weight_packed`). Without the
512 // pack metadata, dequant and runtime-quantize to NVFP4
513 // instead of failing on the absent
514 // `weight_global_scale`. Mirrors the MoE loader's
515 // attention arm (weight_loader/qwen35/load_layers/
516 // attention_arms.rs).
517 let src = if store.contains(&format!("{prefix}.weight_packed")) {
518 quantized_auto(store, &prefix, gpu, variant)?
519 } else {
520 let dense_bf16 =
521 dense_auto(store, &format!("{prefix}.weight"), gpu)?;
522 quantize_to_nvfp4(
523 &dense_bf16,
524 full_n,
525 full_k,
526 gpu,
527 absmax_k,
528 quantize_k,
529 stream,
530 )?
531 };
532 if tp_size == 1 {
533 return Ok(src);
534 }
535 let sharded = shard_quantized_nvfp4(
536 &src, full_n, full_k, kind, tp_rank, tp_size, group_size, gpu,
537 )?;
538 gpu.free(src.weight)?;
539 gpu.free(src.weight_scale)?;
540 Ok(sharded)
541 };
542 let [q, k, v, o] = load_qkvo_tp(config, load_nvfp4)?;
543 let dummy = DenseWeight {
544 weight: spark_runtime::gpu::DevicePtr::NULL,
545 };
546 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu);
547 let attn = AttentionWeights {
548 q_proj: dummy,
549 k_proj: dummy,
550 v_proj: dummy,
551 o_proj: o,
552 q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
553 k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
554 q_norm_full: None,
555 k_norm_full: None,
556 k_scale,
557 v_scale,
558 };
559 (attn, Some(q), Some(k), Some(v))
560 }
561 Nvfp4Variant::Standard | Nvfp4Variant::Fp8Dequanted => {
562 // BF16 → NVFP4 path: shard BF16 then quantize per-rank.
563 let load_bf16_then_nvfp4 = |name: &str,
564 full_n: usize,
565 full_k: usize,
566 kind: TpShardKind|
567 -> Result<(
568 DenseWeight,
569 crate::weight_map::QuantizedWeight,
570 )> {
571 // Pre-quantized Standard NVFP4 (e.g. sakamakismile): weight is U8
572 // on disk. Load directly as QuantizedWeight without BF16 roundtrip.
573 // TP sharding of pre-quantized NVFP4 is not yet supported: enforce
574 // tp_size=1 explicitly, or every rank silently loads the full
575 // unsharded weight (duplicated, not sharded — wrong results with
576 // no error).
577 let weight_key = format!("{p}.{name}.weight");
578 if matches!(
579 store.get(&weight_key).map(|w| w.dtype),
580 Ok(WeightDtype::UInt8)
581 ) {
582 anyhow::ensure!(
583 tp_size == 1,
584 "pre-quantized NVFP4 weight '{weight_key}' (U8 on disk) \
585 cannot be loaded under tensor parallelism (tp_size={tp_size}): \
586 TP sharding of pre-quantized NVFP4 checkpoints is not yet \
587 implemented. Use tp_size=1, or dequantize this checkpoint to \
588 BF16 first so it goes through the shard-then-requantize path."
589 );
590 let null_dense = DenseWeight {
591 weight: spark_runtime::gpu::DevicePtr::NULL,
592 };
593 let qw = quantized_auto(
594 store,
595 &format!("{p}.{name}"),
596 gpu,
597 Nvfp4Variant::Standard,
598 )?;
599 return Ok((null_dense, qw));
600 }
601 let src = dense_auto(store, &weight_key, gpu)?;
602 let (sharded_ptr, local_n, local_k) = shard_dense_bf16(
603 src.weight, full_n, full_k, kind, tp_rank, tp_size, gpu,
604 )?;
605 let sharded = DenseWeight {
606 weight: sharded_ptr,
607 };
608 let q = quantize_to_nvfp4(
609 &sharded, local_n, local_k, gpu, absmax_k, quantize_k, stream,
610 )?;
611 if sharded_ptr != src.weight {
612 gpu.free(sharded_ptr)?;
613 }
614 Ok((src, q))
615 };
616 let [
617 (q_dense, q_nvfp4),
618 (k_dense, k_nvfp4),
619 (v_dense, v_nvfp4),
620 (o_dense, o_nvfp4),
621 ] = load_qkvo_tp(config, load_bf16_then_nvfp4)?;
622
623 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu);
624
625 // The BF16 q/k/v/o dense tensors are only the intermediate
626 // fed to the GPU quantize_to_nvfp4 above. Prefill AND decode
627 // always dispatch the NVFP4 weights, so the BF16 copies are
628 // dead once quantized. Free them instead of retaining a full
629 // second copy of every projection (Atlas issue #A1).
630 gpu.free(q_dense.weight)?;
631 gpu.free(k_dense.weight)?;
632 gpu.free(v_dense.weight)?;
633 gpu.free(o_dense.weight)?;
634
635 let attn = AttentionWeights {
636 q_proj: DenseWeight {
637 weight: spark_runtime::gpu::DevicePtr::NULL,
638 },
639 k_proj: DenseWeight {
640 weight: spark_runtime::gpu::DevicePtr::NULL,
641 },
642 v_proj: DenseWeight {
643 weight: spark_runtime::gpu::DevicePtr::NULL,
644 },
645 o_proj: o_nvfp4,
646 q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
647 k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
648 q_norm_full: None,
649 k_norm_full: None,
650 k_scale,
651 v_scale,
652 };
653 (attn, Some(q_nvfp4), Some(k_nvfp4), Some(v_nvfp4))
654 }
655 Nvfp4Variant::Bf16Raw => {
656 // Native BF16 dense attention: keep Q/K/V/O in BF16
657 // and dispatch the dense_gemv/dense_gemm kernels that
658 // ship in the nvfp4 bundle (common/ dense_*_bf16). No
659 // runtime BF16 -> NVFP4 quant — that lossily quantized
660 // these no-metadata Holo dense checkpoints. Mirrors
661 // qwen35/load_layers.rs:552 (BF16-dequant attention)
662 // and gemma4/loader_a.rs:300/418.
663 let load_bf16_dense =
664 |name: &str,
665 full_n: usize,
666 full_k: usize,
667 kind: TpShardKind|
668 -> Result<DenseWeight> {
669 let src =
670 dense_auto(store, &format!("{p}.{name}.weight"), gpu)?;
671 if tp_size == 1 {
672 return Ok(src);
673 }
674 let (sharded_ptr, _local_n, _local_k) = shard_dense_bf16(
675 src.weight, full_n, full_k, kind, tp_rank, tp_size, gpu,
676 )?;
677 if sharded_ptr != src.weight {
678 gpu.free(src.weight)?;
679 }
680 Ok(DenseWeight {
681 weight: sharded_ptr,
682 })
683 };
684 let [q_dense, k_dense, v_dense, o_dense] =
685 load_qkvo_tp(config, load_bf16_dense)?;
686
687 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu);
688
689 // Keep q/k/v BF16 dense ALIVE (do NOT free): the dense
690 // forward reads them directly. o_proj stays NULL — the
691 // set_o_dense_bf16(o_dense) call after layer build
692 // installs the BF16 O-proj that decode/prefill prefer.
693 let attn = AttentionWeights {
694 q_proj: q_dense,
695 k_proj: k_dense,
696 v_proj: v_dense,
697 o_proj: crate::weight_map::QuantizedWeight::null(),
698 q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
699 k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
700 q_norm_full: None,
701 k_norm_full: None,
702 k_scale,
703 v_scale,
704 };
705 o_dense_bf16 = Some(o_dense);
706 // The NULL o_proj above is only sound while the BF16
707 // dense O-proj is installed after layer build — a null
708 // NVFP4 weight reached by a w4a16 dispatch is the
709 // CUDA-700-at-concurrency failure mode.
710 debug_assert!(
711 o_dense_bf16.is_some(),
712 "Bf16Raw attention must install a dense O-proj to cover the null o_proj"
713 );
714 // BF16 native: no NVFP4 q/k/v weights → dense fallback.
715 (attn, None, None, None)
716 }
717 };
718
719 let mut attn_layer = Qwen3AttentionLayer::new(
720 input_norm,
721 attn,
722 post_attn_norm,
723 ffn,
724 attn_idx,
725 q_nvfp4,
726 k_nvfp4,
727 v_nvfp4,
728 gpu,
729 layer_kv_dtypes[attn_idx],
730 config.fp8_kv_calibration_tokens,
731 config,
732 )?;
733 // Fast-prefill: transposed NVFP4 copies route the 16 full-attn
734 // layers' q/k/v/o prefill GEMMs onto w4a16_gemm_t_m128 (28.8%
735 // of prefill GPU time on the base w4a16_gemm path; ~1.3x e2e).
736 // predequant_for_prefill() is deliberately NOT called: the FP8
737 // predequant route is slower for these bandwidth-bound GEMMs.
738 if let (Some(qw), Some(kw), Some(vw)) = (q_nvfp4, k_nvfp4, v_nvfp4) {
739 let (nh, hd) = (config.num_attention_heads, config.head_dim);
740 let (nkv, hh) = (config.num_key_value_heads, config.hidden_size);
741 let q_n = nh * hd * if config.attn_gated { 2 } else { 1 };
742 let qt = qw.transpose_for_gemm(gpu, q_n, hh)?;
743 let kt = kw.transpose_for_gemm(gpu, nkv * hd, hh)?;
744 let vt = vw.transpose_for_gemm(gpu, nkv * hd, hh)?;
745 let op = &attn_layer.attn.o_proj;
746 let ot = op.transpose_for_gemm(gpu, hh, nh * hd)?;
747 attn_layer.set_prefill_weights(Some(qt), Some(kt), Some(vt), Some(ot));
748 // Fused [q|k|v] twin: k/v are N=1024, which against the
749 // 128-wide N tile is 8 CTAs on 48 SMs (40 idle, 23.6 GB/s,
750 // 9.75x off floor). Concatenated N=14336 runs 112 CTAs in
751 // ONE launch. Bit-identical ONLY when the three share a
752 // single `weight_scale_2` — the GEMM applies one scale2 per
753 // launch — so verify the device values rather than assume.
754 // Bit-exact float comparison, not an epsilon: the GEMM
755 // applies ONE scale2, so anything but exact equality
756 // changes results.
757 let scales_equal = qw.weight_scale_2.to_bits()
758 == kw.weight_scale_2.to_bits()
759 && kw.weight_scale_2.to_bits() == vw.weight_scale_2.to_bits();
760 if scales_equal {
761 let fused =
762 crate::weight_map::QuantizedWeight::transpose_concat_for_gemm(
763 gpu,
764 &[(&qw, q_n), (&kw, nkv * hd), (&vw, nkv * hd)],
765 hh,
766 )?;
767 attn_layer.set_fused_qkv_prefill_weight(Some(fused));
768 } else if attn_idx == 0 {
769 tracing::warn!(
770 "attention q/k/v have differing weight_scale_2 — fused QKV GEMM disabled (3 separate launches per layer)"
771 );
772 }
773 }
774 // Native-BF16 (Bf16Raw): install the dense O-proj so decode +
775 // prefill prefer it over the (NULL) NVFP4 o_proj. Mutually
776 // exclusive with the transposed-NVFP4 block above (q_nvfp4 is
777 // None on the Bf16Raw path).
778 if let Some(o_dense) = o_dense_bf16 {
779 attn_layer.set_o_dense_bf16(o_dense);
780 }
781 // Overlay native FP8 q/k/v/o on top of the NVFP4 weights when
782 // enabled (single-GPU FP8 checkpoint). Hot decode/prefill paths
783 // dispatch FP8 (w8a16); any path without an FP8 branch falls back
784 // to the real NVFP4 weights above (never a null → no CUDA-700).
785 if dense_fp8_enabled()
786 && config.tp_world_size.max(1) == 1
787 && matches!(variant, Nvfp4Variant::Fp8Dequanted)
788 && proj_is_native_fp8(store, &format!("{p}.q_proj"))
789 {
790 let load_fp8_proj = |name: &str,
791 _n: usize,
792 _k: usize,
793 _kind: TpShardKind|
794 -> Result<Fp8Weight> {
795 load_fp8_block_scaled_as_fp8weight(store, &format!("{p}.{name}"), gpu)
796 };
797 let [q_fp8, k_fp8, v_fp8, o_fp8] = load_qkvo_tp(config, load_fp8_proj)?;
798 attn_layer.set_fp8_weights(
799 Some(q_fp8),
800 Some(k_fp8),
801 Some(v_fp8),
802 Some(o_fp8),
803 );
804 if let Err(e) = attn_layer.transpose_fp8_for_prefill(gpu, stream) {
805 tracing::warn!("Layer {i}: dense FP8 transpose failed: {e}");
806 }
807 }
808 layers.push(Box::new(attn_layer));
809 attn_idx += 1;
810 }
811 LayerType::LinearAttention => {
812 let nv = config.linear_num_value_heads;
813 let nk = config.linear_num_key_heads;
814 // GDN HeadParallel: config holds per-rank-LOCAL linear head counts
815 // (topology.rs divided them by tp_size). TpGdnDims rebuilds the FULL
816 // pre-shard sizes so load/concat/interleave run at FULL, then the
817 // shard_gdn_* slicers cut this rank's contiguous head range. value_dim
818 // stays LOCAL (sizes the downstream quantize of the sharded buffers).
819 let tp_size = config.tp_world_size.max(1);
820 let dims = TpGdnDims::from_config(config);
821 let qkv_rows = dims.full_conv_dim();
822 let z_rows = dims.full_value_dim();
823 let value_dim = nv * config.linear_value_head_dim;
824 let la = format!("{lp}.linear_attn");
825
826 // Native keep-packed ternary Q2_0 GDN (Tier-1c): the GGUF
827 // loader kept `in_proj_qkv` (V-region row-permuted) and
828 // `in_proj_z` (row-permuted) 2-bit. Byte-concat them into the
829 // fused [Q|K|V|Z] `qkvz` and dispatch `q2_0_gemv_vec` at decode
830 // / transient-dequant at prefill. `out_proj` (a within-row
831 // COLUMN reorder) is NOT packed here — it stays NVFP4. a/b/
832 // conv1d/norm/A_log stay BF16/F32. Requires tp_size=1.
833 // ATLAS_NO_Q2_GDN forces the BF16/NVFP4 path for A/B bisection.
834 let gdn_q2 = config.tp_world_size.max(1) == 1
835 && std::env::var_os("ATLAS_NO_Q2_GDN").is_none()
836 && proj_q2_group(store, &format!("{la}.in_proj_qkv")).is_some()
837 && proj_q2_group(store, &format!("{la}.in_proj_z")).is_some();
838 if gdn_q2 {
839 let qkv_q2 = packed_q2_from_store(store, &format!("{la}.in_proj_qkv"))?;
840 let z_q2 = packed_q2_from_store(store, &format!("{la}.in_proj_z"))?;
841 anyhow::ensure!(
842 qkv_q2.group == z_q2.group && qkv_q2.k == z_q2.k,
843 "GDN packed qkv/z group|k mismatch ({},{} vs {},{})",
844 qkv_q2.group,
845 qkv_q2.k,
846 z_q2.group,
847 z_q2.k
848 );
849 // Byte-concat packed rows: [Q|K|V] ++ [Z]. Each row is
850 // (k/group)*block_bytes; whole-row copy never splits a block.
851 let group = qkv_q2.group as usize;
852 let block_bytes = 2 + group / 4;
853 let row_bytes = (qkv_q2.k as usize / group) * block_bytes;
854 let qkv_bytes = qkv_q2.n as usize * row_bytes;
855 let z_bytes = z_q2.n as usize * row_bytes;
856 let qkvz_buf = gpu.alloc(qkv_bytes + z_bytes)?;
857 gpu.copy_d2d(qkv_q2.weight, qkvz_buf, qkv_bytes)?;
858 gpu.copy_d2d(z_q2.weight, qkvz_buf.offset(qkv_bytes), z_bytes)?;
859 let qkvz_q2 = PackedQ2Weight {
860 weight: qkvz_buf,
861 n: qkv_q2.n + z_q2.n,
862 k: qkv_q2.k,
863 group: qkv_q2.group,
864 };
865 // out_proj + a/b/conv1d/norm are BF16/F32 in the store
866 // (sidecar dequanted the reorder tensors). out_proj → NVFP4.
867 let in_proj_a = dense_auto(store, &format!("{la}.in_proj_a.weight"), gpu)?;
868 let in_proj_b = dense_auto(store, &format!("{la}.in_proj_b.weight"), gpu)?;
869 let conv1d = dense(store, &format!("{la}.conv1d.weight"))?;
870 let a_log = dense_keep_f32(store, &format!("{la}.A_log"), gpu)?;
871 let dt_bias = dense_keep_f32(store, &format!("{la}.dt_bias"), gpu)?;
872 let norm = dense_f32_safe(store, &format!("{la}.norm.weight"), gpu)?;
873 let ba_dense = interleave_ba(&in_proj_a, &in_proj_b, nv, nk, h, gpu)?;
874 let out_proj_dense =
875 dense_auto(store, &format!("{la}.out_proj.weight"), gpu)?;
876 let out_proj_nvfp4 = quantize_to_nvfp4(
877 &out_proj_dense,
878 h,
879 value_dim,
880 gpu,
881 absmax_k,
882 quantize_k,
883 stream,
884 )?;
885 let out_proj_nvfp4_t =
886 out_proj_nvfp4.transpose_for_gemm(gpu, h, value_dim)?;
887 gpu.free(out_proj_dense.weight)?;
888 let ssm = SsmWeights {
889 in_proj_qkvz: DenseWeight {
890 weight: spark_runtime::gpu::DevicePtr::NULL,
891 },
892 in_proj_ba: ba_dense,
893 conv1d,
894 a_log,
895 dt_bias,
896 norm,
897 out_proj: out_proj_nvfp4,
898 };
899 let mut layer = Qwen3SsmLayer::new_sequential(
900 input_norm,
901 ssm,
902 post_attn_norm,
903 ffn,
904 None,
905 None,
906 Some(out_proj_nvfp4_t),
907 config,
908 gpu,
909 )?;
910 layer.set_packed_q2_qkvz(qkvz_q2, gpu);
911 layer.predequant_for_prefill(gpu, config, stream)?;
912 tracing::info!(
913 "SSM[{lp}] native keep-packed Q2_0 GDN: qkvz 2-bit \
914 (concat qkv+z row-permuted), out_proj NVFP4"
915 );
916 layers.push(Box::new(layer));
917 continue;
918 }
919
920 // SSM projections are loaded per-projection by on-disk dtype:
921 // each of in_proj_qkv / in_proj_z / out_proj may independently
922 // be NVFP4-packed (`weight_packed`) or plain (`weight`, routed
923 // by `dense_auto` → BF16/FP32/FP8). The unsloth NVFP4 re-quant
924 // of Qwen3.6-27B quantizes ONLY out_proj while keeping the
925 // in_proj_* in BF16; the old all-or-nothing gate (keyed on
926 // in_proj_qkv.weight_packed) then looked for a non-existent
927 // out_proj.weight and failed to build. `dense_auto` is dequant-
928 // to-BF16 for the concat pipeline regardless of source dtype.
929 let load_ssm_proj =
930 |name: &str, rows: usize, cols: usize| -> Result<DenseWeight> {
931 if store.contains(&format!("{name}.weight_packed")) {
932 dequant_nvfp4_to_bf16(store, name, rows, cols, gpu)
933 } else if matches!(
934 store.get(&format!("{name}.weight")).map(|w| w.dtype),
935 Ok(WeightDtype::UInt8)
936 ) {
937 // Standard-convention NVFP4 (packed bytes at
938 // `.weight`, not `.weight_packed`) — same dequant,
939 // different on-disk key.
940 dequant_nvfp4_to_bf16(store, name, rows, cols, gpu)
941 } else {
942 dense_auto(store, &format!("{name}.weight"), gpu)
943 }
944 };
945 // Native FP8 GDN (nvidia mixed-precision checkpoint): the
946 // in_proj_qkv / in_proj_z / out_proj projections ship as
947 // F8_E4M3 + per-tensor scale — modelopt's sensitivity
948 // analysis keeps the SSM projections high-precision. The
949 // default path (`load_ssm_proj` → `dense_auto`) dequants to
950 // BF16 then RE-quantizes to NVFP4 (4-bit), a lossy
951 // double-quant of these 48/64 layers that regressed BFCL-ST
952 // ~7pt (non_live 85.4→76.6). Load the on-disk FP8 directly
953 // (concat qkv+z on-device into [Q|K|V|Z] order) and route
954 // BOTH prefill (w8a16_gemm_pipelined) and decode
955 // (w8a16_gemv) through the fp8w fields — no requant, decode
956 // stays fast (FP8 = half BF16's weight bytes). MUST run
957 // BEFORE `load_ssm_proj` consumes the store tensors.
958 // Internal opt-out for the FP8-vs-NVFP4 GDN A/B + KL-drift
959 // gate (not a user choice; mirrors the `ATLAS_NO_*` debug
960 // levers). Default engages native FP8.
961 if std::env::var_os("ATLAS_NO_GDN_FP8").is_none()
962 && proj_is_fp8_any_scale(store, &format!("{la}.in_proj_qkv"))
963 && proj_is_fp8_any_scale(store, &format!("{la}.in_proj_z"))
964 && proj_is_fp8_any_scale(store, &format!("{la}.out_proj"))
965 {
966 let in_proj_a = dense(store, &format!("{la}.in_proj_a.weight"))?;
967 let in_proj_b = dense(store, &format!("{la}.in_proj_b.weight"))?;
968 let conv1d = dense(store, &format!("{la}.conv1d.weight"))?;
969 let a_log = dense_keep_f32(store, &format!("{la}.A_log"), gpu)?;
970 let dt_bias = dense_keep_f32(store, &format!("{la}.dt_bias"), gpu)?;
971 let norm = dense_f32_safe(store, &format!("{la}.norm.weight"), gpu)?;
972 let ba_dense = interleave_ba(&in_proj_a, &in_proj_b, nv, nk, h, gpu)?;
973 let qkv_f = load_fp8_block_scaled_as_fp8weight(
974 store,
975 &format!("{la}.in_proj_qkv"),
976 gpu,
977 )?;
978 let z_f = load_fp8_block_scaled_as_fp8weight(
979 store,
980 &format!("{la}.in_proj_z"),
981 gpu,
982 )?;
983 let out_f = load_fp8_block_scaled_as_fp8weight(
984 store,
985 &format!("{la}.out_proj"),
986 gpu,
987 )?;
988 let qkvz_f = concat_fp8_block_scaled(&qkv_f, &z_f, h, gpu)?;
989 // The concat copied both grids; free the per-projection
990 // scale allocs (weight bytes are store-owned, not freed).
991 gpu.free(qkv_f.row_scale)?;
992 gpu.free(z_f.row_scale)?;
993 let ssm = SsmWeights {
994 in_proj_qkvz: DenseWeight {
995 weight: spark_runtime::gpu::DevicePtr::NULL,
996 },
997 in_proj_ba: ba_dense,
998 conv1d,
999 a_log,
1000 dt_bias,
1001 norm,
1002 out_proj: crate::weight_map::QuantizedWeight::null(),
1003 };
1004 let mut layer = Qwen3SsmLayer::new_sequential(
1005 input_norm,
1006 ssm,
1007 post_attn_norm,
1008 ffn,
1009 None,
1010 None,
1011 None,
1012 config,
1013 gpu,
1014 )?;
1015 layer.set_fp8_decode_weights(Some(qkvz_f), Some(out_f));
1016 tracing::info!(
1017 "SSM[{lp}] native FP8 GDN: qkvz+out_proj block-scaled FP8 \
1018 (no NVFP4 requant; prefill+decode via w8a16)"
1019 );
1020 layers.push(Box::new(layer));
1021 continue;
1022 }
1023
1024 // A, B, conv1d, A_log, dt_bias, norm are independent of the
1025 // qkv/z/out_proj on-disk format below — load them once up
1026 // front so both the native-NVFP4 fast path and the legacy
1027 // dequant/requant path can share them.
1028 //
1029 // A_log and dt_bias MUST be FP32 — consumer kernels in
1030 // `ssm_preprocess.cu` and `mamba2_ssm_decode.cu` declare
1031 // them `const float*`. Loading via `dense()` kept BF16
1032 // storage, reinterpreting 48-elt BF16 (96B) as 48-elt
1033 // FP32 → per-head scrambled decay gates and exponential
1034 // error amplification through GDR recurrence at long
1035 // context. The MoE sister loader (`ssm_qwen35.rs`)
1036 // already promotes these; dense was missing the mirror.
1037 //
1038 // in_proj_a/b: route through `load_ssm_proj` (not the raw
1039 // `dense()` byte-reinterpret) so a Standard-NVFP4 A/B
1040 // (U8-packed) checkpoint dequants correctly instead of
1041 // being read as BF16 garbage.
1042 let in_proj_a = load_ssm_proj(&format!("{la}.in_proj_a"), nv, h)?;
1043 let in_proj_b = load_ssm_proj(&format!("{la}.in_proj_b"), nv, h)?;
1044 let conv1d = dense(store, &format!("{la}.conv1d.weight"))?;
1045 let a_log = dense_keep_f32(store, &format!("{la}.A_log"), gpu)?;
1046 let dt_bias = dense_keep_f32(store, &format!("{la}.dt_bias"), gpu)?;
1047 // norm.weight: use `dense_f32_safe` (FP32-aware: detects
1048 // a fp32 checkpoint and truncates to BF16 with logging;
1049 // bf16 passes through). Mirrors `weight_map/ssm_qwen35.rs`
1050 // MoE sister loader (backported here 2026-05-20).
1051 let norm = dense_f32_safe(store, &format!("{la}.norm.weight"), gpu)?;
1052 let ba_dense = interleave_ba(&in_proj_a, &in_proj_b, nv, nk, h, gpu)?;
1053 let qkvz_size = config.ssm_qkvz_size();
1054
1055 // Native Standard-NVFP4 GDN (pre-quantized checkpoint, e.g.
1056 // sakamakismile): in_proj_qkv / in_proj_z / out_proj ship
1057 // U8-packed NVFP4 directly on disk (`.weight` dtype UInt8,
1058 // not `.weight_packed` — that's the compressed-tensors
1059 // convention `load_ssm_proj` already dequants above). Load
1060 // them straight into `QuantizedWeight` and concat on GPU,
1061 // skipping the BF16-dequant→re-quantize roundtrip entirely
1062 // (that roundtrip is what the FP8/BF16-opt-in paths above
1063 // exist to avoid for FP8-native and BF16-preferring
1064 // checkpoints; here there's no lossy step to avoid in the
1065 // first place — the data is already NVFP4). Requires all
1066 // three projections to be U8; a partial-U8 checkpoint
1067 // falls through to the legacy path below, where the
1068 // `load_ssm_proj` UInt8 branch added above still dequants
1069 // each U8 tensor correctly on its own.
1070 let native_nvfp4 = matches!(
1071 store
1072 .get(&format!("{la}.in_proj_qkv.weight"))
1073 .map(|w| w.dtype),
1074 Ok(WeightDtype::UInt8)
1075 ) && matches!(
1076 store
1077 .get(&format!("{la}.in_proj_z.weight"))
1078 .map(|w| w.dtype),
1079 Ok(WeightDtype::UInt8)
1080 ) && matches!(
1081 store.get(&format!("{la}.out_proj.weight")).map(|w| w.dtype),
1082 Ok(WeightDtype::UInt8)
1083 );
1084 if native_nvfp4 {
1085 let qkv_qw = quantized_auto(
1086 store,
1087 &format!("{la}.in_proj_qkv"),
1088 gpu,
1089 Nvfp4Variant::Standard,
1090 )?;
1091 let z_qw = quantized_auto(
1092 store,
1093 &format!("{la}.in_proj_z"),
1094 gpu,
1095 Nvfp4Variant::Standard,
1096 )?;
1097 let qkvz_nvfp4 = qkv_qw.concat_rows(&z_qw, qkv_rows, z_rows, h, gpu)?;
1098 let qkvz_nvfp4_t = qkvz_nvfp4.transpose_for_gemm(gpu, qkvz_size, h)?;
1099
1100 let out_proj_nvfp4 = quantized_auto(
1101 store,
1102 &format!("{la}.out_proj"),
1103 gpu,
1104 Nvfp4Variant::Standard,
1105 )?;
1106 let out_proj_nvfp4_t =
1107 out_proj_nvfp4.transpose_for_gemm(gpu, h, value_dim)?;
1108
1109 let ssm = SsmWeights {
1110 in_proj_qkvz: DenseWeight {
1111 weight: spark_runtime::gpu::DevicePtr::NULL,
1112 },
1113 in_proj_ba: ba_dense,
1114 conv1d,
1115 a_log,
1116 dt_bias,
1117 norm,
1118 out_proj: out_proj_nvfp4,
1119 };
1120 let mut layer = Qwen3SsmLayer::new_sequential(
1121 input_norm,
1122 ssm,
1123 post_attn_norm,
1124 ffn,
1125 Some(qkvz_nvfp4),
1126 Some(qkvz_nvfp4_t),
1127 Some(out_proj_nvfp4_t),
1128 config,
1129 gpu,
1130 )?;
1131 layer.predequant_for_prefill(gpu, config, stream)?;
1132 tracing::info!(
1133 "SSM[{lp}] native NVFP4 GDN: qkvz+out_proj loaded pre-quantized \
1134 (U8-packed on disk; no BF16 dequant/requant roundtrip)"
1135 );
1136 layers.push(Box::new(layer));
1137 continue;
1138 }
1139
1140 // PER-ROW FP8 for the row-wise cuBLASLt PREFILL arm
1141 // (`ATLAS_FP8_ROWWISE=1`). Read from the store BEFORE the
1142 // dequant below, though the bytes are store-owned either
1143 // way; the NVFP4 build continues underneath because decode
1144 // still needs it — `w8a16_gemv` cannot index a per-row
1145 // scale. See weight_loader/qwen35_dense/rowwise_fp8.rs.
1146 let rowwise_gdn = rowwise_fp8::rowwise_fp8_enabled()
1147 && rowwise_fp8::proj_is_fp8_per_row(store, &format!("{la}.in_proj_qkv"))
1148 && rowwise_fp8::proj_is_fp8_per_row(store, &format!("{la}.in_proj_z"))
1149 && rowwise_fp8::proj_is_fp8_per_row(store, &format!("{la}.out_proj"));
1150 let (qkvz_rowwise, out_proj_rowwise) = if rowwise_gdn && tp_size == 1 {
1151 let qkv_r = rowwise_fp8::load_fp8_per_row(
1152 store,
1153 &format!("{la}.in_proj_qkv"),
1154 gpu,
1155 )?;
1156 let z_r =
1157 rowwise_fp8::load_fp8_per_row(store, &format!("{la}.in_proj_z"), gpu)?;
1158 let out_r =
1159 rowwise_fp8::load_fp8_per_row(store, &format!("{la}.out_proj"), gpu)?;
1160 let qkvz_r = rowwise_fp8::concat_fp8_per_row(&qkv_r, &z_r, h, gpu)?;
1161 // The concat copied both scale vectors; free the
1162 // per-projection allocs. Weight bytes are store-owned.
1163 gpu.free(qkv_r.row_scale)?;
1164 gpu.free(z_r.row_scale)?;
1165 (Some(qkvz_r), Some(out_r))
1166 } else {
1167 (None, None)
1168 };
1169
1170 let qkv_dense = load_ssm_proj(&format!("{la}.in_proj_qkv"), qkv_rows, h)?;
1171 let z_dense = load_ssm_proj(&format!("{la}.in_proj_z"), z_rows, h)?;
1172 let out_proj_dense =
1173 load_ssm_proj(&format!("{la}.out_proj"), h, dims.full_value_dim())?;
1174
1175 let qkvz_dense =
1176 gpu_concat_rows(&qkv_dense, qkv_rows, &z_dense, z_rows, h, gpu)?;
1177 // qkv/z BF16 are only inputs to the concat above; free them now
1178 // rather than leaking them for the layer's lifetime (Atlas issue #A1).
1179 gpu.free(qkv_dense.weight)?;
1180 gpu.free(z_dense.weight)?;
1181
1182 let ba_dense =
1183 interleave_ba(&in_proj_a, &in_proj_b, dims.full_nv, dims.full_nk, h, gpu)?;
1184
1185 // GDN HeadParallel shard: cut the FULL concat/interleave/on-disk buffers
1186 // to this rank's contiguous head range (mirrors the MoE loader,
1187 // linear_attn_arms.rs). Runs BEFORE both consumers below (the Bf16Raw
1188 // branch AND the NVFP4/FP8 path), so both get sharded weights. qkvz
1189 // (segmented [Q|K|V|Z]) + conv (segmented [Q|K|V]) slice per-block; ba
1190 // slices contiguously; a_log/dt_bias are per-value-head FP32 scalars;
1191 // out_proj is row-parallel on value_dim (partials summed by the
1192 // post-out_proj all-reduce already in Qwen3SsmLayer::forward). norm is
1193 // [vd] shared across value heads -> REPLICATE (never sliced). Free only
1194 // the fresh qkvz/ba concat buffers; conv/a_log/dt_bias/out_proj sources
1195 // are store aliases or dequant bufs freed downstream (the NVFP4 path
1196 // frees the sharded qkvz/out_proj later). At tp==1 the else arm is a
1197 // pure pass-through and every slicer no-ops -> byte-identical.
1198 let (qkvz_dense, ba_dense, conv1d, a_log, dt_bias, out_proj_dense) = if tp_size
1199 > 1
1200 {
1201 let d_conv = config.linear_conv_kernel_dim;
1202 let (qkvz_ptr, _, _) = shard_gdn_qkvz_rows(qkvz_dense.weight, &dims, gpu)?;
1203 gpu.free(qkvz_dense.weight)?;
1204 let (ba_ptr, _, _) = shard_gdn_ba_rows(ba_dense.weight, &dims, gpu)?;
1205 gpu.free(ba_dense.weight)?;
1206 let (conv_ptr, _, _) =
1207 shard_gdn_conv_rows(conv1d.weight, &dims, d_conv, gpu)?;
1208 let (a_log_ptr, _) =
1209 shard_gdn_value_vector(a_log.weight, &dims, 1, 4, gpu)?;
1210 let (dt_bias_ptr, _) =
1211 shard_gdn_value_vector(dt_bias.weight, &dims, 1, 4, gpu)?;
1212 let (out_ptr, _, _) =
1213 shard_gdn_out_proj_row_parallel(out_proj_dense.weight, &dims, gpu)?;
1214 (
1215 DenseWeight { weight: qkvz_ptr },
1216 DenseWeight { weight: ba_ptr },
1217 DenseWeight { weight: conv_ptr },
1218 DenseWeight { weight: a_log_ptr },
1219 DenseWeight {
1220 weight: dt_bias_ptr,
1221 },
1222 DenseWeight { weight: out_ptr },
1223 )
1224 } else {
1225 (qkvz_dense, ba_dense, conv1d, a_log, dt_bias, out_proj_dense)
1226 };
1227
1228 // Native-BF16 SSM arm (no-metadata dense Holo checkpoints:
1229 // Nvfp4Variant::Bf16Raw). The standard nvfp4 bundle ships the
1230 // common/ BF16 dense kernels (dense_gemv_bf16 / dense_gemm_bf16
1231 // / dense_gemm_bf16_pipelined) that every SSM forward arm's
1232 // dense fallback already dispatches, so keep the concatenated
1233 // qkvz_dense [Q|K|V|Z] and out_proj_dense ALIVE and route
1234 // through those instead of the lossy BF16->NVFP4 runtime
1235 // requant. No NVFP4/FP8 copy is built at all: in_proj_qkvz +
1236 // out_proj_dense feed dense_gemv (per-seq decode,
1237 // ssm_forward.rs:107/412), dense_gemm (batched decode,
1238 // trait_decode_batched.rs:113/350 + ssm_batched.rs:180/279)
1239 // and dense_gemm_bf16_pipelined (prefill,
1240 // trait_prefill_proj.rs:298 + trait_prefill_helper.rs:89).
1241 // ssm.out_proj stays null — every out_proj arm prefers
1242 // out_proj_dense when Some; predequant_for_prefill /
1243 // set_fp8_prefill_only_weights are skipped (NVFP4/FP8 only).
1244 if matches!(variant, Nvfp4Variant::Bf16Raw) {
1245 let ssm = SsmWeights {
1246 in_proj_qkvz: qkvz_dense,
1247 in_proj_ba: ba_dense,
1248 conv1d,
1249 a_log,
1250 dt_bias,
1251 norm,
1252 out_proj: crate::weight_map::QuantizedWeight::null(),
1253 };
1254 let mut layer = Qwen3SsmLayer::new_sequential(
1255 input_norm,
1256 ssm,
1257 post_attn_norm,
1258 ffn,
1259 None, // qkvz_nvfp4 — BF16 dense fallback used instead
1260 None, // qkvz_nvfp4_t
1261 None, // out_proj_nvfp4_t
1262 config,
1263 gpu,
1264 )?;
1265 // pub field (qwen3_ssm/mod.rs:46); selected by every
1266 // out_proj arm (ssm_forward.rs:412,
1267 // trait_decode_batched.rs:350, ssm_batched.rs:279,
1268 // trait_prefill_helper.rs:89).
1269 layer.out_proj_dense = Some(out_proj_dense);
1270 layers.push(Box::new(layer));
1271 continue;
1272 }
1273
1274 let qkvz_size = config.ssm_qkvz_size();
1275
1276 // GDN ≥FP8 precision policy (2026-07-04). The nvidia
1277 // Qwen3.6-27B-NVFP4 checkpoint ships GDN in_proj_qkv /
1278 // out_proj as native F8_E4M3 (modelopt sensitivity
1279 // analysis deliberately keeps the SSM projections
1280 // high-precision); `load_ssm_proj` dequants them to BF16,
1281 // and the code below then RE-quantizes to NVFP4 (4-bit) —
1282 // a lossy double-quant of the exact tensors the toolchain
1283 // protected. That regressed BFCL-ST ~7pt (non_live 85.4→
1284 // 76.6) vs the 06-15 reference. When enabled, keep the
1285 // BF16 dequant (≥FP8) and route qkvz + out_proj through the
1286 // dense_gemv / dense_gemm dispatch (in_proj_qkvz +
1287 // out_proj_dense fields), mirroring the MoE sister loader's
1288 // arm (qwen35/load_layers/linear_attn_arms.rs). Gated for a
1289 // clean A/B + KL-drift gate before flipping the default.
1290 let gdn_bf16 = matches!(
1291 std::env::var("ATLAS_GDN_BF16_WEIGHTS").ok().as_deref(),
1292 Some("1")
1293 );
1294 if gdn_bf16 {
1295 let ssm = SsmWeights {
1296 in_proj_qkvz: DenseWeight {
1297 weight: qkvz_dense.weight,
1298 },
1299 in_proj_ba: ba_dense,
1300 conv1d,
1301 a_log,
1302 dt_bias,
1303 norm,
1304 // Unused: out_proj_dense (set below) has higher
1305 // dispatch priority in both prefill and decode.
1306 out_proj: crate::weight_map::QuantizedWeight::null(),
1307 };
1308 let mut layer = Qwen3SsmLayer::new_sequential(
1309 input_norm,
1310 ssm,
1311 post_attn_norm,
1312 ffn,
1313 None,
1314 None,
1315 None,
1316 config,
1317 gpu,
1318 )?;
1319 layer.out_proj_dense = Some(out_proj_dense);
1320 tracing::info!(
1321 "SSM[{lp}] ATLAS_GDN_BF16_WEIGHTS: qkvz + out_proj kept BF16 \
1322 (≥FP8; NVFP4 requant skipped)"
1323 );
1324 layers.push(Box::new(layer));
1325 continue;
1326 }
1327
1328 let qkvz_nvfp4 = quantize_to_nvfp4(
1329 &qkvz_dense,
1330 qkvz_size,
1331 h,
1332 gpu,
1333 absmax_k,
1334 quantize_k,
1335 stream,
1336 )?;
1337
1338 let qkvz_nvfp4_t = qkvz_nvfp4.transpose_for_gemm(gpu, qkvz_size, h)?;
1339
1340 let out_proj_nvfp4 = quantize_to_nvfp4(
1341 &out_proj_dense,
1342 h,
1343 value_dim,
1344 gpu,
1345 absmax_k,
1346 quantize_k,
1347 stream,
1348 )?;
1349
1350 let out_proj_nvfp4_t = out_proj_nvfp4.transpose_for_gemm(gpu, h, value_dim)?;
1351
1352 // Native FP8 SSM prefill GEMM: build a single-scale FP8
1353 // copy of `qkvz_dense` [qkvz_size, h] and `out_proj_dense`
1354 // [h, value_dim] by direct BF16→FP8 truncation. SSM weight
1355 // magnitudes fit in FP8 E4M3 range (|w| ≤ 448), so no
1356 // separate scalar dequant is needed at GEMM time — the
1357 // `fp8_gemm_n128` kernel interprets the FP8 bytes as
1358 // values directly (mirrors how `predequant_nvfp4_to_fp8`
1359 // bakes `scale2` into the FP8 stream). PCND: gated.
1360 let (qkvz_fp8_prefill, out_proj_fp8_prefill) =
1361 if let Some(b2f_k) = bf16_to_fp8_k {
1362 let qkvz_total = (qkvz_size * h) as u32;
1363 let qkvz_fp8 = gpu.alloc(qkvz_size * h)?;
1364 crate::layers::ops::bf16_to_fp8(
1365 gpu,
1366 b2f_k,
1367 qkvz_dense.weight,
1368 qkvz_fp8,
1369 qkvz_total,
1370 stream,
1371 )?;
1372 let out_total = (h * value_dim) as u32;
1373 let out_fp8 = gpu.alloc(h * value_dim)?;
1374 crate::layers::ops::bf16_to_fp8(
1375 gpu,
1376 b2f_k,
1377 out_proj_dense.weight,
1378 out_fp8,
1379 out_total,
1380 stream,
1381 )?;
1382 gpu.synchronize(stream)?;
1383 (Some(qkvz_fp8), Some(out_fp8))
1384 } else {
1385 (None, None)
1386 };
1387
1388 // SSM prefill/decode always dispatch qkvz_nvfp4/_t and the NVFP4
1389 // out_proj; the BF16 qkvz_dense / out_proj_dense were only quantize
1390 // inputs. Free them rather than keep a third full-precision copy of
1391 // the largest SSM tensor across every layer (Atlas issue #A1).
1392 gpu.free(qkvz_dense.weight)?;
1393 gpu.free(out_proj_dense.weight)?;
1394
1395 let ssm = SsmWeights {
1396 in_proj_qkvz: DenseWeight {
1397 weight: spark_runtime::gpu::DevicePtr::NULL,
1398 },
1399 in_proj_ba: ba_dense,
1400 conv1d,
1401 a_log,
1402 dt_bias,
1403 norm,
1404 out_proj: out_proj_nvfp4,
1405 };
1406
1407 let mut layer = Qwen3SsmLayer::new_sequential(
1408 input_norm,
1409 ssm,
1410 post_attn_norm,
1411 ffn,
1412 Some(qkvz_nvfp4),
1413 Some(qkvz_nvfp4_t),
1414 Some(out_proj_nvfp4_t),
1415 config,
1416 gpu,
1417 )?;
1418 layer.predequant_for_prefill(gpu, config, stream)?;
1419 // Install the FP8 prefill weights AFTER `predequant_for_prefill`
1420 // (which sets `out_proj_fp8` from NVFP4 + scale2). The
1421 // native-FP8 path overrides both pointers when active,
1422 // routing prefill through `fp8_gemm_n128` instead of
1423 // `w4a16_gemm_t`. Decode batch paths keep their NVFP4
1424 // fallback (the `qkvz_nvfp4*` fields above).
1425 if qkvz_fp8_prefill.is_some() || out_proj_fp8_prefill.is_some() {
1426 layer.set_fp8_prefill_only_weights(qkvz_fp8_prefill, out_proj_fp8_prefill);
1427 }
1428 // …and LAST, so it wins over both of the prefill installs
1429 // above: the checkpoint's own per-row FP8, which reaches
1430 // the GEMM with no conversion at all. Decode is untouched.
1431 if qkvz_rowwise.is_some() {
1432 layer.set_fp8_rowwise_prefill_weights(qkvz_rowwise, out_proj_rowwise);
1433 if i == 0 {
1434 tracing::info!(
1435 "SSM[{lp}] ATLAS_FP8_ROWWISE: qkvz + out_proj prefill via \
1436 native per-row FP8 (no BF16 dequant, no NVFP4 requant); \
1437 decode keeps NVFP4"
1438 );
1439 }
1440 }
1441 layers.push(Box::new(layer));
1442 }
1443 LayerType::SlidingAttention => {
1444 unreachable!("unexpected SlidingAttention in this loader")
1445 }
1446 LayerType::Moe => unreachable!("Qwen3.5 dense has no standalone MoE layers"),
1447 // GLM-5.3's `deepseek_sparse_attention`: a full-rank mixer whose visible key set
1448 // is chosen at runtime by an indexer. Hard error, not a silent fallthrough into
1449 // the dense-attention arm -- that would attend over the WHOLE cache and look right.
1450 LayerType::SparseAttention => anyhow::bail!(
1451 "layer {i}: SparseAttention needs a DSA indexer and per-query top-k; Qwen3.5 dense has neither"
1452 ),
1453 }
1454
1455 if (i + 1) % 10 == 0 {
1456 tracing::info!("Loaded layers 0..{}", i + 1);
1457 spark_runtime::progress::layer(i + 1, config.num_hidden_layers);
1458 }
1459 }
1460
1461 tracing::info!(
1462 "Qwen3.5 dense weight loader: {} layers ({} attention, {} SSM, dense FFN)",
1463 layers.len(),
1464 attn_idx,
1465 layers.len() - attn_idx,
1466 );
1467
1468 Ok(layers)
1469 }
1470
1471 fn load_embedding(
1472 &self,
1473 store: &WeightStore,
1474 config: &ModelConfig,
1475 _gpu: &dyn GpuBackend,
1476 ) -> Result<DenseWeight> {
1477 loaders_b::load_embedding(store, config)
1478 }
1479
1480 fn load_final_norm(
1481 &self,
1482 store: &WeightStore,
1483 config: &ModelConfig,
1484 _gpu: &dyn GpuBackend,
1485 ) -> Result<DenseWeight> {
1486 loaders_b::load_final_norm(store, config)
1487 }
1488
1489 fn load_lm_head(
1490 &self,
1491 store: &WeightStore,
1492 config: &ModelConfig,
1493 gpu: &dyn GpuBackend,
1494 ) -> Result<DenseWeight> {
1495 loaders_b::load_lm_head(store, config, gpu)
1496 }
1497
1498 fn load_mtp_weights(
1499 &self,
1500 store: &WeightStore,
1501 config: &ModelConfig,
1502 gpu: &dyn GpuBackend,
1503 ) -> Result<Option<MtpWeights>> {
1504 if !store.contains("mtp.fc.weight") {
1505 return Ok(None);
1506 }
1507 let variant = detect_nvfp4_variant(store, config);
1508 tracing::info!(
1509 "Loading dense MTP weights (variant={:?}, hidden={}, inter={})",
1510 variant,
1511 config.hidden_size,
1512 config.intermediate_size,
1513 );
1514 // `load_mtp` auto-detects MoE vs dense FFN by inspecting the weight
1515 // names. For dense Qwen3.6-27B-FP8 it returns a MtpWeights with
1516 // `dense_ffn = Some(...)` and NULL placeholders for the MoE fields.
1517 let mtp = load_mtp(store, config.num_experts, gpu, variant)?;
1518 if mtp.dense_ffn.is_some() {
1519 tracing::info!("Dense MTP head ready (FP8 e4m3 projections + dense gate/up/down MLP)");
1520 } else {
1521 tracing::info!(
1522 "MoE MTP head ready ({} experts) — dense loader sees MoE bundle",
1523 mtp.experts.len(),
1524 );
1525 }
1526 Ok(Some(mtp))
1527 }
1528
1529 fn load_vision_encoder(
1530 &self,
1531 store: &WeightStore,
1532 config: &ModelConfig,
1533 gpu: &dyn GpuBackend,
1534 ) -> Result<Option<crate::layers::VisionEncoder>> {
1535 // Dense Qwen3.5 / Holo VL checkpoints (e.g. Holo-3.1-0.8B, Ornith-1.0-9B)
1536 // ship the SAME Qwen3-VL ViT tower as their MoE siblings. The MoE
1537 // loader's `load_vision_encoder` reads only `store` + `config.vision`
1538 // (no MoE-specific state), so reuse it verbatim. The shared model
1539 // forward (`model/trait_impl/*`, gated on `vision_encoder.is_some()`)
1540 // then merges image embeddings — no dense-specific forward changes.
1541 super::qwen35::Qwen35WeightLoader.load_vision_encoder(store, config, gpu)
1542 }
1543}