metrale_model_engine/model/
impl_a1.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `TransformerModel::new`: resolves the model-level kernels and
4//! levers, sizes the SSM, snapshot, speculative and capture buffers, and builds
5//! the model around the loaded layers.
6//!
7//! Owner: model-engine.
8//! Invariants:
9//! - The constructed model's `mtp_prefill_capacity` is the row count allocated
10//!   for `mtp_prefill_hidden`, and 0 when that buffer is NULL.
11
12#![allow(unused_imports, dead_code)]
13
14use parking_lot::Mutex;
15use std::collections::HashMap;
16use std::sync::Arc;
17
18use anyhow::{Result, bail};
19use metrale_cache::kv_cache::PagedKvCache;
20use metrale_config::{LayerType, ModelConfig};
21use metrale_gpu_runtime::buffers::BufferArena;
22use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, GraphHandle, KernelHandle};
23
24use super::block_mgmt::{
25    apply_evicted_blocks, ensure_blocks_through_decode, ensure_blocks_through_prefill,
26    extract_layer_refs, reuse_prefix_match_disk_ids,
27};
28use super::ssm_pool::SsmStatePool;
29use super::ssm_snapshot::SsmSnapshotPool;
30use super::types::{PinnedMetaStaging, TransformerModel};
31use crate::traits::{ChunkedPrefillPageMetadata, Model, SequenceState};
32use metrale_model_layers::layer::{
33    AttnMetadataDev, ForwardContext, GdnPrefillBuffers, LayerState, SsmLayerState, TransformerLayer,
34};
35use metrale_model_layers::layers::ops;
36use metrale_model_layers::speculative::DraftProposer;
37use metrale_model_layers::weight_map::{DenseWeight, MtpWeights, QuantizedWeight};
38
39mod comm_setup;
40mod kernels;
41mod spec_buffers;
42mod ssm_setup;
43
44pub(crate) use spec_buffers::build_lm_head_nvfp4_t;
45
46use kernels::{AuxKernels, ModelKernels};
47
48impl TransformerModel {
49    pub fn new(
50        config: ModelConfig,
51        embed_tokens: DenseWeight,
52        final_norm: DenseWeight,
53        lm_head_weight: DenseWeight,
54        lm_head_nvfp4: Option<QuantizedWeight>,
55        // 2026-09-25: FP8 LM head, `Some` only under `--lm-head-dtype fp8`, and
56        // then `lm_head_nvfp4` is `None`.
57        lm_head_fp8: Option<metrale_model_layers::weight_map::Fp8DenseWeight>,
58        // 2026-09-25: NVFP4 head used only by the MTP draft proposer. The
59        // factory builds it when the main head is not NVFP4 and speculative
60        // decoding has MTP weights; otherwise it is `None` and the proposer uses
61        // `lm_head_nvfp4` (see `draft_lm_head_nvfp4`).
62        mtp_lm_head_nvfp4: Option<QuantizedWeight>,
63        // 2026-10-01: The padded transposed twin of `lm_head_nvfp4` and its row stride
64        // (`build_lm_head_nvfp4_t`), built by the factory before the KV pool is sized.
65        lm_head_nvfp4_t: Option<(QuantizedWeight, u32)>,
66        layers: Vec<Box<dyn TransformerLayer>>,
67        buffers: BufferArena,
68        kv_cache: PagedKvCache,
69        mtp_weights: Vec<MtpWeights>,
70        gpu: Box<dyn GpuBackend>,
71        max_seq_len: usize,
72        max_batch_size: usize,
73        mtp_quant: metrale_model_layers::layers::MtpQuantization,
74        use_speculative: bool,
75        prefix_cache: Box<dyn metrale_telemetry::prefix_cache::PrefixCache>,
76        mtp_vocab_size: u32,
77        comm: Option<std::sync::Arc<dyn metrale_comm::CommBackend>>,
78        self_speculative: bool,
79        num_drafts: usize,
80        vision_encoder: Option<metrale_model_layers::layers::VisionTower>,
81        ssm_cache_slots: usize,
82        ssm_checkpoint_interval: usize,
83    ) -> Result<Self> {
84        // 2026-09-25: `rms_norm_kernel` is used only for `final_norm`, in
85        // `final_norm_apply`.
86        let rms_norm_kernel = if metrale_model_layers::ships_vanilla_norm_weights(&config) {
87            gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?
88        } else {
89            gpu.kernel("norm", "rms_norm")?
90        };
91        let ModelKernels {
92            dense_gemv_kernel,
93            dense_gemv_fp32out_kernel,
94            w4a16_gemv_kernel,
95            w4a16_gemv_logits_kernel,
96            w4a16_gemm_t_kernel,
97            w4a16_gemm_t_bf16_kernel,
98            w4a16_gemm_kernel,
99            w4a16_gemv_batch2_kernel,
100            w4a16_batchm,
101            w4a16_gemv_batch16_kernel,
102            dense_gemv_fp8w_kernel,
103            dense_gemv_fp8w_batch2_kernel,
104            dense_gemm_kernel,
105            dense_gemv_batchm_kernel,
106            lm_head_m16_tc_kernel,
107            lm_head_m16_tc_n64_kernel,
108            argmax_kernel,
109            argmax_batch_kernel,
110            argmax_logits_kernel,
111            batched_embed_kernel,
112            fill_slots_kernel,
113            argmax_feed_kernel,
114            feed_resolve_kernel,
115        } = kernels::resolve_model_kernels(&config, gpu.as_ref())?;
116        let profile = config.profile;
117        let profile_first = std::env::var("METRALE_PROFILE_FIRST").is_ok();
118
119        // 2026-09-25: A fresh `ModelLevers::from_env()`, not the process-cached
120        // `ModelLevers::get()`, so a second model built in the same process
121        // resolves its own levers. `max_decode_seqs` pins the split-K attention
122        // split count to the configured max batch, so a sequence's attention
123        // reduction does not depend on how many others share its batch.
124        let mut levers = ops::ModelLevers::from_env();
125        levers.max_decode_seqs = (max_batch_size as u32).max(1);
126
127        tracing::info!(
128            "TransformerModel: {} layers, vocab={}, hidden={}{}{}",
129            layers.len(),
130            config.vocab_size,
131            config.hidden_size,
132            if profile { " [PROFILE MODE]" } else { "" },
133            if profile_first {
134                " [PROFILE_FIRST]"
135            } else {
136                ""
137            },
138        );
139
140        let (dflash_kgamma, draft_lm_head_nvfp4, has_mtp, mtp_quant_fwd, num_intermediates) =
141            ssm_setup::size_rollback_pools(
142                &config,
143                mtp_lm_head_nvfp4,
144                lm_head_nvfp4,
145                self_speculative,
146                use_speculative,
147                &mtp_weights,
148                mtp_quant,
149                num_drafts,
150            );
151        let ssm_pool = std::sync::Arc::new(SsmStatePool::new(
152            &config,
153            max_batch_size,
154            has_mtp,
155            num_intermediates,
156            num_drafts,
157            // 2026-09-25: h pools sized at 2 bytes per element
158            // (`--ssm-h-dtype f16-pool`).
159            metrale_model_layers::layers::qwen3_ssm::ssm_h_f16_pool_enabled(),
160            // 2026-09-25: `--ssm-rollback-mode`; `Snapshot` unless the server
161            // published another mode.
162            metrale_model_layers::ssm_reserve::ssm_rollback_mode(),
163            gpu.as_ref(),
164        )?);
165
166        let (ssm_snapshots, ssm_tier_store) = ssm_setup::build_ssm_snapshots(
167            &config,
168            &ssm_pool,
169            use_speculative,
170            ssm_cache_slots,
171            prefix_cache.as_ref(),
172            max_batch_size,
173            ssm_checkpoint_interval,
174            &kv_cache,
175            gpu.as_ref(),
176        )?;
177
178        // 2026-09-25: Fixed per-sequence stride of the attention block-table
179        // metadata, so captured CUDA graphs stay valid.
180        let max_blocks_per_seq = (max_seq_len / kv_cache.block_size() + 1) as u32;
181
182        // 2026-09-25: Permanent KV block for padding rows and for block-table
183        // default fill, zeroed so a read of it yields zeros rather than stale
184        // or uninitialized bytes.
185        let mut kv_cache = kv_cache;
186        let dummy_kv_block = kv_cache.alloc_block()?;
187        kv_cache.zero_block(dummy_kv_block, gpu.as_ref(), gpu.default_stream())?;
188        gpu.synchronize(gpu.default_stream())?;
189
190        // 2026-09-25: `lm_head_nvfp4_t`: the transposed lm_head twin with its row stride padded
191        // to a multiple of 128, so the tile GEMM's 16-byte `cp.async` B loads stay aligned
192        // whatever the vocab size. Built unless `METRALE_NO_LMHEAD_TGEMM=1`.
193        // 2026-09-25: The drafter may use the twin only when its head is the
194        // main head (`mtp_lm_head_nvfp4` absent): the twin is a transpose of
195        // `lm_head_nvfp4`, so a dedicated draft head would score drafts against
196        // the wrong weight. The shared case copies the handle, not the memory.
197        let draft_lm_head_nvfp4_t = if mtp_lm_head_nvfp4.is_none() {
198            lm_head_nvfp4_t
199        } else {
200            None
201        };
202        let proposer: Option<Arc<dyn DraftProposer>> = super::impl_a1_init::build_mtp_proposer(
203            use_speculative,
204            mtp_weights,
205            embed_tokens,
206            final_norm,
207            draft_lm_head_nvfp4,
208            draft_lm_head_nvfp4_t,
209            &config,
210            gpu.as_ref(),
211            mtp_quant,
212            mtp_vocab_size,
213            max_seq_len,
214            kv_cache.num_blocks(),
215            &levers,
216        )?;
217
218        ssm_setup::log_self_speculative(&config, self_speculative);
219
220        let (
221            mtp_hidden_save,
222            verify_hidden_stash,
223            verify_catchup_stash,
224            verify_wy_tables,
225            gdn_woa_na_tab,
226            mtp_catchup_ring,
227        ) = spec_buffers::alloc_verify_buffers(&config, &proposer, &levers, gpu.as_ref())?;
228
229        let (capture_rows, mtp_prefill_hidden) = spec_buffers::alloc_prefill_capture(
230            &config,
231            &proposer,
232            max_seq_len,
233            dflash_kgamma,
234            has_mtp,
235            mtp_quant_fwd,
236            &levers,
237            mtp_quant,
238            gpu.as_ref(),
239        )?;
240
241        let (dflash_capture_layers, dflash_hidden_save_rows, dflash_hidden_save) =
242            spec_buffers::alloc_dflash_capture(
243                &config,
244                dflash_kgamma,
245                max_batch_size,
246                gpu.as_ref(),
247            )?;
248
249        // 2026-09-25: One u32 for the EP command broadcast.
250        let ep_cmd_buf = gpu.alloc(4)?;
251
252        // 2026-09-25: Fixed-address buffer (CUDA-graph safe) for the batched
253        // decode MoE per-row adapter map, 4 bytes per batch row, uploaded each
254        // decode step. Allocated unconditionally because LoRA weights arrive
255        // after construction (`set_lora_weights`).
256        let moe_row_adapter_buf = gpu.alloc(max_batch_size.max(1) * 4)?;
257
258        // 2026-09-25: Secondary stream and its event, used by the asynchronous
259        // checkpoint copies (`trait_impl/async_chkpt.rs`).
260        let secondary_stream = gpu.create_stream()?;
261        let secondary_event = gpu.create_event()?;
262        // 2026-09-25: Orders SSM snapshot saves (default stream) before a warm
263        // Marconi restore (prefill stream); see the `snapshot_event` field doc.
264        let snapshot_event = gpu.create_event()?;
265
266        // 2026-09-25: Levers that shape the collective schedule are read per rank
267        // from the environment, so ranks that disagree can hang or reduce the
268        // wrong extent. `rank_agree::assert_ranks_agree` checks them before the
269        // first token.
270        if let Some(ref comm) = comm {
271            comm_setup::assert_rank_levers_agree(gpu.as_ref(), comm)?;
272        }
273
274        // 2026-09-25: At world size 2, register the reduce targets
275        // (`moe_output`, `norm_output`, and `logits` for the vocab-parallel
276        // lm_head) with the comm backend and give it the `bf16_add_inplace`
277        // kernel for its send/recv path. Each step is best-effort: a failure is
278        // logged and construction continues.
279        if let Some(ref comm) = comm
280            && comm.world_size() == 2
281        {
282            comm_setup::register_reduce_buffers(comm, &buffers, gpu.as_ref());
283        }
284
285        // 2026-09-25: Device token feed buffers: one u32 per decode-metadata row
286        // for cells, ids and sources, two for masks. Allocated only when both
287        // feed kernels are loaded.
288        let (feed_rows, feed_cells, feed_ids, feed_sources, feed_masks) =
289            kernels::alloc_feed_buffers(
290                &buffers,
291                argmax_feed_kernel,
292                feed_resolve_kernel,
293                gpu.as_ref(),
294            )?;
295        let pinned_bytes = buffers.sizes().scratch.max(64 * 1024);
296        let pinned_ptr = gpu.alloc_host_pinned(pinned_bytes)?;
297        tracing::info!("Pinned metadata staging: {} KB", pinned_bytes / 1024);
298        let max_batch_tokens = buffers.max_batch_tokens();
299        let pinned_staging = std::cell::UnsafeCell::new(PinnedMetaStaging {
300            ptr: pinned_ptr,
301            bytes: pinned_bytes,
302            positions: Vec::with_capacity(max_batch_tokens),
303            positions_h: Vec::with_capacity(max_batch_tokens),
304            positions_w: Vec::with_capacity(max_batch_tokens),
305            slots: Vec::with_capacity(max_batch_tokens),
306        });
307
308        let AuxKernels {
309            ssm_norm_k,
310            ssm_norm_f16_k,
311            ssm_h_f32_to_f16_k,
312            ssm_h_f16_to_f32_k,
313            logit_softcap_kernel,
314            logit_softcap_fp32_kernel,
315            use_fp32_logits,
316            logits_fp32_buf,
317            embed_scale_kernel,
318            ssm_norm_ptrs,
319        } = kernels::resolve_aux_kernels(&config, &ssm_pool, gpu.as_ref())?;
320
321        // 2026-09-25: Sized for `min(max_batch_tokens, max_seq_len)` tokens;
322        // `prefill_twophase` falls back to `prefill_chunk` for a longer prompt.
323        let (gdn_qkv, gdn_gate_beta, gdn_out, gdn_z, gdn_buf_len) =
324            super::impl_a1_init::build_gdn_prefill_buffers(
325                &config,
326                max_batch_tokens,
327                max_seq_len,
328                gpu.as_ref(),
329            )?;
330
331        // 2026-09-25: FP8 KV calibration observes only the FP8 cache write
332        // (`write_kv_cache_fp8.rs`), so `fp8_kv_calibration_tokens` suppresses
333        // CUDA graphs only when the cache is FP8.
334        let has_fp8_calibration = config.fp8_kv_calibration_tokens > 0
335            && kv_cache.dtype() == metrale_cache::kv_cache::KvCacheDtype::Fp8;
336        // 2026-09-25: Resolve the token-overlay kernels before `gpu` moves into
337        // `Self`.
338        let overlay_kernels =
339            metrale_model_layers::layers::ops::token_overlay::OverlayKernels::new(gpu.as_ref());
340        Ok(Self {
341            // 2026-09-25: Installed by the factory after construction
342            // (`adopt_weight_store`).
343            weight_store: None,
344            config,
345            dispatch: metrale_model_layers::layers::ops::GemmDispatch::from_env(),
346            derived: metrale_model_layers::layers::ops::DerivedWeights::new(),
347            levers,
348            stats: ops::ModelStats::new(),
349            #[cfg(feature = "cuda")]
350            innerq: kernels::start_innerq(gpu.as_ref()),
351            embed_tokens,
352            ngram_embed: None,
353            final_norm,
354            lm_head_weight,
355            lm_head_nvfp4,
356            lm_head_nvfp4_t,
357            lm_head_fp8,
358            lm_head_q6k: None,
359            lm_head_rows: super::lm_head_fp8_rows::LmHeadRows::resolve(gpu.as_ref()),
360            // 2026-09-25: Computed before `layers` moves into the struct, since
361            // the veto is a fold over the layers.
362            decode_graph_veto: layers.iter().any(|l| l.decode_graph_unsupported()),
363            layers,
364            buffers,
365            lora: None,
366            lora_rotatable: false,
367            kv_cache: Mutex::new(kv_cache),
368            gpu,
369            rms_norm_kernel,
370            dense_gemv_kernel,
371            dense_gemv_fp32out_kernel,
372            w4a16_gemv_kernel,
373            w4a16_gemv_logits_kernel,
374            w4a16_gemm_t_kernel,
375            w4a16_gemm_t_bf16_kernel,
376            w4a16_gemm_kernel,
377            w4a16_gemv_batch2_kernel,
378            w4a16_batchm,
379            w4a16_gemv_batch16_kernel,
380            dense_gemv_fp8w_kernel,
381            dense_gemv_fp8w_batch2_kernel,
382            dense_gemm_kernel,
383            dense_gemv_batchm_kernel,
384            lm_head_m16_tc_kernel,
385            lm_head_m16_tc_n64_kernel,
386            argmax_kernel,
387            argmax_batch_kernel,
388            argmax_logits_kernel,
389            batched_embed_kernel,
390            fill_slots_kernel,
391            argmax_feed_kernel,
392            feed_resolve_kernel,
393            feed_cells,
394            feed_ids,
395            feed_sources,
396            feed_masks,
397            feed_rows,
398            inflight_fed_graph_keys: Mutex::new(std::collections::VecDeque::new()),
399            decode_graph: Mutex::new(std::collections::HashMap::new()),
400            batch_decode_graphs: Mutex::new((HashMap::new(), 0)),
401            suppress_graphs: std::sync::atomic::AtomicBool::new(
402                has_fp8_calibration
403                    || std::env::var("METRALE_DIAG_GEMMA4").is_ok_and(|v| v == "1" || v == "true")
404                    // 2026-09-25: Eager decode, so `METRALE_DEBUG_SYNC_KERNELS` can
405                    // synchronize after each launch and report an async fault at
406                    // the kernel that caused it.
407                    || std::env::var("METRALE_DEBUG_NO_GRAPH").as_deref() == Ok("1"),
408            ),
409            ssm_pool,
410            ssm_snapshots,
411            ssm_tier_store,
412            max_blocks_per_seq,
413            dummy_kv_block,
414            profile,
415            profile_first_pending: std::sync::atomic::AtomicBool::new(profile_first),
416            proposer,
417            mtp_hidden_save,
418            verify_hidden_stash,
419            verify_catchup_stash,
420            mtp_catchup_ring,
421            mtp_catchup_meta: parking_lot::Mutex::new((0, 0)),
422            mtp_prefill_hidden,
423            // 2026-09-25: The rows actually allocated (both caps applied), which
424            // the capture guard in `drafter_prefill.rs` checks against.
425            mtp_prefill_capacity: if mtp_prefill_hidden.is_null() {
426                0
427            } else {
428                capture_rows
429            },
430            mtp_prefill_capture_len: std::sync::atomic::AtomicUsize::new(0),
431            mtp_prefill_capture_gen: std::sync::atomic::AtomicU64::new(0),
432            mtp_store_gen_seq: std::sync::atomic::AtomicU64::new(0),
433            mtp_carry: parking_lot::Mutex::new(None),
434            mtp_store_range: parking_lot::Mutex::new(
435                metrale_model_layers::mtp_carry::StoreRange::EMPTY,
436            ),
437            dflash_hidden_save,
438            verify_ptok_meta: std::sync::OnceLock::new(),
439            dflash_hidden_save_rows,
440            dflash_kgamma,
441            dflash_capture_layers,
442            verify2_graph: Mutex::new(std::collections::HashMap::new()),
443            verify3_graph: Mutex::new(std::collections::HashMap::new()),
444            verify4_graph: Mutex::new(std::collections::HashMap::new()),
445            verify_batched_graphs: Mutex::new((std::collections::HashMap::new(), 0)),
446            verify_wy_tables,
447            gdn_woa_na_tab,
448            gdn_woa_folded_slots: parking_lot::Mutex::new(Vec::new()),
449            gdn_woa_eligible: std::sync::atomic::AtomicBool::new(false),
450            gdn_woa_bound: parking_lot::Mutex::new((DevicePtr::NULL, DevicePtr::NULL, 0)),
451            gdn_carry: Default::default(),
452            // 2026-09-25: Nothing staged yet, so the first batched verify step
453            // uploads the tables.
454            verify_wy_cache: Mutex::new(None),
455            verify_kgamma_graph: Mutex::new(std::collections::HashMap::new()),
456            fused_graph: Mutex::new(std::collections::HashMap::new()),
457            prefix_cache,
458            secondary_stream,
459            secondary_event,
460            snapshot_event,
461            comm,
462            ep_cmd_buf,
463            ep_protocol_v2: matches!(std::env::var("METRALE_EP_PROTOCOL").as_deref(), Ok("v2")),
464            self_speculative,
465            last_mtp_hidden_idx: std::sync::atomic::AtomicUsize::new(0),
466            vision_encoder,
467            vision_embed_patches: Mutex::new(0),
468            vision_image_grids: Mutex::new(Vec::new()),
469            vision_row_base: Mutex::new(0),
470            vision_grid_base: Mutex::new(0),
471            vision_owned_images: Mutex::new(0),
472            pinned_staging,
473            ssm_checkpoint_interval,
474            ssm_state_norm_kernel: ssm_norm_k,
475            ssm_state_norm_f16_kernel: ssm_norm_f16_k,
476            ssm_h_f32_to_f16_kernel: ssm_h_f32_to_f16_k,
477            ssm_h_f16_to_f32_kernel: ssm_h_f16_to_f32_k,
478            ssm_h_f16_scratch: std::sync::OnceLock::new(),
479            ssm_norm_ptrs_buf: ssm_norm_ptrs,
480            moe_row_adapter_buf,
481            gdn_buf_qkv: gdn_qkv,
482            gdn_buf_gate_beta: gdn_gate_beta,
483            gdn_buf_out: gdn_out,
484            gdn_buf_z: gdn_z,
485            gdn_buf_max_len: gdn_buf_len,
486            logit_softcap_kernel,
487            logit_softcap_fp32_kernel,
488            use_fp32_logits,
489            logits_fp32_buf,
490            embed_scale_kernel,
491            overlays: None,
492            overlay_kernels,
493            overlay_route_slot: std::sync::atomic::AtomicI32::new(-1),
494            decode_moe_route: std::sync::atomic::AtomicI32::new(1), // 2026-09-25: `MoeLoraRoute::Fold`
495            circuit: parking_lot::RwLock::new(None),
496        })
497    }
498}