burn_mamba/mamba3/cache.rs
1//! # Mamba-3 Cache and Pathway Selection
2//!
3//! [`Mamba3Cache`] / [`Mamba3Caches`] are **enums** tagging which SSD pathway a
4//! cache belongs to (`DoubleSsd` | `SingleSsd`). Supplying one of these to
5//! [`Mamba3::forward`](crate::mamba3::mamba3::Mamba3::forward) /
6//! [`Mamba3::step`](crate::mamba3::mamba3::Mamba3::step) is what selects the
7//! pathway at runtime; a missing cache defaults to `SingleSsd`.
8//!
9//! The two pathways' SSM accumulators differ **mid-sequence**, so the cache
10//! types are kept distinct to prevent silently mixing them inside a chunked
11//! pass. They coincide at sequence boundaries, however — where caches are
12//! actually produced and consumed — so the `From` impls at the bottom convert
13//! between them by a lossless field-by-field move (see the note there).
14
15use crate::mamba3::double_ssd::prelude::*;
16use crate::mamba3::single_ssd::prelude::*;
17
18/// A pathway-tagged bundle of per-layer caches, so a single dispatch entry can
19/// accept / return either cache family.
20///
21/// The caches selection infers whether Double-SSD or Single-SSD is used.
22/// If none is specified, this defaults to [`Self::SingleSsd`].
23///
24/// See also [`crate::mamba3::ssd_path::Mamba3SsdPath`].
25#[derive(Debug)]
26pub enum Mamba3Caches {
27 /// Caches for the double-ssd pathway.
28 DoubleSsd(Mamba3DoubleSsdCaches),
29 /// Caches for the single-ssd pathway.
30 SingleSsd(Mamba3SingleSsdCaches),
31}
32
33/// A pathway-tagged bundle of per-block cache, so a single dispatch entry can
34/// accept / return either cache family.
35///
36/// The cache selection infers whether Double-SSD or Single-SSD is used.
37/// If none is specified, this defaults to [`Self::SingleSsd`].
38///
39/// See also [`crate::mamba3::ssd_path::Mamba3SsdPath`].
40#[derive(Debug)]
41pub enum Mamba3Cache {
42 /// Caches for double-ssd pathway.
43 DoubleSsd(Mamba3DoubleSsdCache),
44 /// Caches for single-ssd pathway.
45 SingleSsd(Mamba3SingleSsdCache),
46}
47
48impl Mamba3Caches {
49 /// Unwrap to the double-SSD caches, or `None` if this is the single-SSD variant.
50 pub fn double_ssd(self) -> Option<Mamba3DoubleSsdCaches> {
51 match self {
52 Self::DoubleSsd(caches) => Some(caches),
53 Self::SingleSsd(_caches) => None,
54 }
55 }
56
57 /// Unwrap to the single-SSD caches, or `None` if this is the double-SSD variant.
58 pub fn single_ssd(self) -> Option<Mamba3SingleSsdCaches> {
59 match self {
60 Self::DoubleSsd(_caches) => None,
61 Self::SingleSsd(caches) => Some(caches),
62 }
63 }
64
65 /// Number of per-layer caches (independent of pathway).
66 pub fn caches_len(&self) -> usize {
67 match self {
68 Self::DoubleSsd(caches) => caches.caches.len(),
69 Self::SingleSsd(caches) => caches.caches.len(),
70 }
71 }
72
73 /// Collect per-layer caches into a pathway-tagged bundle. The pathway is
74 /// inferred from the first element (an empty vec implies single-SSD).
75 pub fn from_vec(vec: Vec<Mamba3Cache>) -> Self {
76 // peek at first; empty implies single_ssd
77 let is_double = matches!(vec.first(), Some(Mamba3Cache::DoubleSsd(_)));
78 if is_double {
79 Mamba3DoubleSsdCaches {
80 caches: vec
81 .into_iter()
82 .map(Mamba3Cache::double_ssd)
83 .map(Option::unwrap)
84 .collect(),
85 }
86 .into()
87 } else {
88 Mamba3SingleSsdCaches {
89 caches: vec
90 .into_iter()
91 .map(Mamba3Cache::single_ssd)
92 .map(Option::unwrap)
93 .collect(),
94 }
95 .into()
96 }
97 }
98
99 /// Wrap each per-layer cache in `Some` so the loop can `take` it without
100 /// cloning (Burn tensors are reference-counted).
101 pub fn into_options(self) -> Vec<Option<Mamba3Cache>> {
102 match self {
103 Self::DoubleSsd(caches) => caches
104 .caches
105 .into_iter()
106 .map(Mamba3Cache::from)
107 .map(Some)
108 .collect(),
109 Self::SingleSsd(caches) => caches
110 .caches
111 .into_iter()
112 .map(Mamba3Cache::from)
113 .map(Some)
114 .collect(),
115 }
116 }
117
118 /// Inverse of [`Self::into_options`]: unwrap each slot and re-bundle.
119 pub fn from_options(options: Vec<Option<Mamba3Cache>>) -> Self {
120 let caches = options.into_iter().map(Option::unwrap).collect();
121 Self::from_vec(caches)
122 }
123}
124
125impl Mamba3Cache {
126 /// Unwrap to the double-SSD cache, or `None` if this is the single-SSD variant.
127 pub fn double_ssd(self) -> Option<Mamba3DoubleSsdCache> {
128 match self {
129 Self::DoubleSsd(cache) => Some(cache),
130 Self::SingleSsd(_cache) => None,
131 }
132 }
133
134 /// Unwrap to the single-SSD cache, or `None` if this is the double-SSD variant.
135 pub fn single_ssd(self) -> Option<Mamba3SingleSsdCache> {
136 match self {
137 Self::DoubleSsd(_cache) => None,
138 Self::SingleSsd(cache) => Some(cache),
139 }
140 }
141}
142
143impl From<Mamba3DoubleSsdCaches> for Mamba3Caches {
144 fn from(caches: Mamba3DoubleSsdCaches) -> Self {
145 Mamba3Caches::DoubleSsd(caches)
146 }
147}
148
149impl From<Mamba3SingleSsdCaches> for Mamba3Caches {
150 fn from(caches: Mamba3SingleSsdCaches) -> Self {
151 Mamba3Caches::SingleSsd(caches)
152 }
153}
154
155impl From<Mamba3DoubleSsdCache> for Mamba3Cache {
156 fn from(cache: Mamba3DoubleSsdCache) -> Self {
157 Mamba3Cache::DoubleSsd(cache)
158 }
159}
160
161impl From<Mamba3SingleSsdCache> for Mamba3Cache {
162 fn from(cache: Mamba3SingleSsdCache) -> Self {
163 Mamba3Cache::SingleSsd(cache)
164 }
165}
166
167// ---------------------------------------------------------------------------
168// Conversions between the two pathway caches
169// ---------------------------------------------------------------------------
170//
171// At a cache boundary (the last token of a `forward` / `step` call) the
172// look-ahead term `(1 − λₜ₊₁)·Δₜ₊₁` vanishes, so `scaleₜ = γₜ` for the final
173// position. Substituting that into the single-ssd accumulator
174// `h'ₜ = αₜ h'ₜ₋₁ + scaleₜ Bₜ⊗xₜ` makes it coincide *exactly* with the
175// double-ssd state `hₜ = αₜ hₜ₋₁ + βₜ Bₜ₋₁⊗xₜ₋₁ + γₜ Bₜ⊗xₜ` — the deferred β
176// contribution of the next token is reconstructed on the following call from
177// the saved `k_state`/`v_state`, identically in both forms. The remaining
178// three fields (previous-token K/V history and cumulative RoPE angle) carry the
179// same meaning in both caches. Hence the conversion is a field-by-field move.
180//
181// The accumulators differ only *mid-sequence*; since caches are only ever
182// produced and consumed at boundaries, the move is lossless there. The distinct
183// types still prevent silently mixing the two accumulators inside a single
184// chunked pass.
185
186impl From<Mamba3SingleSsdCache> for Mamba3DoubleSsdCache {
187 fn from(cache: Mamba3SingleSsdCache) -> Self {
188 Mamba3DoubleSsdCache {
189 ssm_bhpr: cache.ssm_bhpr,
190 k_state_bmhr: cache.k_state_bmhr,
191 v_state_bhp: cache.v_state_bhp,
192 rotation: cache.rotation,
193 }
194 }
195}
196
197impl From<Mamba3DoubleSsdCache> for Mamba3SingleSsdCache {
198 fn from(cache: Mamba3DoubleSsdCache) -> Self {
199 Mamba3SingleSsdCache {
200 ssm_bhpr: cache.ssm_bhpr,
201 k_state_bmhr: cache.k_state_bmhr,
202 v_state_bhp: cache.v_state_bhp,
203 rotation: cache.rotation,
204 }
205 }
206}
207
208impl From<Mamba3SingleSsdCaches> for Mamba3DoubleSsdCaches {
209 fn from(caches: Mamba3SingleSsdCaches) -> Self {
210 Mamba3DoubleSsdCaches {
211 caches: caches.caches.into_iter().map(Into::into).collect(),
212 }
213 }
214}
215
216impl From<Mamba3DoubleSsdCaches> for Mamba3SingleSsdCaches {
217 fn from(caches: Mamba3DoubleSsdCaches) -> Self {
218 Mamba3SingleSsdCaches {
219 caches: caches.caches.into_iter().map(Into::into).collect(),
220 }
221 }
222}