spark_model/model/impl_b3_accessors.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Post-construction proposer-wiring accessors for [`TransformerModel`].
4//! Split out of `impl_b3.rs` (500-LoC cap) â borrow/install hooks only.
5
6use atlas_core::config::ModelConfig;
7use spark_runtime::gpu::GpuBackend;
8
9use super::types::TransformerModel;
10use crate::speculative::DraftProposer;
11
12impl TransformerModel {
13 /// Borrow the GPU backend for post-construction wiring (e.g. installing
14 /// a DFlash proposer that needs to allocate paged KV caches against the
15 /// same GPU the target uses).
16 pub fn gpu_backend(&self) -> &dyn GpuBackend {
17 self.gpu.as_ref()
18 }
19
20 /// Borrow the model config for post-construction wiring (e.g. building the
21 /// DeepSeek-V4 MTP proposer, which needs `hidden_size` / `kv_lora_rank` /
22 /// `qk_rope_head_dim` to size its private MLA KV cache).
23 pub fn config_ref(&self) -> &ModelConfig {
24 &self.config
25 }
26
27 /// Install a DFlash drafter as the active proposer, replacing whatever
28 /// MTP proposer (if any) `TransformerModel::new` built. The target's
29 /// hidden-state capture buffer is already allocated when the config's
30 /// `dflash_capture_layers` is non-empty (factory.rs populates it before
31 /// construction), so this method only swaps the proposer slot.
32 ///
33 /// Mutually exclusive with `--speculative` MTP at the CLI level
34 /// (clap `conflicts_with`); this method does not enforce that â the
35 /// caller is expected to have validated the flag combination already.
36 pub fn set_dflash_proposer(&mut self, proposer: std::sync::Arc<dyn DraftProposer>) {
37 if self.proposer.is_some() {
38 tracing::info!("DFlash: replacing existing MTP proposer with BlockDiffusionDraftHead");
39 }
40 // ðī ANOMALIES A59. `new()` sized `mtp_prefill_hidden` at `max_seq_len` because the
41 // post-construction proposers (V4, GLM-5.3, DFlash) do not exist yet when it runs â
42 // they need the model's owned GPU backend and its shared embed/lm_head. Now that one
43 // is installed, ask it how many rows it can actually be handed and give back the rest.
44 //
45 // Keyed to the trait, not to a model name: a proposer that can follow the target to
46 // the end of the served context returns `max_seq_len` (the default) and nothing
47 // happens. GLM-5.3's drafter is a DSA block capped at `max_dsa_context`, so at
48 // `--max-seq-len 524288` this returns 4.0 GiB of unreachable capture buffer that was
49 // covered by no reserve at all (see `Glm5NextMtpHead::new`).
50 //
51 // Safe here and nowhere later: construction time, no sequence exists, so no capture is
52 // in flight and no `mtp_prefill_capture_len` is live. Shrink only â a proposer must
53 // never be able to GROW a buffer the capture epilogue already bounds-checks against.
54 // ðŠĪ FREE the old buffer BEFORE allocating the small one. The obvious alloc-then-free
55 // ordering holds both at once, and the peak it creates â 4.3 GB â is exactly the
56 // pressure this is here to remove, on a box that has ~3 GB free at this point.
57 let rows = proposer.prefill_hidden_rows(self.mtp_prefill_capacity);
58 if !self.mtp_prefill_hidden.is_null() && rows < self.mtp_prefill_capacity {
59 let was = self.mtp_prefill_capacity;
60 let bytes = rows * self.config.hidden_size * 2;
61 let old = std::mem::replace(
62 &mut self.mtp_prefill_hidden,
63 spark_runtime::gpu::DevicePtr::NULL,
64 );
65 self.mtp_prefill_capacity = 0;
66 match self.gpu.free(old).and_then(|_| self.gpu.alloc(bytes)) {
67 Ok(smaller) => {
68 self.mtp_prefill_hidden = smaller;
69 self.mtp_prefill_capacity = rows;
70 tracing::info!(
71 "MTP drafter context: capture buffer rightsized {was} -> {rows} rows \
72 ({:.0} -> {:.0} MB) â the proposer cannot be handed a position past \
73 {rows} (A59)",
74 (was * self.config.hidden_size * 2) as f64 / 1e6,
75 bytes as f64 / 1e6,
76 );
77 }
78 // NULL + capacity 0 is the feature's own "off" state: the capture epilogue
79 // and the propose-site coverage check both gate on it, so drafter-prefill
80 // disables and the serve keeps running at plain acceptance. Losing a
81 // throughput feature beats failing a serve over an optimisation.
82 Err(e) => tracing::warn!(
83 "MTP drafter context: rightsizing the capture buffer failed ({e:#}) â \
84 drafter prefill and carry are DISABLED for this serve"
85 ),
86 }
87 }
88 self.proposer = Some(proposer);
89 }
90
91 /// Install the fused n-gram input embedding (LongCat family). Once set,
92 /// every embedding site routes through it instead of the plain
93 /// `embed_tokens` gather.
94 pub fn set_ngram_embedding(&mut self, ngram: crate::layers::ngram_embed::NgramEmbedding) {
95 tracing::info!("set_ngram_embedding: installed on the served model");
96 self.ngram_embed = Some(std::sync::Mutex::new(ngram));
97 }
98
99 /// True when this model fuses n-gram lookups into its input embedding.
100 pub fn has_ngram_embedding(&self) -> bool {
101 self.ngram_embed.is_some()
102 }
103
104 /// True when MLA prefill cannot honour a prefix-cache skip.
105 ///
106 /// `paged_mla`'s flash call is fed the K/V it just assembled â its own
107 /// comment says "not from paged cache" â so it attends ONLY over the
108 /// tokens being processed. For a full prompt that is correct, and it is
109 /// how every MLA model has been exercised. With a SKIPPED prefix it is
110 /// not: the cached tokens are simply absent from attention and the model
111 /// answers from the tail of its prompt, fluently and wrongly.
112 ///
113 /// MLA keeps a COMPRESSED (latent) KV cache, so letting this path attend
114 /// over history means absorbed attention against that cache, not a wider
115 /// gather. Until that exists, decline the SKIP rather than the cache:
116 /// prefix caching stays on and correct â block reuse and the decode path
117 /// still benefit â and prefill pays full price.
118 ///
119 /// ATLAS_MLA_PREFIX_SKIP=1 opts back in once `paged_mla` attends the cache.
120 pub(crate) fn mla_prefill_needs_full_recompute(&self) -> bool {
121 if std::env::var("ATLAS_MLA_PREFIX_SKIP").as_deref() == Ok("1") {
122 return false;
123 }
124 self.layers.iter().any(|l| l.uses_local_mla_prefill())
125 }
126}