metrale_model_layers/speculative/
verify_key.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Batched-verify ordering and graph key: the dispatch order and
4//! depth assignment of one verify batch, and the CUDA-graph cache key built
5//! from them.
6//!
7//! Owner: model-layers (speculative).
8//! Invariants:
9//! - `verify_batch_permutation` returns a permutation of `0..n`, and panics
10//!   when `slots` and `ks` differ in length.
11//! - With `canonical = true`, `verify_batch_order` places slots in
12//!   non-decreasing order and depths in non-increasing order, and returns the
13//!   multiset of depths it was given.
14//! - With `canonical = false`, each member keeps its own depth, ordered
15//!   deepest first, then by slot.
16//!
17//! The scheduler orders each batch with these functions (`mtp_dcut::plan`,
18//! `mtp_step`) and the model keys its batched-verify graphs with
19//! [`verify_graph_key`] (`verify_e2::verify_batched_graph_key`), so the
20//! dispatch and the key see one order. Under the canonical assignment the key
21//! depends on the slot set and the depth multiset, not on which sequence D-Cut
22//! gave which depth, which reduces the number of distinct keys. Ascending
23//! slots also keep a depth run on consecutive slots when the pool slots are
24//! consecutive; the batched GDN path checks the actual pointers and declines
25//! otherwise (`trait_decode_batched_conv_gdn_multi.rs`).
26//!
27//! [`canonical_assignment`] chooses the arm from the batch width.
28//! `METRALE_NO_CANONICAL_VERIFY_KEY`, present with any value (`0` included),
29//! turns the canonical arm off at every width.
30
31/// 2026-09-25: True unless `METRALE_NO_CANONICAL_VERIFY_KEY` is present. Read
32/// once per process.
33pub fn canonical_verify_key_enabled() -> bool {
34    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
35    *ON.get_or_init(|| std::env::var_os("METRALE_NO_CANONICAL_VERIFY_KEY").is_none())
36}
37
38/// 2026-09-25: Batch width (sequences) at or above which [`canonical_assignment`]
39/// selects the canonical arm, unless `METRALE_CANONICAL_KEY_MIN_WIDTH` overrides
40/// it. Above the scheduler's D-Cut width cap (`SchedLevers::dcut_width_cap`),
41/// `mtp_dcut::plan` leaves every depth equal, and then both arms give the same
42/// order (`uniform_depths_are_identical_under_both_arms`).
43pub const CANONICAL_KEY_MIN_WIDTH: usize = 8;
44
45/// 2026-09-25: The width threshold: `METRALE_CANONICAL_KEY_MIN_WIDTH` when it
46/// parses as a `usize` (0 selects the canonical arm at every width), else
47/// [`CANONICAL_KEY_MIN_WIDTH`]. Read once per process.
48pub fn canonical_key_min_width() -> usize {
49    static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
50    *N.get_or_init(|| min_width_from_env(std::env::var_os(ENV_MIN_WIDTH)))
51}
52
53const ENV_MIN_WIDTH: &str = "METRALE_CANONICAL_KEY_MIN_WIDTH";
54
55/// 2026-09-25: Parses a raw [`ENV_MIN_WIDTH`] value; [`canonical_key_min_width`]
56/// does the environment read, so tests call this directly.
57fn min_width_from_env(raw: Option<std::ffi::OsString>) -> usize {
58    raw.and_then(|v| v.into_string().ok())
59        .and_then(|v| v.trim().parse::<usize>().ok())
60        .unwrap_or(CANONICAL_KEY_MIN_WIDTH)
61}
62
63/// 2026-09-25: Whether a batch of `n` sequences gets the canonical depth-to-slot
64/// assignment. `mtp_dcut::plan` asks with the batch width and `mtp_step` asks
65/// with the same width for each chunk it dispatches, so the order `plan`
66/// assigned and the permutation `mtp_step` applies use the same arm.
67pub fn canonical_assignment(n: usize) -> bool {
68    canonical_assignment_at(n, canonical_key_min_width(), canonical_verify_key_enabled())
69}
70
71/// 2026-09-25: The policy of [`canonical_assignment`] with its two
72/// environment inputs passed in: the threshold, and whether the kill switch
73/// is clear. With the kill switch set, no width or threshold selects the
74/// canonical arm.
75fn canonical_assignment_at(n: usize, min_width: usize, kill_switch_clear: bool) -> bool {
76    kill_switch_clear && n >= min_width
77}
78
79/// 2026-09-25: Dispatch order for one verify batch, as a permutation only.
80///
81/// `slots[i]` and `ks[i]` describe member `i` in the caller's order (`ks[i]`
82/// is its row count, drafts + 1). `order[p]` is the input index dispatched at
83/// position `p`.
84///
85/// * `canonical = true`: ascending ssm slot; `ks` is unread.
86/// * `canonical = false`: deepest first, then ascending ssm slot.
87///
88/// Ties break on input index, so the order is a deterministic function of
89/// the inputs. Under `canonical = true` a second application to the ordered
90/// batch returns the identity (`canonical_order_is_idempotent`).
91///
92/// Panics when `slots` and `ks` differ in length.
93pub fn verify_batch_permutation(slots: &[usize], ks: &[usize], canonical: bool) -> Vec<usize> {
94    assert_eq!(
95        slots.len(),
96        ks.len(),
97        "verify_batch_permutation: slots/ks mismatch"
98    );
99    let n = slots.len().min(ks.len());
100    let mut order: Vec<usize> = (0..n).collect();
101    if canonical {
102        order.sort_by_key(|&i| (slots[i], i));
103    } else {
104        order.sort_by_key(|&i| (std::cmp::Reverse(ks[i]), slots[i], i));
105    }
106    order
107}
108
109/// 2026-09-25: Orders one verify batch and assigns its depths; the entry point
110/// of `mtp_dcut::plan`, which truncates each sequence's drafts to the depth
111/// assigned here.
112///
113/// Returns `(order, depths)`: `order` is [`verify_batch_permutation`] and
114/// `depths[p]` is the row count position `p` verifies.
115///
116/// * `canonical = true`: the depth multiset is sorted descending onto the
117///   ordered batch, so `depths[p]` need not be `ks[order[p]]`. Slots are
118///   non-decreasing in `p` and depths non-increasing.
119/// * `canonical = false`: each member keeps its own depth.
120///
121/// Stages that only need the dispatch order use [`verify_batch_permutation`],
122/// which leaves every member's depth attached to it.
123pub fn verify_batch_order(
124    slots: &[usize],
125    ks: &[usize],
126    canonical: bool,
127) -> (Vec<usize>, Vec<usize>) {
128    let order = verify_batch_permutation(slots, ks, canonical);
129    let depths: Vec<usize> = if canonical {
130        let mut d: Vec<usize> = ks[..order.len()].to_vec();
131        d.sort_unstable_by(|a, b| b.cmp(a));
132        d
133    } else {
134        order.iter().map(|&i| ks[i]).collect()
135    };
136    (order, depths)
137}
138
139/// 2026-09-25: The batched-verify CUDA-graph cache key: the `(ssm slot, row
140/// count)` pairs in dispatch order, then one sentinel word,
141/// `u32::MAX - wy_tables_null - 2 * write_on_accept`, which differs for each
142/// of the four flag combinations. `write_on_accept` is in the key because it
143/// can select a different WY launch (the write-on-accept K=4 twin,
144/// `trait_decode_batched_conv_gdn_multi.rs`).
145pub fn verify_graph_key(
146    pairs: &[(u32, u32)],
147    wy_tables_null: bool,
148    write_on_accept: bool,
149) -> Vec<u32> {
150    let mut key: Vec<u32> = Vec::with_capacity(2 * pairs.len() + 1);
151    for &(slot, k) in pairs {
152        key.push(slot);
153        key.push(k);
154    }
155    key.push(u32::MAX - u32::from(wy_tables_null) - 2 * u32::from(write_on_accept));
156    key
157}
158
159#[cfg(test)]
160#[path = "verify_key_tests.rs"]
161mod tests;