metrale_model_layers/
drafter_context.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Resolution of the MTP drafter context from the environment: whether
4//! the drafter KV is batch-prefilled over the prompt (`prefill`) and whether it is
5//! carried across the turns of a session (`carry`).
6//!
7//! Carry depends on prefill. In the model's `trait_impl/speculative.rs` a carried
8//! drafter KV is adopted (`try_carry_drafter`) only inside
9//! `if !self.mtp_prefill_hidden.is_null()`, and the model allocates that capture
10//! only when `drafter.prefill` is set (`layers::mtp_drafter_prefill_enabled`), so
11//! the two are resolved together.
12//! A configured carry is armed only in single-sequence MTP
13//! (`mtp_carry::carry_armed`).
14//!
15//! | variable (exactly `1`) | result |
16//! |---|---|
17//! | neither | `DrafterContext::BOTH` |
18//! | `METRALE_NO_MTP_DRAFTER_CONTEXT` | `DrafterContext::OFF` |
19//! | `METRALE_MTP_DRAFTER_CONTEXT_PREFILL_ONLY_UNSAFE` | prefill on, carry off; warned at startup |
20//!
21//! Any other value, `0` included, changes nothing.
22//!
23//! Owner: model-layers (MTP drafter).
24//! Invariants:
25//! - [`resolve`] never returns `carry` without `prefill`.
26//! - The kill switch wins over the prefill-only variable.
27
28/// 2026-09-25: Kill switch for both halves; only the exact value `1` counts.
29pub const DISABLE_ENV: &str = "METRALE_NO_MTP_DRAFTER_CONTEXT";
30
31/// 2026-09-25: Prefill without carry; only the exact value `1` counts, and
32/// [`DISABLE_ENV`] wins over it.
33pub const PREFILL_ONLY_ENV: &str = "METRALE_MTP_DRAFTER_CONTEXT_PREFILL_ONLY_UNSAFE";
34
35/// 2026-09-25: Which halves of the drafter context are configured. [`resolve`]
36/// never returns `carry` without `prefill`.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct DrafterContext {
39    /// 2026-09-25: Capture prompt hiddens and batch-prefill the drafter KV.
40    pub prefill: bool,
41    /// 2026-09-25: Carry the drafter KV across turns of a session; it runs only
42    /// when `mtp_carry::carry_armed` also holds.
43    pub carry: bool,
44}
45
46impl Default for DrafterContext {
47    /// 2026-09-25: [`BOTH`](DrafterContext::BOTH). Written out because a derived
48    /// `Default` would be [`OFF`](DrafterContext::OFF).
49    fn default() -> Self {
50        Self::BOTH
51    }
52}
53
54impl DrafterContext {
55    /// 2026-09-25: Both halves on: what [`resolve`] returns when neither variable is `1`.
56    pub const BOTH: Self = Self {
57        prefill: true,
58        carry: true,
59    };
60    /// 2026-09-25: Both halves off: what [`resolve`] returns for the kill switch.
61    pub const OFF: Self = Self {
62        prefill: false,
63        carry: false,
64    };
65}
66
67/// 2026-09-25: Resolve the two variables' values with no environment access, so
68/// the rule is unit-tested (SBIO). `disable` wins over `prefill_only`.
69pub fn resolve(disable: Option<&str>, prefill_only: Option<&str>) -> DrafterContext {
70    if disable == Some("1") {
71        return DrafterContext::OFF;
72    }
73    if prefill_only == Some("1") {
74        // 2026-09-25: The only branch that returns prefill without carry.
75        return DrafterContext {
76            prefill: true,
77            carry: false,
78        };
79    }
80    DrafterContext::BOTH
81}
82
83/// 2026-09-25: Resolve the policy from the environment. `ModelLevers::from_env`
84/// calls it, and the result is `ModelLevers::drafter`. The outcome is logged once
85/// per process (`REPORTED`).
86pub fn resolve_from_env() -> DrafterContext {
87    {
88        let disable = std::env::var(DISABLE_ENV).ok();
89        let prefill_only = std::env::var(PREFILL_ONLY_ENV).ok();
90        let cfg = resolve(disable.as_deref(), prefill_only.as_deref());
91
92        REPORTED.call_once(|| report(cfg));
93        cfg
94    }
95}
96
97/// 2026-09-25: Makes `report` run once per process, on the first [`resolve_from_env`].
98static REPORTED: std::sync::Once = std::sync::Once::new();
99
100fn report(cfg: DrafterContext) {
101    // 2026-09-25: Report the armed carry (`mtp_carry::carry_armed`, the predicate
102    // the runtime gates on), not the configured one: the MTP dispatch cap can
103    // force the carry off under this config.
104    let carry_armed = crate::mtp_carry::carry_armed(cfg);
105    tracing::info!(
106        "MTP drafter context: prefill={} carry={} ({}). Disable both with {}=1.",
107        on_off(cfg.prefill),
108        on_off(carry_armed),
109        if cfg == DrafterContext::BOTH {
110            "default"
111        } else {
112            "overridden by environment"
113        },
114        DISABLE_ENV,
115    );
116    if cfg.carry && !carry_armed {
117        tracing::warn!(
118            "MTP cross-turn carry is CONFIGURED ON but INERT: the MTP \
119             dispatch cap is {} (>1), and the carry slot is single-sequence \
120             by design, so it is force-disabled. Pass --mtp-max-seqs 1 \
121             to arm it; leave it unset to keep multi-sequence MTP.",
122            crate::speculative::mtp_max_seqs(),
123        );
124    }
125    if cfg.prefill && !cfg.carry {
126        tracing::warn!(
127            "{PREFILL_ONLY_ENV}=1: drafter prefill is ON with cross-turn carry \
128             OFF. This is a MEASUREMENT ARM, not a deployment — a warm turn \
129             rebuilds the drafter for a measured 1136 ms against ~211 ms/turn \
130             of decode saving (net -927 ms/turn, spent on TTFT).",
131        );
132    }
133}
134
135fn on_off(b: bool) -> &'static str {
136    if b { "ON" } else { "OFF" }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn bare_environment_enables_both() {
145        assert_eq!(resolve(None, None), DrafterContext::BOTH);
146    }
147
148    #[test]
149    fn kill_switch_disables_both() {
150        assert_eq!(resolve(Some("1"), None), DrafterContext::OFF);
151    }
152
153    #[test]
154    fn prefill_only_arm_disables_carry_only() {
155        assert_eq!(
156            resolve(None, Some("1")),
157            DrafterContext {
158                prefill: true,
159                carry: false
160            }
161        );
162    }
163
164    #[test]
165    fn kill_switch_beats_the_research_arm() {
166        assert_eq!(resolve(Some("1"), Some("1")), DrafterContext::OFF);
167    }
168
169    #[test]
170    fn only_exactly_one_switches_anything() {
171        for v in ["0", "", "true", "yes", "2", "1 "] {
172            assert_eq!(
173                resolve(Some(v), None),
174                DrafterContext::BOTH,
175                "{DISABLE_ENV}={v:?} must not disable",
176            );
177            assert_eq!(
178                resolve(None, Some(v)),
179                DrafterContext::BOTH,
180                "{PREFILL_ONLY_ENV}={v:?} must not change anything",
181            );
182        }
183    }
184}