1use alloc::collections::BTreeMap;
15use alloc::format;
16use alloc::string::String;
17use alloc::vec::Vec;
18
19use burn_core::tensor::{Device, TensorData};
20use burn_pack::Scalar;
21
22pub use burn_derive::RecordState;
23
24pub fn join_path(prefix: &str, leaf: &str) -> String {
28 if prefix.is_empty() {
29 return String::from(leaf);
30 }
31 let mut path = String::with_capacity(prefix.len() + 1 + leaf.len());
32 path.push_str(prefix);
33 path.push('.');
34 path.push_str(leaf);
35 path
36}
37
38pub fn join_index(prefix: &str, index: usize) -> String {
42 format!("{prefix}.{index}")
43}
44
45#[derive(Default, Debug)]
47pub struct StateSink {
48 pub tensors: Vec<(String, TensorData)>,
50 pub scalars: Vec<(String, Scalar)>,
52}
53
54impl StateSink {
55 pub fn push_tensor(&mut self, prefix: &str, leaf: &str, data: TensorData) {
57 self.tensors.push((join_path(prefix, leaf), data));
58 }
59
60 pub fn push_scalar(&mut self, prefix: &str, leaf: &str, value: Scalar) {
62 self.scalars.push((join_path(prefix, leaf), value));
63 }
64}
65
66#[derive(Default, Debug)]
71pub struct StateSource {
72 tensors: BTreeMap<String, TensorData>,
73 scalars: BTreeMap<String, Scalar>,
74}
75
76impl StateSource {
77 pub fn new(scalars: BTreeMap<String, Scalar>) -> Self {
79 Self {
80 tensors: BTreeMap::new(),
81 scalars,
82 }
83 }
84
85 pub fn insert_tensor(&mut self, name: String, data: TensorData) {
87 self.tensors.insert(name, data);
88 }
89
90 pub fn take_tensor(&mut self, prefix: &str, leaf: &str) -> Option<TensorData> {
92 self.tensors.remove(&join_path(prefix, leaf))
93 }
94
95 pub fn take_scalar(&mut self, prefix: &str, leaf: &str) -> Option<Scalar> {
97 self.scalars.get(&join_path(prefix, leaf)).copied()
98 }
99
100 pub fn has_under(&self, prefix: &str) -> bool {
106 let pat = join_path(prefix, "");
107 self.tensors.keys().any(|k| k.starts_with(&pat))
108 || self.scalars.keys().any(|k| k.starts_with(&pat))
109 }
110}
111
112pub trait RecordState: Sized + Send + Sync + 'static {
117 fn state_flatten(&self, prefix: &str, out: &mut StateSink);
119
120 fn state_unflatten(prefix: &str, src: &mut StateSource, device: &Device) -> Option<Self>;
129}
130
131impl RecordState for () {
133 fn state_flatten(&self, _prefix: &str, _out: &mut StateSink) {}
134
135 fn state_unflatten(_prefix: &str, _src: &mut StateSource, _device: &Device) -> Option<Self> {
136 Some(())
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use burn::tensor::Tensor;
144 use burn_core as burn;
145
146 fn round_trip<T: RecordState>(state: &T) -> Option<T> {
148 let mut sink = StateSink::default();
149 state.state_flatten("p", &mut sink);
150
151 let scalars: BTreeMap<String, Scalar> = sink.scalars.into_iter().collect();
152 let mut source = StateSource::new(scalars);
153 for (name, data) in sink.tensors {
154 source.insert_tensor(name, data);
155 }
156
157 T::state_unflatten("p", &mut source, &Device::default())
158 }
159
160 fn tensor(values: &[f32]) -> Tensor<1> {
161 Tensor::from_data(TensorData::from(values), &Device::default())
162 }
163
164 fn data(t: &Tensor<1>) -> Vec<f32> {
165 t.clone().into_data().to_vec().unwrap()
166 }
167
168 #[derive(RecordState, Clone, Debug)]
169 struct Inner<const D: usize> {
170 weight: Tensor<D>,
171 step: i64,
172 }
173
174 #[derive(RecordState, Clone, Debug)]
175 struct Full<const D: usize> {
176 t: Tensor<D>,
177 opt_tensor: Option<Tensor<D>>,
178 history: Vec<Tensor<D>>,
179 count: usize,
180 opt_scalar: Option<f64>,
181 nested: Inner<D>,
182 opt_nested: Option<Inner<D>>,
183 }
184
185 #[test]
186 fn all_field_kinds_round_trip() {
187 let state = Full::<1> {
188 t: tensor(&[1.0, 2.0]),
189 opt_tensor: Some(tensor(&[3.0])),
190 history: vec![tensor(&[4.0]), tensor(&[5.0, 6.0])],
191 count: 7,
192 opt_scalar: Some(8.5),
193 nested: Inner {
194 weight: tensor(&[9.0]),
195 step: 10,
196 },
197 opt_nested: Some(Inner {
198 weight: tensor(&[11.0]),
199 step: 12,
200 }),
201 };
202
203 let out = round_trip(&state).unwrap();
204
205 assert_eq!(data(&out.t), vec![1.0, 2.0]);
206 assert_eq!(data(&out.opt_tensor.unwrap()), vec![3.0]);
207 assert_eq!(out.history.len(), 2);
208 assert_eq!(data(&out.history[0]), vec![4.0]);
209 assert_eq!(data(&out.history[1]), vec![5.0, 6.0]);
210 assert_eq!(out.count, 7);
211 assert_eq!(out.opt_scalar, Some(8.5));
212 assert_eq!(data(&out.nested.weight), vec![9.0]);
213 assert_eq!(out.nested.step, 10);
214 let opt_nested = out.opt_nested.unwrap();
215 assert_eq!(data(&opt_nested.weight), vec![11.0]);
216 assert_eq!(opt_nested.step, 12);
217 }
218
219 #[test]
220 fn absent_optionals_round_trip_to_none() {
221 let state = Full::<1> {
222 t: tensor(&[1.0]),
223 opt_tensor: None,
224 history: vec![],
225 count: 0,
226 opt_scalar: None,
227 nested: Inner {
228 weight: tensor(&[2.0]),
229 step: 0,
230 },
231 opt_nested: None,
232 };
233
234 let out = round_trip(&state).unwrap();
235
236 assert!(out.opt_tensor.is_none());
237 assert!(out.history.is_empty());
238 assert!(out.opt_scalar.is_none());
239 assert!(out.opt_nested.is_none());
240 }
241
242 #[derive(RecordState, Clone, Debug)]
245 struct AllOptional<const D: usize> {
246 x: Option<Tensor<D>>,
247 y: Option<f64>,
248 }
249
250 #[derive(RecordState, Clone, Debug)]
251 struct OuterOpt<const D: usize> {
252 inner: Option<AllOptional<D>>,
253 }
254
255 #[test]
256 fn optional_all_optional_nested_stays_none() {
257 let state = OuterOpt::<1> { inner: None };
258 let out = round_trip(&state).unwrap();
259 assert!(out.inner.is_none());
260 }
261
262 #[test]
263 fn optional_all_optional_nested_present_with_content() {
264 let state = OuterOpt::<1> {
265 inner: Some(AllOptional {
266 x: Some(tensor(&[1.0, 2.0])),
267 y: None,
268 }),
269 };
270 let out = round_trip(&state).unwrap();
271 let inner = out.inner.expect("present because a leaf was recorded");
272 assert_eq!(data(&inner.x.unwrap()), vec![1.0, 2.0]);
273 assert!(inner.y.is_none());
274 }
275
276 #[test]
277 fn missing_required_tensor_yields_none() {
278 let mut source = StateSource::new(BTreeMap::new());
280 assert!(Inner::<1>::state_unflatten("p", &mut source, &Device::default()).is_none());
281 }
282}