1use anyhow::Result;
14use metrale_cache::kv_cache::{KvCacheConfig, KvCacheDtype, PagedKvCache};
15use metrale_config::ModelConfig;
16use metrale_gpu_runtime::buffers::BufferArena;
17use metrale_gpu_runtime::gpu::GpuBackend;
18use metrale_model_weights::weights::WeightStore;
19use metrale_telemetry::prefix_cache::PrefixCache;
20
21use super::loader_for_config;
22use super::m2_setup::maybe_run_minimax_m2_moe_transpose;
23use super::{DflashBuildArgs, LoraBuildArgs};
24use crate::model::TransformerModel;
25use metrale_model_layers::layers::MtpQuantization;
26
27mod alloc_digest;
28mod kv_blocks;
29mod kv_budget;
30mod kv_sizing;
31mod kv_summary;
32mod mem_trace;
33mod mtp_modules;
34mod proposers;
35pub mod slots;
36mod weights_prep;
37
38use mem_trace::MemTrace;
39
40pub fn build_model(
41 mut config: ModelConfig,
42 mut store: WeightStore,
45 gpu: Box<dyn GpuBackend>,
46 max_batch_tokens: usize,
47 kv_block_size: usize,
48 max_seq_len: usize,
49 slots: slots::SlotPlan<'_>,
52 mtp_quant: MtpQuantization,
53 use_speculative: bool,
54 prefix_cache: Box<dyn PrefixCache>,
55 mtp_vocab_size: u32,
56 comm: Option<std::sync::Arc<dyn metrale_comm::CommBackend>>,
57 self_speculative: bool,
58 num_drafts: usize,
59 kv_dtype: KvCacheDtype,
60 gpu_memory_utilization: f64,
61 ssm_cache_slots: usize,
62 layer_dtypes: Vec<KvCacheDtype>,
63 ssm_checkpoint_interval: usize,
64 hss_cache_blocks_per_seq: Option<u32>,
67 dflash_args: Option<DflashBuildArgs<'_>>,
69 lora_args: Option<LoraBuildArgs<'_>>,
71 nllb_lang: Option<(u32, u32)>,
74 nllb_lora_dir: Option<std::path::PathBuf>,
77) -> Result<slots::BuiltModel> {
78 #[cfg(feature = "cuda")]
82 if matches!(config.model_type.as_str(), "m2m_100" | "nllb") {
83 let (src, tgt) = nllb_lang.ok_or_else(|| {
84 anyhow::anyhow!(
85 "NLLB serving requires --src-lang and --tgt-lang (translation language pair)"
86 )
87 })?;
88 let lang = crate::model::nllb::NllbLang {
89 src_lang_id: src,
90 tgt_lang_id: tgt,
91 decoder_start_id: config.eos_token_id,
92 eos_id: config.eos_token_id,
93 pad_id: 1,
94 };
95 let nllb_slots = match slots.request {
96 slots::SlotRequest::Count(n) => n,
97 slots::SlotRequest::Auto => anyhow::bail!(
98 "--max-batch-size auto balances recurrent-state slots against the KV pool; \
99 an encoder-decoder model has neither. Pass a count."
100 ),
101 };
102 let model = crate::model::nllb::NllbGpuModel::new(
103 &config,
104 &store,
105 gpu,
106 lang,
107 max_seq_len,
108 nllb_slots,
109 nllb_lora_dir.as_deref(),
110 )?;
111 return Ok(slots::BuiltModel {
112 max_batch_size: nllb_slots,
113 model: Box::new(model),
114 drafter_weights_sha256: None,
115 });
116 }
117 #[cfg(not(feature = "cuda"))]
118 let _ = (nllb_lang, nllb_lora_dir);
119
120 let loader = loader_for_config(&config)?;
122
123 let build_entry_free = gpu.free_memory().ok();
127
128 let lora_weights: Option<metrale_model_layers::lora::LoraWeights> =
133 if let Some(ref la) = lora_args {
134 config.adapter_max_rank = la.max_lora_rank;
135 loader.load_lora_adapters(
136 &la.adapters,
137 &config,
138 gpu.as_ref(),
139 la.max_loras,
140 la.max_lora_rank,
141 )?
142 } else {
143 None
144 };
145
146 weights_prep::apply_dflash_capture_config(&mut config, &dflash_args);
150
151 let attn_layer_dtypes: Vec<KvCacheDtype> = if layer_dtypes.is_empty() {
153 vec![kv_dtype; config.num_attention_layers()]
154 } else {
155 layer_dtypes.clone()
156 };
157
158 config.kv_layer_dims = loader.kv_layer_dims(&config);
162
163 let mut mem = MemTrace::new(gpu.as_ref());
167 let free_before_layers = gpu.free_memory().unwrap_or(0);
168 let mut layers = loader.load_layers(&store, &config, gpu.as_ref(), &attn_layer_dtypes)?;
169 let free_after_layers = gpu.free_memory().unwrap_or(0);
170 tracing::info!(
171 "Layer construction: {:.2} GB consumed ({:.2} GB free -> {:.2} GB free) \
172 across {} layers, {:.1} MB/layer average",
173 (free_before_layers.saturating_sub(free_after_layers)) as f64 / 1e9,
174 free_before_layers as f64 / 1e9,
175 free_after_layers as f64 / 1e9,
176 config.num_hidden_layers,
177 (free_before_layers.saturating_sub(free_after_layers)) as f64
178 / 1e6
179 / config.num_hidden_layers.max(1) as f64,
180 );
181 mem.mark("load_layers");
182 let embed = loader.load_embedding(&store, &config, gpu.as_ref())?;
183 mem.mark("load_embedding");
184 let ngram_embed =
187 loader.load_ngram_embedding(&store, &config, gpu.as_ref(), max_batch_tokens)?;
188 let final_norm = loader.load_final_norm(&store, &config, gpu.as_ref())?;
189 mem.mark("load_final_norm");
190 let lm_head = loader.load_lm_head(&store, &config, gpu.as_ref())?;
191 mem.mark("load_lm_head");
192 let mtp_weights = if use_speculative {
197 loader.load_mtp_weights_multi(&store, &config, gpu.as_ref())?
198 } else {
199 if metrale_model_weights::mtp_layout::detect_in_store(&store, &config).is_some() {
200 tracing::info!(
201 "the checkpoint has MTP weights, but --speculative is not set: no MTP head is \
202 built"
203 );
204 }
205 Vec::new()
206 };
207 mem.mark("load_mtp_weights_multi");
208
209 let glm_mtp_module =
215 mtp_modules::load_glm_mtp_module(&store, &config, gpu.as_ref(), use_speculative);
216 let glm_mtp_embed = embed;
217 let glm_mtp_lm_head = lm_head;
218
219 let v4_mtp_module = mtp_modules::load_v4_mtp_module(
220 &store,
221 &config,
222 gpu.as_ref(),
223 &attn_layer_dtypes,
224 use_speculative,
225 );
226
227 if use_speculative
232 && mtp_weights.is_empty()
233 && glm_mtp_module.is_none()
234 && v4_mtp_module.is_none()
235 {
236 mtp_modules::warn_unbound_mtp_head(&store, &config);
237 }
238 mem.mark("mtp modules (glm/v4)");
239 let vision_encoder = loader.load_vision_encoder(&store, &config, gpu.as_ref())?;
240 mem.mark("load_vision_encoder");
241
242 if vision_encoder.is_none() {
247 weights_prep::release_unbound_vision_tower(&mut store, gpu.as_ref(), &config)?;
248 }
249 mem.mark("vision reclaim");
250
251 let effective_mtp_quant =
255 mtp_modules::effective_mtp_quantization(&mtp_weights, &config, &store, mtp_quant)?;
256
257 let (lm_head_nvfp4, lm_head_fp8, mtp_lm_head_nvfp4) = super::lm_head_setup::setup_lm_heads(
260 &store,
261 &lm_head,
262 &config,
263 gpu.as_ref(),
264 use_speculative,
265 !mtp_weights.is_empty(),
266 )?;
267 let drafter_weights_sha256 = store.drafter_weights_sha256(gpu.as_ref())?;
270
271 let lm_head_nvfp4_t =
274 crate::model::impl_a1::build_lm_head_nvfp4_t(&lm_head_nvfp4, &config, gpu.as_ref())?;
275
276 let v4_mtp_embed = embed;
280 let v4_mtp_lm_head = lm_head;
281
282 maybe_run_minimax_m2_moe_transpose(&config, gpu.as_ref(), &mut layers)?;
285
286 loader.prune_after_load(&mut store, &config, gpu.as_ref())?;
290 mem.mark("prune_after_load");
291 tracing::info!(
292 "WeightStore after prune: {} tensors, {:.3} GiB still resident",
293 store.len(),
294 store.resident_bytes() as f64 / (1024.0 * 1024.0 * 1024.0),
295 );
296
297 let buffers = BufferArena::new(
300 &config,
301 max_batch_tokens,
302 max_seq_len,
303 kv_block_size,
304 slots.request.ceiling(),
305 gpu.as_ref(),
306 )?;
307
308 let (kv_num_heads, kv_head_dim) = if config.kv_lora_rank > 0 {
312 let mla_cache_dim = config.kv_lora_rank + config.qk_rope_head_dim;
313 tracing::info!(
314 "MLA absorbed KV cache: 1 head × {} dims ({}+{}) per token (vs {} heads × {})",
315 mla_cache_dim,
316 config.kv_lora_rank,
317 config.qk_rope_head_dim,
318 config.num_key_value_heads,
319 config.head_dim,
320 );
321 (1, mla_cache_dim)
322 } else {
323 (config.num_key_value_heads, config.head_dim)
324 };
325 let kv_config = KvCacheConfig {
326 block_size: kv_block_size,
327 num_kv_heads: kv_num_heads,
328 head_dim: kv_head_dim,
329 num_layers: config.num_attention_layers(),
330 dtype: kv_dtype,
331 layer_dtypes: layer_dtypes.clone(),
332 layer_dims: config.kv_layer_dims.clone(),
333 cache_blocks_per_seq: hss_cache_blocks_per_seq,
334 };
335
336 if hss_cache_blocks_per_seq.is_some() {
337 kv_summary::log_hss_kv_summary(&kv_config);
338 }
339 let gib = |b: usize| b as f64 / (1024.0 * 1024.0 * 1024.0);
345 let dflash_reserve: usize =
351 kv_budget::dflash_reserve_bytes(&dflash_args, &config, max_seq_len, gib);
352 let sized = kv_sizing::size_kv(
353 &kv_sizing::KvInputs {
354 gpu: gpu.as_ref(),
355 store: &store,
356 build_entry_free,
357 config: &config,
358 kv_config: &kv_config,
359 prefix_cache: prefix_cache.as_ref(),
360 dflash_reserve,
361 use_speculative,
362 mtp_weights: &mtp_weights,
363 effective_mtp_quant,
364 max_seq_len,
365 kv_block_size,
366 hss_cache_blocks_per_seq,
367 gpu_memory_utilization,
368 },
369 &slots,
370 )?;
371 let (max_batch_size, num_kv_blocks) = (sized.slots, sized.num_kv_blocks);
372 let total_mem = gpu.total_memory()?;
373 let total_budget = (total_mem as f64 * gpu_memory_utilization) as usize;
374 let kv_cache = PagedKvCache::new(kv_config, num_kv_blocks, gpu.as_ref())?;
375
376 let target_embed_for_dflash = embed.weight;
379 let target_lm_head_for_dflash = lm_head.weight;
380 let target_lm_head_nvfp4_for_dflash = lm_head_nvfp4;
382 let target_hidden_for_dflash = config.hidden_size;
383 let target_lm_head_native_fp8_for_dflash = if dflash_args.is_some() {
386 super::lm_head_setup::native_fp8_lm_head_share(&store, &config, gpu.as_ref())?
387 } else {
388 None
389 };
390
391 let mut model = TransformerModel::new(
392 config,
393 embed,
394 final_norm,
395 lm_head,
396 lm_head_nvfp4,
397 lm_head_fp8,
398 mtp_lm_head_nvfp4,
399 lm_head_nvfp4_t,
400 layers,
401 buffers,
402 kv_cache,
403 mtp_weights,
404 gpu,
405 max_seq_len,
406 max_batch_size,
407 effective_mtp_quant,
408 use_speculative,
409 prefix_cache,
410 mtp_vocab_size,
411 comm,
412 self_speculative,
413 num_drafts,
414 vision_encoder,
415 ssm_cache_slots,
416 ssm_checkpoint_interval,
417 )?;
418
419 if store
423 .get("lm_head.weight")
424 .is_ok_and(|w| w.dtype == metrale_model_weights::weights::WeightDtype::Q6K)
425 {
426 model.set_lm_head_q6k(lm_head.weight)?;
427 }
428 model.install_declared_lm_head_w8a8()?;
430 model.install_declared_lm_head_w4a16_rows()?;
432
433 proposers::install_v4_mtp_proposer(
437 &mut model,
438 v4_mtp_module,
439 v4_mtp_embed,
440 v4_mtp_lm_head,
441 mtp_vocab_size,
442 max_seq_len,
443 );
444
445 proposers::install_glm_mtp_proposer(
448 &mut model,
449 glm_mtp_module,
450 glm_mtp_embed,
451 glm_mtp_lm_head,
452 max_seq_len,
453 );
454
455 proposers::install_dflash_drafter(
458 &mut model,
459 dflash_args,
460 target_embed_for_dflash,
461 target_lm_head_for_dflash,
462 target_lm_head_nvfp4_for_dflash,
463 target_lm_head_native_fp8_for_dflash,
464 target_hidden_for_dflash,
465 max_seq_len,
466 max_batch_size,
467 )?;
468
469 if let Some(ngram) = ngram_embed {
472 model.set_ngram_embedding(ngram);
473 }
474 model.set_lora_weights(lora_weights)?;
475
476 model.adopt_weight_store(store);
479
480 proposers::log_alloc_report(&model, total_budget, gpu_memory_utilization, total_mem, gib);
481 Ok(slots::BuiltModel {
482 model: Box::new(model),
483 max_batch_size,
484 drafter_weights_sha256,
485 })
486}