metrale_model_engine/model/
impl_b3_accessors.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Post-construction wiring for [`TransformerModel`]: borrow the GPU and
4//! config, install a proposer or an n-gram embedding, and the MLA prefix-skip check.
5//!
6//! Owner: model-engine.
7//! Invariants:
8//! - `set_dflash_proposer` never grows the MTP prefill capture buffer. If shrinking it
9//!   fails, the buffer is left NULL with capacity 0.
10
11use metrale_config::ModelConfig;
12use metrale_gpu_runtime::gpu::GpuBackend;
13
14use super::types::TransformerModel;
15use metrale_model_layers::speculative::DraftProposer;
16
17impl TransformerModel {
18    /// 2026-09-25: Borrow the GPU backend. The factory uses it to build the
19    /// proposers installed after construction and to read allocation reports.
20    pub fn gpu_backend(&self) -> &dyn GpuBackend {
21        self.gpu.as_ref()
22    }
23
24    /// 2026-09-25: Borrow the model config, for building the proposers that
25    /// are installed after construction.
26    pub fn config_ref(&self) -> &ModelConfig {
27        &self.config
28    }
29
30    /// 2026-09-25: Install a proposer built after construction (the
31    /// DeepSeek-V4 or GLM-5.3 MTP head, or a DFlash drafter), replacing any
32    /// MTP proposer `TransformerModel::new` built. It also shrinks the MTP
33    /// prefill capture buffer to the rows this proposer can use.
34    ///
35    /// It does not check flag combinations; clap rejects `--dflash` with
36    /// `--speculative`.
37    pub fn set_dflash_proposer(&mut self, proposer: std::sync::Arc<dyn DraftProposer>) {
38        if self.proposer.is_some() {
39            tracing::info!("DFlash: replacing existing MTP proposer with BlockDiffusionDraftHead");
40        }
41        // 2026-09-25: `new()` sized `mtp_prefill_hidden` before this proposer
42        // existed. Ask the proposer how many rows it can be handed
43        // (`DraftProposer::prefill_hidden_rows`; the default keeps the size) and
44        // shrink to that; never grow. The old buffer is freed before the smaller
45        // one is allocated, so the two are never held at once.
46        let rows = proposer.prefill_hidden_rows(self.mtp_prefill_capacity);
47        if !self.mtp_prefill_hidden.is_null() && rows < self.mtp_prefill_capacity {
48            let was = self.mtp_prefill_capacity;
49            let bytes = rows * self.config.hidden_size * 2;
50            let old = std::mem::replace(
51                &mut self.mtp_prefill_hidden,
52                metrale_gpu_runtime::gpu::DevicePtr::NULL,
53            );
54            self.mtp_prefill_capacity = 0;
55            match self.gpu.free(old).and_then(|_| self.gpu.alloc(bytes)) {
56                Ok(smaller) => {
57                    self.mtp_prefill_hidden = smaller;
58                    self.mtp_prefill_capacity = rows;
59                    tracing::info!(
60                        "MTP drafter context: capture buffer rightsized {was} -> {rows} rows \
61                         ({:.0} -> {:.0} MB) — the proposer cannot be handed a position past \
62                         {rows} (A59)",
63                        (was * self.config.hidden_size * 2) as f64 / 1e6,
64                        bytes as f64 / 1e6,
65                    );
66                }
67                // 2026-09-25: NULL with capacity 0 is the feature's off state: the
68                // capture epilogue and the propose site both check it, so drafter
69                // prefill is disabled and the serve keeps running.
70                Err(e) => tracing::warn!(
71                    "MTP drafter context: rightsizing the capture buffer failed ({e:#}) — \
72                     drafter prefill and carry are DISABLED for this serve"
73                ),
74            }
75        }
76        self.proposer = Some(proposer);
77    }
78
79    /// 2026-09-25: Install the fused n-gram input embedding (only the LongCat
80    /// loader builds one). Once set, `embed_ctx` embeds through it and the
81    /// bare-token `embed` refuses to run.
82    pub fn set_ngram_embedding(
83        &mut self,
84        ngram: metrale_model_layers::layers::ngram_embed::NgramEmbedding,
85    ) {
86        tracing::info!("set_ngram_embedding: installed on the served model");
87        self.ngram_embed = Some(std::sync::Mutex::new(ngram));
88    }
89
90    /// 2026-09-25: True when this model fuses n-gram lookups into its input embedding.
91    pub fn has_ngram_embedding(&self) -> bool {
92        self.ngram_embed.is_some()
93    }
94
95    /// 2026-09-25: True when prefill must not skip a cached prefix because a
96    /// layer's local MLA prefill (`uses_local_mla_prefill`) attends only over
97    /// the tokens it processes: the skipped prefix would be absent from
98    /// attention. The prefix lookup then uses an empty match and prefill
99    /// recomputes the whole prompt. `METRALE_MLA_PREFIX_SKIP=1` makes it
100    /// return false.
101    pub(crate) fn mla_prefill_needs_full_recompute(&self) -> bool {
102        if std::env::var("METRALE_MLA_PREFIX_SKIP").as_deref() == Ok("1") {
103            return false;
104        }
105        self.layers.iter().any(|l| l.uses_local_mla_prefill())
106    }
107}