1use anyhow::Result;
4use atlas_core::config::{LayerType, ModelConfig};
5use spark_runtime::gpu::GpuBackend;
6use spark_runtime::kv_cache::KvCacheDtype;
7use spark_runtime::weights::WeightStore;
8
9use super::{ModelWeightLoader, QuantFormat};
10use crate::layer::TransformerLayer;
11use crate::layers::{FfnComponent, MoeLayer, Qwen3AttentionLayer, Qwen3SsmLayer};
12use crate::tp_shard::{TpShardKind, load_qkvo_tp, shard_dense_bf16, shard_fp8_block_scaled};
13use crate::weight_map::{
14 AttentionWeights, DenseWeight, MtpWeights, Nvfp4Variant, QuantizeCtx, QuantizedWeight, dense,
15 detect_nvfp4_variant, load_attention, load_fp8_block_scaled_as_fp8weight, load_kv_scales,
16 load_moe, load_moe_qwen35_fp8_experts, load_moe_skip_experts, load_mtp, load_ssm,
17 quantize_to_nvfp4,
18};
19
20pub struct Qwen3WeightLoader;
21
22impl ModelWeightLoader for Qwen3WeightLoader {
23 fn supports_tp(&self) -> bool {
24 true
30 }
31
32 fn load_layers(
33 &self,
34 store: &WeightStore,
35 config: &ModelConfig,
36 gpu: &dyn GpuBackend,
37 layer_kv_dtypes: &[KvCacheDtype],
38 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
39 let layer_types = if config.layer_types.is_empty() {
40 (0..config.num_hidden_layers)
41 .map(|i| config.layer_type(i))
42 .collect::<Vec<_>>()
43 } else {
44 config.layer_types.clone()
45 };
46
47 let mut layers: Vec<Box<dyn TransformerLayer>> =
48 Vec::with_capacity(config.num_hidden_layers);
49 let mut attn_idx = 0usize;
50
51 let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
53 let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
54 let stream = gpu.default_stream();
55 let qctx = QuantizeCtx {
56 absmax_k,
57 quantize_k,
58 stream,
59 };
60
61 let variant = detect_nvfp4_variant(store, config);
63 let quant_format = if variant == Nvfp4Variant::Fp8Dequanted {
64 QuantFormat::Fp8
65 } else {
66 QuantFormat::Nvfp4
67 };
68 let native_fp8 = quant_format == QuantFormat::Fp8;
69 tracing::info!(
70 "Qwen3 weight variant: {:?}, native_fp8: {}",
71 variant,
72 native_fp8
73 );
74
75 let h = config.hidden_size;
76
77 let skip_moe_transpose = !super::moe_prefill_copies_fit(config, gpu);
81
82 for (i, lt) in layer_types.iter().enumerate() {
83 let lp = config.layer_prefix(i);
84 let input_norm = dense(store, &format!("{lp}.input_layernorm.weight"))?;
85 let post_attn_norm = dense(store, &format!("{lp}.post_attention_layernorm.weight"))?;
86
87 let moe_weights = if native_fp8 {
89 load_moe_skip_experts(store, &lp, config.num_experts, gpu, config, variant, qctx)?
90 } else {
91 load_moe(store, &lp, config.num_experts, gpu, config, variant, qctx)?
92 };
93 let gate_nvfp4 = if std::env::var("ATLAS_BF16_ROUTER").as_deref() == Ok("1") {
103 None
104 } else {
105 Some(quantize_to_nvfp4(
106 &moe_weights.gate,
107 config.num_experts,
108 h,
109 gpu,
110 absmax_k,
111 quantize_k,
112 stream,
113 )?)
114 };
115 let mut moe_layer =
116 MoeLayer::new(moe_weights, config.num_experts, gate_nvfp4, gpu, config)?;
117 if !native_fp8 && !skip_moe_transpose {
118 moe_layer.transpose_for_prefill(gpu, config)?;
119 }
120 if !native_fp8 {
121 moe_layer.predequant_for_prefill(gpu, config, stream)?;
122 }
123
124 if native_fp8
126 && let Ok(fp8_experts) =
127 load_moe_qwen35_fp8_experts(store, &lp, config.num_experts, gpu, config)
128 {
129 let sp = format!("{lp}.mlp.shared_expert");
130 use crate::weight_map::{Fp8ExpertWeight as FEW, Fp8Weight as FW};
131 use spark_runtime::gpu::DevicePtr;
132 let null_fw = FW {
133 weight: DevicePtr::NULL,
134 row_scale: DevicePtr::NULL,
135 n: 0,
136 k: 0,
137 scale_format: crate::weight_map::WeightQuantFormat::Fp8BlockScaled,
143 };
144 let sh_gate =
145 load_fp8_block_scaled_as_fp8weight(store, &format!("{sp}.gate_proj"), gpu);
146 let sh_up =
147 load_fp8_block_scaled_as_fp8weight(store, &format!("{sp}.up_proj"), gpu);
148 let sh_down =
149 load_fp8_block_scaled_as_fp8weight(store, &format!("{sp}.down_proj"), gpu);
150 let shared_fp8 = FEW {
151 gate_proj: sh_gate.unwrap_or(null_fw),
152 up_proj: sh_up.unwrap_or(null_fw),
153 down_proj: sh_down.unwrap_or(null_fw),
154 };
155 if let Err(e) = moe_layer.set_fp8_experts(&fp8_experts, shared_fp8, gpu) {
156 tracing::error!("Layer {i}: FP8 expert tables failed: {e:#}");
157 } else {
158 tracing::info!("Layer {i}: MoE experts loaded as native FP8");
159 }
160 }
161
162 let ffn = FfnComponent::Moe(moe_layer);
163
164 match lt {
165 LayerType::FullAttention if native_fp8 => {
167 let p = format!("{lp}.self_attn");
168 let tp_rank = config.tp_rank;
169 let tp_size = config.tp_world_size.max(1);
170 let block_size = 128usize;
171 let load_fp8 = |name: &str,
172 _full_n: usize,
173 _full_k: usize,
174 kind: TpShardKind|
175 -> Result<crate::weight_map::Fp8Weight> {
176 let src =
177 load_fp8_block_scaled_as_fp8weight(store, &format!("{p}.{name}"), gpu)?;
178 if tp_size == 1 {
179 return Ok(src);
180 }
181 let sharded =
182 shard_fp8_block_scaled(&src, kind, tp_rank, tp_size, block_size, gpu)?;
183 gpu.free(src.weight)?;
184 gpu.free(src.row_scale)?;
185 Ok(sharded)
186 };
187 let [q_fp8, k_fp8, v_fp8, o_fp8] = load_qkvo_tp(config, load_fp8)?;
188
189 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu);
190 let dummy = DenseWeight {
191 weight: spark_runtime::gpu::DevicePtr::NULL,
192 };
193 let attn = AttentionWeights {
194 q_proj: dummy,
195 k_proj: dummy,
196 v_proj: dummy,
197 o_proj: QuantizedWeight::null(),
198 q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
199 k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
200 q_norm_full: None,
201 k_norm_full: None,
202 k_scale,
203 v_scale,
204 };
205
206 let layer_kv_dtype = layer_kv_dtypes[attn_idx];
207 let mut layer = Qwen3AttentionLayer::new(
208 input_norm,
209 attn,
210 post_attn_norm,
211 ffn,
212 attn_idx,
213 None,
214 None,
215 None, gpu,
217 layer_kv_dtype,
218 config.fp8_kv_calibration_tokens,
219 config,
220 )?;
221 layer.set_fp8_weights(Some(q_fp8), Some(k_fp8), Some(v_fp8), Some(o_fp8));
222 if let Err(e) = layer.transpose_fp8_for_prefill(gpu, stream) {
223 tracing::warn!("Layer {i}: FP8 transpose failed: {e}");
224 }
225 layers.push(Box::new(layer));
226 attn_idx += 1;
227 }
228 LayerType::FullAttention => {
230 let mut attn = load_attention(store, &lp, gpu, variant, qctx, config)?;
231 let tp_rank = config.tp_rank;
232 let tp_size = config.tp_world_size.max(1);
233 if tp_size > 1 {
238 use crate::tp_shard::TpAttentionDims;
239 let dims = TpAttentionDims::from_config(config);
240 let (qp, _, _) = shard_dense_bf16(
241 attn.q_proj.weight,
242 dims.full_q_n,
243 dims.h,
244 TpShardKind::ColumnParallel,
245 tp_rank,
246 tp_size,
247 gpu,
248 )?;
249 if qp != attn.q_proj.weight {
250 gpu.free(attn.q_proj.weight)?;
251 }
252 attn.q_proj.weight = qp;
253 let (kp, _, _) = shard_dense_bf16(
254 attn.k_proj.weight,
255 dims.full_kv_n,
256 dims.h,
257 TpShardKind::ColumnParallel,
258 tp_rank,
259 tp_size,
260 gpu,
261 )?;
262 if kp != attn.k_proj.weight {
263 gpu.free(attn.k_proj.weight)?;
264 }
265 attn.k_proj.weight = kp;
266 let (vp, _, _) = shard_dense_bf16(
267 attn.v_proj.weight,
268 dims.full_kv_n,
269 dims.h,
270 TpShardKind::ColumnParallel,
271 tp_rank,
272 tp_size,
273 gpu,
274 )?;
275 if vp != attn.v_proj.weight {
276 gpu.free(attn.v_proj.weight)?;
277 }
278 attn.v_proj.weight = vp;
279 let (op, _, _) = shard_dense_bf16(
280 attn.o_proj.weight,
281 dims.h,
282 dims.full_o_in,
283 TpShardKind::RowParallel,
284 tp_rank,
285 tp_size,
286 gpu,
287 )?;
288 if op != attn.o_proj.weight {
289 gpu.free(attn.o_proj.weight)?;
290 }
291 attn.o_proj.weight = op;
292 }
293 let q_nvfp4 = quantize_to_nvfp4(
294 &attn.q_proj,
295 config.num_attention_heads * config.head_dim * 2,
296 h,
297 gpu,
298 absmax_k,
299 quantize_k,
300 stream,
301 )?;
302 let k_nvfp4 = quantize_to_nvfp4(
303 &attn.k_proj,
304 config.num_key_value_heads * config.head_dim,
305 h,
306 gpu,
307 absmax_k,
308 quantize_k,
309 stream,
310 )?;
311 let v_nvfp4 = quantize_to_nvfp4(
312 &attn.v_proj,
313 config.num_key_value_heads * config.head_dim,
314 h,
315 gpu,
316 absmax_k,
317 quantize_k,
318 stream,
319 )?;
320 let layer_kv_dtype = layer_kv_dtypes[attn_idx];
321 let mut layer = Qwen3AttentionLayer::new(
322 input_norm,
323 attn,
324 post_attn_norm,
325 ffn,
326 attn_idx,
327 Some(q_nvfp4),
328 Some(k_nvfp4),
329 Some(v_nvfp4),
330 gpu,
331 layer_kv_dtype,
332 config.fp8_kv_calibration_tokens,
333 config,
334 )?;
335 let qt = q_nvfp4.transpose_for_gemm(
336 gpu,
337 config.num_attention_heads * config.head_dim * 2,
338 h,
339 )?;
340 let kt = k_nvfp4.transpose_for_gemm(
341 gpu,
342 config.num_key_value_heads * config.head_dim,
343 h,
344 )?;
345 let vt = v_nvfp4.transpose_for_gemm(
346 gpu,
347 config.num_key_value_heads * config.head_dim,
348 h,
349 )?;
350 let ot = layer.attn.o_proj.transpose_for_gemm(
351 gpu,
352 h,
353 config.num_attention_heads * config.head_dim,
354 )?;
355 layer.set_prefill_weights(Some(qt), Some(kt), Some(vt), Some(ot));
356 layer.predequant_for_prefill(gpu, config, stream)?;
357 layers.push(Box::new(layer));
358 attn_idx += 1;
359 }
360 LayerType::LinearAttention => {
366 let ssm = load_ssm(store, &lp, gpu, variant, qctx, config)?;
367 let qkvz_nvfp4 = quantize_to_nvfp4(
368 &ssm.in_proj_qkvz,
369 config.ssm_qkvz_size(),
370 h,
371 gpu,
372 absmax_k,
373 quantize_k,
374 stream,
375 )?;
376 layers.push(Box::new(Qwen3SsmLayer::new(
377 input_norm,
378 ssm,
379 post_attn_norm,
380 ffn,
381 Some(qkvz_nvfp4),
382 config,
383 gpu,
384 )?));
385 }
386 LayerType::SlidingAttention => {
387 unreachable!("unexpected SlidingAttention in this loader")
388 }
389 LayerType::Moe => unreachable!("Qwen3 has no standalone MoE layers"),
390 LayerType::SparseAttention => anyhow::bail!(
394 "layer {i}: SparseAttention needs a DSA indexer and per-query top-k; Qwen3 has neither"
395 ),
396 }
397
398 if (i + 1) % 12 == 0 {
399 tracing::info!("Loaded layers 0..{}", i + 1);
400 }
401 }
402
403 tracing::info!(
404 "Weight loader: {} layers ({} attention, {} SSM)",
405 layers.len(),
406 attn_idx,
407 layers.len() - attn_idx,
408 );
409
410 Ok(layers)
411 }
412
413 fn load_embedding(
414 &self,
415 store: &WeightStore,
416 _config: &ModelConfig,
417 _gpu: &dyn GpuBackend,
418 ) -> Result<DenseWeight> {
419 dense(store, "model.embed_tokens.weight")
420 }
421
422 fn load_final_norm(
423 &self,
424 store: &WeightStore,
425 _config: &ModelConfig,
426 _gpu: &dyn GpuBackend,
427 ) -> Result<DenseWeight> {
428 dense(store, "model.norm.weight")
429 }
430
431 fn load_lm_head(
432 &self,
433 store: &WeightStore,
434 _config: &ModelConfig,
435 _gpu: &dyn GpuBackend,
436 ) -> Result<DenseWeight> {
437 if store.contains("lm_head.weight") {
438 dense(store, "lm_head.weight")
439 } else {
440 dense(store, "model.embed_tokens.weight")
441 }
442 }
443
444 fn load_mtp_weights(
445 &self,
446 store: &WeightStore,
447 config: &ModelConfig,
448 gpu: &dyn GpuBackend,
449 ) -> Result<Option<MtpWeights>> {
450 if !store.contains("mtp.fc.weight") {
451 tracing::info!("No MTP weights found — speculative decoding disabled");
452 return Ok(None);
453 }
454 let variant = detect_nvfp4_variant(store, config);
455 tracing::info!("Loading MTP weights (variant={:?})...", variant);
456 let mtp = load_mtp(store, config.num_experts, gpu, variant)?;
457 tracing::info!(
458 "MTP weights loaded: fc=[2048,4096], {} experts, attn layer",
459 mtp.experts.len(),
460 );
461 Ok(Some(mtp))
462 }
463}