1use anyhow::{Context, Result};
11use metrale_cache::kv_cache::KvCacheDtype;
12use metrale_config::ModelConfig;
13use metrale_gpu_runtime::gpu::GpuBackend;
14use metrale_model_weights::weights::WeightStore;
15
16use super::{ModelWeightLoader, WeightFormat};
17use crate::tp_shard::{TpShardKind, load_qkvo_tp, shard_quantized_nvfp4};
18use metrale_model_layers::layer::TransformerLayer;
19use metrale_model_layers::layers::vision_encoder::{MergerLayer, ViTBlock};
20use metrale_model_layers::layers::{
21 FfnComponent, MoeLayer, Qwen3AttentionLayer, VisionEncoder, VisionTower,
22};
23use metrale_model_layers::weight_map::{
24 AttentionWeights, DenseWeight, MtpWeights, dense, detect_nvfp4_variant, load_kv_scales,
25 load_moe_no_shared, quantize_to_nvfp4, quantized_auto,
26};
27
28pub struct Qwen3VLWeightLoader;
29
30impl ModelWeightLoader for Qwen3VLWeightLoader {
31 fn supports_tp(&self) -> bool {
32 true
36 }
37
38 fn load_layers(
39 &self,
40 store: &WeightStore,
41 config: &ModelConfig,
42 gpu: &dyn GpuBackend,
43 layer_kv_dtypes: &[KvCacheDtype],
44 ) -> Result<Vec<Box<dyn TransformerLayer>>> {
45 let mut layers: Vec<Box<dyn TransformerLayer>> =
46 Vec::with_capacity(config.num_hidden_layers);
47
48 let variant = detect_nvfp4_variant(store, config);
49 let weight_format = WeightFormat::detect(store, config);
50 tracing::info!(
51 "Weight format: {:?}, NVFP4 variant: {:?}",
52 weight_format,
53 variant
54 );
55
56 let moe_prefill_copies = super::moe_prefill_copies_fit(config, gpu);
60 let absmax_k = gpu.kernel("quantize_nvfp4", "nvfp4_global_absmax")?;
61 let quantize_k = gpu.kernel("quantize_nvfp4", "quantize_bf16_to_nvfp4")?;
62 let stream = gpu.default_stream();
63 let h = config.hidden_size;
64
65 for i in 0..config.num_hidden_layers {
66 let lp = config.layer_prefix(i);
67 let input_norm = dense(store, &format!("{lp}.input_layernorm.weight"))?;
68 let post_attn_norm = dense(store, &format!("{lp}.post_attention_layernorm.weight"))?;
69
70 let moe_weights =
71 load_moe_no_shared(store, &lp, config.num_experts, gpu, config, variant)?;
72 let gate_nvfp4 = quantize_to_nvfp4(
73 &moe_weights.gate,
74 config.num_experts,
75 h,
76 gpu,
77 absmax_k,
78 quantize_k,
79 stream,
80 )?;
81 let mut moe_layer = MoeLayer::new(
82 moe_weights,
83 config.num_experts,
84 Some(gate_nvfp4),
85 gpu,
86 config,
87 )?;
88 if moe_prefill_copies {
89 moe_layer.transpose_for_prefill(gpu, config)?;
90 moe_layer.predequant_for_prefill(gpu, config, stream)?;
91 }
92 let ffn = FfnComponent::Moe(moe_layer);
93
94 let p = format!("{lp}.self_attn");
97 let tp_rank = config.tp_rank;
98 let tp_size = config.tp_world_size.max(1);
99 let group_size = 16usize;
100 let load_proj =
101 |name: &str,
102 full_n: usize,
103 full_k: usize,
104 kind: TpShardKind|
105 -> Result<metrale_model_layers::weight_map::QuantizedWeight> {
106 let src = quantized_auto(store, &format!("{p}.{name}"), gpu, variant)?;
107 if tp_size == 1 {
108 return Ok(src);
109 }
110 let sharded = shard_quantized_nvfp4(
111 &src, full_n, full_k, kind, tp_rank, tp_size, group_size, gpu,
112 )?;
113 gpu.free(src.weight)?;
114 gpu.free(src.weight_scale)?;
115 Ok(sharded)
116 };
117 let [q, k, v, o] = load_qkvo_tp(config, load_proj)?;
118 let dummy = DenseWeight {
119 weight: metrale_gpu_runtime::gpu::DevicePtr::NULL,
120 };
121 let (k_scale, v_scale) = load_kv_scales(store, &p, gpu)?;
122 let attn = AttentionWeights {
123 q_proj: dummy,
124 k_proj: dummy,
125 v_proj: dummy,
126 o_proj: o,
127 q_norm: dense(store, &format!("{p}.q_norm.weight"))?,
128 k_norm: dense(store, &format!("{p}.k_norm.weight"))?,
129 q_norm_full: None,
130 k_norm_full: None,
131 k_scale,
132 v_scale,
133 };
134
135 layers.push(Box::new(Qwen3AttentionLayer::new_ungated(
136 input_norm,
137 attn,
138 post_attn_norm,
139 ffn,
140 i,
141 Some(q),
142 Some(k),
143 Some(v),
144 gpu,
145 layer_kv_dtypes[i],
146 config.fp8_kv_calibration_tokens,
147 config,
148 )?));
149
150 if (i + 1) % 10 == 0 {
151 tracing::info!("Loaded layers 0..{}", i + 1);
152 }
153 }
154
155 tracing::info!(
156 "Qwen3-VL weight loader: {} layers (all attention, ungated)",
157 layers.len(),
158 );
159
160 Ok(layers)
161 }
162
163 fn load_embedding(
164 &self,
165 store: &WeightStore,
166 config: &ModelConfig,
167 _gpu: &dyn GpuBackend,
168 ) -> Result<DenseWeight> {
169 let prefix = &config.weight_prefix;
170 dense(store, &format!("{prefix}.embed_tokens.weight"))
171 }
172
173 fn load_final_norm(
174 &self,
175 store: &WeightStore,
176 config: &ModelConfig,
177 _gpu: &dyn GpuBackend,
178 ) -> Result<DenseWeight> {
179 let prefix = &config.weight_prefix;
180 dense(store, &format!("{prefix}.norm.weight"))
181 }
182
183 fn load_lm_head(
184 &self,
185 store: &WeightStore,
186 config: &ModelConfig,
187 _gpu: &dyn GpuBackend,
188 ) -> Result<DenseWeight> {
189 for pattern in &[
190 "lm_head.weight",
191 "language_model.lm_head.weight",
192 "model.lm_head.weight",
193 ] {
194 if store.contains(pattern) {
195 return dense(store, pattern);
196 }
197 }
198 let prefix = &config.weight_prefix;
199 dense(store, &format!("{prefix}.embed_tokens.weight"))
200 }
201
202 fn load_mtp_weights(
203 &self,
204 _store: &WeightStore,
205 _config: &ModelConfig,
206 _gpu: &dyn GpuBackend,
207 ) -> Result<Option<MtpWeights>> {
208 Ok(None)
209 }
210
211 fn load_vision_encoder(
212 &self,
213 store: &WeightStore,
214 config: &ModelConfig,
215 gpu: &dyn GpuBackend,
216 ) -> Result<Option<VisionTower>> {
217 let vcfg = match &config.vision {
218 Some(v) => v.clone(),
219 None => return Ok(None),
220 };
221 let vp = "model.visual";
222
223 let patch_embed_w = dense(store, &format!("{vp}.patch_embed.proj.weight"))?;
224 let patch_embed_b = dense(store, &format!("{vp}.patch_embed.proj.bias"))?;
225 let pos_embed = dense(store, &format!("{vp}.pos_embed.weight"))?;
226 let pos_embed_shape = store.get(&format!("{vp}.pos_embed.weight"))?.shape.clone();
227 let num_position_embeddings = pos_embed_shape
228 .first()
229 .copied()
230 .context("pos_embed shape missing rows")?;
231
232 let mut blocks = Vec::with_capacity(vcfg.depth);
233 for i in 0..vcfg.depth {
234 let bp = format!("{vp}.blocks.{i}");
235 blocks.push(ViTBlock {
236 norm1_w: dense(store, &format!("{bp}.norm1.weight"))?.weight,
237 norm1_b: dense(store, &format!("{bp}.norm1.bias"))?.weight,
238 qkv_w: dense(store, &format!("{bp}.attn.qkv.weight"))?.weight,
239 qkv_b: dense(store, &format!("{bp}.attn.qkv.bias"))?.weight,
240 proj_w: dense(store, &format!("{bp}.attn.proj.weight"))?.weight,
241 proj_b: dense(store, &format!("{bp}.attn.proj.bias"))?.weight,
242 norm2_w: dense(store, &format!("{bp}.norm2.weight"))?.weight,
243 norm2_b: dense(store, &format!("{bp}.norm2.bias"))?.weight,
244 fc1_w: dense(store, &format!("{bp}.mlp.linear_fc1.weight"))?.weight,
245 fc1_b: dense(store, &format!("{bp}.mlp.linear_fc1.bias"))?.weight,
246 fc2_w: dense(store, &format!("{bp}.mlp.linear_fc2.weight"))?.weight,
247 fc2_b: dense(store, &format!("{bp}.mlp.linear_fc2.bias"))?.weight,
248 });
249 }
250
251 let mut deepstack = Vec::with_capacity(vcfg.deepstack_visual_indexes.len());
252 for i in 0..vcfg.deepstack_visual_indexes.len() {
253 let mp = format!("{vp}.deepstack_merger_list.{i}");
254 deepstack.push(MergerLayer {
255 norm_w: dense(store, &format!("{mp}.norm.weight"))?.weight,
256 norm_b: dense(store, &format!("{mp}.norm.bias"))?.weight,
257 fc1_w: dense(store, &format!("{mp}.linear_fc1.weight"))?.weight,
258 fc1_b: dense(store, &format!("{mp}.linear_fc1.bias"))?.weight,
259 fc2_w: dense(store, &format!("{mp}.linear_fc2.weight"))?.weight,
260 fc2_b: dense(store, &format!("{mp}.linear_fc2.bias"))?.weight,
261 });
262 }
263
264 let mp = format!("{vp}.merger");
265 let merger = MergerLayer {
266 norm_w: dense(store, &format!("{mp}.norm.weight"))?.weight,
267 norm_b: dense(store, &format!("{mp}.norm.bias"))?.weight,
268 fc1_w: dense(store, &format!("{mp}.linear_fc1.weight"))?.weight,
269 fc1_b: dense(store, &format!("{mp}.linear_fc1.bias"))?.weight,
270 fc2_w: dense(store, &format!("{mp}.linear_fc2.weight"))?.weight,
271 fc2_b: dense(store, &format!("{mp}.linear_fc2.bias"))?.weight,
272 };
273
274 let deepstack_indexes = vcfg.deepstack_visual_indexes.clone();
275 let ve = VisionEncoder::new(
276 patch_embed_w.weight,
277 patch_embed_b.weight,
278 pos_embed.weight,
279 num_position_embeddings,
280 blocks,
281 deepstack,
282 deepstack_indexes,
283 merger,
284 vcfg.hidden_size,
285 vcfg.num_heads,
286 vcfg.spatial_merge_size,
287 vcfg.out_hidden_size,
288 vcfg.intermediate_size,
289 vcfg.patch_size,
290 vcfg.max_pixels,
291 gpu,
292 )?;
293 tracing::info!(
294 "Vision encoder loaded: depth={}, hidden={}, heads={}, deepstack={:?}",
295 vcfg.depth,
296 vcfg.hidden_size,
297 vcfg.num_heads,
298 vcfg.deepstack_visual_indexes,
299 );
300 Ok(Some(VisionTower::qwen(ve)))
301 }
302}