metrale_speculative/spec_capacity.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Draft-count limits for speculative dispatch: the verify-slot
4//! capacity of every active sequence, and the serial path's verify arm.
5//!
6//! Owner: speculative.
7//! Invariants:
8//! - `clamp_drafts_to_slot_capacity` returns no more than `drafts` and no
9//! more than any capacity it is given.
10//! - `serial_verify_plan` keeps at most `step_drafts` of an MTP sequence's
11//! pending drafts.
12//!
13//! The verify pools size each slot for the deepest draft count the static
14//! ladder can give a sequence in it (`ssm_reserve::verify_slot_drafts`).
15//! With the default ladder `4:3,8:3,16:1,32:1` and `--num-drafts 3`, slots
16//! 0..8 hold 3 drafts and slots from 8 on hold 1. A step can ask for more:
17//! a sequence can sit on a high slot while `n_active` is small, and
18//! `adaptive_rung` can raise n in 9..=16 to 2 drafts. A step's draft count is
19//! therefore clamped to the smallest capacity among its active sequences,
20//! read from the model's pools (`Model::mtp_slot_draft_capacity`).
21//! `METRALE_MTP_POOL_FULL_WIDTH` gives every slot `num_drafts`, which makes
22//! the clamp a no-op. With the tiered default the adaptive lift never
23//! survives the clamp: nine or more active sequences hold nine distinct
24//! slots, one of them at index 8 or above, whose capacity is 1.
25
26/// 2026-09-25: Clamps a step's draft count to the smallest verify-slot
27/// capacity among the active sequences. `usize::MAX` (the `Model` default,
28/// for a model without SSM verify pools) does not clamp; an empty iterator
29/// leaves `drafts` unchanged.
30pub fn clamp_drafts_to_slot_capacity(
31 drafts: usize,
32 slot_capacities: impl IntoIterator<Item = usize>,
33) -> usize {
34 slot_capacities
35 .into_iter()
36 .fold(drafts, |acc, cap| acc.min(cap))
37}
38
39/// 2026-09-25: The verify arm the serial (per-sequence) path runs for one
40/// sequence.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum SerialArm {
43 DFlash,
44 /// 2026-10-02: Four or more drafts on an MTP serve (a long prompt-lookup
45 /// copy): one K-row verify, then the K4 verdict.
46 KN,
47 K4,
48 K3,
49 K2,
50}
51
52/// 2026-09-25: How many of a sequence's `pending` drafts the serial path
53/// keeps, and which arm verifies them. An MTP sequence can carry more
54/// pending drafts than this step's width `step_drafts` (the ladder count
55/// after [`clamp_drafts_to_slot_capacity`]); the surplus is dropped so the
56/// verify never exceeds the slot. With `dflash` every pending draft is kept.
57/// Four or more kept drafts run the DFlash arm on a DFlash serve and the KN arm
58/// otherwise; fewer run K4, K3 or K2 by the kept count, each also bounded by
59/// `step_drafts`.
60pub fn serial_verify_plan(pending: usize, step_drafts: usize, dflash: bool) -> (usize, SerialArm) {
61 let keep = if dflash {
62 pending
63 } else {
64 pending.min(step_drafts)
65 };
66 let arm = if keep >= 4 && dflash {
67 SerialArm::DFlash
68 } else if keep >= 4 {
69 SerialArm::KN
70 } else if step_drafts >= 3 && keep >= 3 {
71 SerialArm::K4
72 } else if step_drafts >= 2 && keep >= 2 {
73 SerialArm::K3
74 } else {
75 SerialArm::K2
76 };
77 (keep, arm)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn a_low_capacity_slot_bounds_the_whole_step() {
86 // 2026-09-25: One capacity-1 slot limits the whole step to one draft.
87 assert_eq!(clamp_drafts_to_slot_capacity(3, [3, 3, 1]), 1);
88 // 2026-09-25: One sequence on a high slot at small n.
89 assert_eq!(clamp_drafts_to_slot_capacity(3, [1]), 1);
90 // 2026-09-25: The adaptive-rung lift (2 drafts at n in 9..=16) is
91 // clamped by a capacity-1 slot in the batch.
92 assert_eq!(clamp_drafts_to_slot_capacity(2, [3, 1, 3]), 1);
93 }
94
95 #[test]
96 fn full_capacity_slots_do_not_clamp() {
97 assert_eq!(clamp_drafts_to_slot_capacity(3, [3, 3, 3]), 3);
98 // 2026-09-25: `usize::MAX` (no SSM verify pools) does not clamp.
99 assert_eq!(clamp_drafts_to_slot_capacity(3, [usize::MAX; 4]), 3);
100 // 2026-09-25: An empty iterator leaves the count unchanged.
101 assert_eq!(clamp_drafts_to_slot_capacity(2, std::iter::empty()), 2);
102 }
103
104 #[test]
105 fn a_ladder_below_num_drafts_never_reaches_the_k4_verify_at_n1() {
106 // 2026-09-25: A one-draft ladder (`METRALE_MTP_K_LADDER=1:1`) under
107 // `--num-drafts 3`: slot 0 holds one draft, so the step width is 1 and
108 // a sequence carrying three or two pending drafts keeps one and runs
109 // K2.
110 let step = clamp_drafts_to_slot_capacity(1, [1]);
111 assert_eq!(serial_verify_plan(3, step, false), (1, SerialArm::K2));
112 assert_eq!(serial_verify_plan(2, step, false), (1, SerialArm::K2));
113 // 2026-09-25: A two-draft step keeps two of three and runs K3.
114 assert_eq!(serial_verify_plan(3, 2, false), (2, SerialArm::K3));
115 }
116
117 #[test]
118 fn a_full_width_step_dispatches_as_before() {
119 assert_eq!(serial_verify_plan(3, 3, false), (3, SerialArm::K4));
120 assert_eq!(serial_verify_plan(2, 3, false), (2, SerialArm::K3));
121 assert_eq!(serial_verify_plan(1, 3, false), (1, SerialArm::K2));
122 assert_eq!(serial_verify_plan(0, 3, false), (0, SerialArm::K2));
123 // 2026-09-25: DFlash γ-blocks are never truncated to an MTP width.
124 assert_eq!(serial_verify_plan(16, 16, true), (16, SerialArm::DFlash));
125 assert_eq!(serial_verify_plan(3, 3, true), (3, SerialArm::K4));
126 }
127
128 #[test]
129 fn a_long_mtp_copy_takes_the_kn_arm_never_dflash() {
130 // 2026-10-02: A prompt-lookup copy of 8 on an MTP serve: the step width
131 // is the copy's, and 4+ drafts take the K-row MTP verify, not DFlash's
132 // (whose drafter trims and telemetry belong to a block drafter).
133 assert_eq!(serial_verify_plan(8, 8, false), (8, SerialArm::KN));
134 assert_eq!(serial_verify_plan(4, 4, false), (4, SerialArm::KN));
135 assert_eq!(serial_verify_plan(8, 3, false), (3, SerialArm::K4));
136 }
137}