1#![warn(missing_docs)]
26#![warn(clippy::all)]
27#![warn(clippy::pedantic)]
28#![allow(clippy::cast_possible_truncation)]
30#![allow(clippy::cast_sign_loss)]
31#![allow(clippy::cast_precision_loss)]
32#![allow(clippy::cast_possible_wrap)]
33#![allow(clippy::missing_errors_doc)]
34#![allow(clippy::missing_panics_doc)]
35#![allow(clippy::must_use_candidate)]
36#![allow(clippy::module_name_repetitions)]
37#![allow(clippy::similar_names)]
38#![allow(clippy::many_single_char_names)]
39#![allow(clippy::too_many_arguments)]
40#![allow(clippy::doc_markdown)]
41#![allow(clippy::cast_lossless)]
42#![allow(clippy::needless_pass_by_value)]
43#![allow(clippy::redundant_closure_for_method_calls)]
44#![allow(clippy::uninlined_format_args)]
45#![allow(clippy::ptr_arg)]
46#![allow(clippy::return_self_not_must_use)]
47#![allow(clippy::not_unsafe_ptr_arg_deref)]
48#![allow(clippy::items_after_statements)]
49#![allow(clippy::unreadable_literal)]
50#![allow(clippy::if_same_then_else)]
51#![allow(clippy::needless_range_loop)]
52#![allow(clippy::trivially_copy_pass_by_ref)]
53#![allow(clippy::unnecessary_wraps)]
54#![allow(clippy::match_same_arms)]
55#![allow(clippy::unused_self)]
56#![allow(clippy::too_many_lines)]
57#![allow(clippy::single_match_else)]
58#![allow(clippy::fn_params_excessive_bools)]
59#![allow(clippy::struct_excessive_bools)]
60#![allow(clippy::format_push_string)]
61#![allow(clippy::erasing_op)]
62#![allow(clippy::type_repetition_in_bounds)]
63#![allow(clippy::iter_without_into_iter)]
64#![allow(clippy::should_implement_trait)]
65#![allow(clippy::use_debug)]
66#![allow(clippy::case_sensitive_file_extension_comparisons)]
67#![allow(clippy::large_enum_variant)]
68#![allow(clippy::panic)]
69#![allow(clippy::struct_field_names)]
70#![allow(clippy::missing_fields_in_debug)]
71#![allow(clippy::upper_case_acronyms)]
72#![allow(clippy::assigning_clones)]
73#![allow(clippy::option_if_let_else)]
74#![allow(clippy::manual_let_else)]
75#![allow(clippy::explicit_iter_loop)]
76#![allow(clippy::default_trait_access)]
77#![allow(clippy::only_used_in_recursion)]
78#![allow(clippy::manual_clamp)]
79#![allow(clippy::ref_option)]
80#![allow(clippy::multiple_bound_locations)]
81#![allow(clippy::comparison_chain)]
82#![allow(clippy::manual_assert)]
83#![allow(clippy::unnecessary_debug_formatting)]
84
85mod bundle;
90mod checkpoint;
91mod convert;
92mod format;
93mod state_dict;
94
95pub use bundle::{
100 AXONML_BUNDLE_VERSION, AXONML_MAGIC, BundleError, BundleHeader, BundleResult, ModelBundle,
101 load_bundle, load_bundle_from_bytes, load_header, save_bundle,
102};
103pub use checkpoint::{Checkpoint, CheckpointBuilder, TrainingState};
104pub use convert::{
105 OnnxOpType, convert_from_pytorch, from_onnx_shape, from_pytorch_key, pytorch_layer_mapping,
106 to_onnx_shape, to_pytorch_key, transpose_linear_weights,
107};
108pub use format::{Format, detect_format, detect_format_from_bytes};
109pub use state_dict::{StateDict, StateDictEntry, TensorData};
110
111use axonml_core::{Error, Result};
116use axonml_nn::Module;
117use std::fs::File;
118use std::io::{BufReader, BufWriter, Read, Write};
119use std::path::Path;
120
121pub fn save_model<M: Module, P: AsRef<Path>>(model: &M, path: P) -> Result<()> {
129 let path = path.as_ref();
130 let format = detect_format(path);
131 let state_dict = StateDict::from_module(model);
132
133 save_state_dict(&state_dict, path, format)
134}
135
136pub fn load_model<M: Module, P: AsRef<Path>>(model: &M, path: P) -> Result<usize> {
148 let state_dict = load_state_dict(path)?;
149 let named_params = model.named_parameters();
150 let mut loaded = 0;
151
152 for (name, param) in &named_params {
153 if let Some(entry) = state_dict.get(name) {
154 if let Ok(tensor) = entry.data.to_tensor() {
155 if tensor.shape() == param.data().shape() {
156 param.update_data(tensor);
157 loaded += 1;
158 }
159 }
160 }
161 }
162
163 if named_params.is_empty() {
166 let params = model.parameters();
167 let entries: Vec<_> = state_dict.entries().collect();
168 for ((_name, entry), param) in entries.iter().zip(params.iter()) {
169 if let Ok(tensor) = entry.data.to_tensor() {
170 if tensor.shape() == param.data().shape() {
171 param.update_data(tensor);
172 loaded += 1;
173 }
174 }
175 }
176 }
177
178 Ok(loaded)
179}
180
181pub fn save_state_dict<P: AsRef<Path>>(
183 state_dict: &StateDict,
184 path: P,
185 format: Format,
186) -> Result<()> {
187 let path = path.as_ref();
188 let file = File::create(path).map_err(|e| Error::InvalidOperation {
189 message: e.to_string(),
190 })?;
191 let mut writer = BufWriter::new(file);
192
193 match format {
194 Format::Axonml => {
195 let encoded = bincode::serialize(state_dict).map_err(|e| Error::InvalidOperation {
196 message: e.to_string(),
197 })?;
198 writer
199 .write_all(&encoded)
200 .map_err(|e| Error::InvalidOperation {
201 message: e.to_string(),
202 })?;
203 }
204 Format::Json => {
205 serde_json::to_writer_pretty(&mut writer, state_dict).map_err(|e| {
206 Error::InvalidOperation {
207 message: e.to_string(),
208 }
209 })?;
210 }
211 #[cfg(feature = "safetensors")]
212 Format::SafeTensors => {
213 save_safetensors(state_dict, path)?;
214 }
215 #[cfg(not(feature = "safetensors"))]
216 Format::SafeTensors => {
217 return Err(Error::InvalidOperation {
218 message: "SafeTensors format requires 'safetensors' feature".to_string(),
219 });
220 }
221 }
222
223 Ok(())
224}
225
226pub fn load_state_dict<P: AsRef<Path>>(path: P) -> Result<StateDict> {
228 let path = path.as_ref();
229 let format = detect_format(path);
230
231 let file = File::open(path).map_err(|e| Error::InvalidOperation {
232 message: e.to_string(),
233 })?;
234 let mut reader = BufReader::new(file);
235
236 match format {
237 Format::Axonml => {
238 let mut bytes = Vec::new();
239 reader
240 .read_to_end(&mut bytes)
241 .map_err(|e| Error::InvalidOperation {
242 message: e.to_string(),
243 })?;
244 bincode::deserialize(&bytes).map_err(|e| Error::InvalidOperation {
245 message: e.to_string(),
246 })
247 }
248 Format::Json => serde_json::from_reader(reader).map_err(|e| Error::InvalidOperation {
249 message: e.to_string(),
250 }),
251 #[cfg(feature = "safetensors")]
252 Format::SafeTensors => load_safetensors(path),
253 #[cfg(not(feature = "safetensors"))]
254 Format::SafeTensors => Err(Error::InvalidOperation {
255 message: "SafeTensors format requires 'safetensors' feature".to_string(),
256 }),
257 }
258}
259
260pub fn save_checkpoint<P: AsRef<Path>>(checkpoint: &Checkpoint, path: P) -> Result<()> {
262 let path = path.as_ref();
263 let file = File::create(path).map_err(|e| Error::InvalidOperation {
264 message: e.to_string(),
265 })?;
266 let writer = BufWriter::new(file);
267
268 bincode::serialize_into(writer, checkpoint).map_err(|e| Error::InvalidOperation {
269 message: e.to_string(),
270 })
271}
272
273pub fn load_checkpoint<P: AsRef<Path>>(path: P) -> Result<Checkpoint> {
275 let path = path.as_ref();
276 let file = File::open(path).map_err(|e| Error::InvalidOperation {
277 message: e.to_string(),
278 })?;
279 let reader = BufReader::new(file);
280
281 bincode::deserialize_from(reader).map_err(|e| Error::InvalidOperation {
282 message: e.to_string(),
283 })
284}
285
286#[cfg(feature = "safetensors")]
291fn save_safetensors<P: AsRef<Path>>(state_dict: &StateDict, path: P) -> Result<()> {
292 use safetensors::tensor::{Dtype, TensorView};
293 use std::collections::HashMap;
294
295 let mut tensor_data: HashMap<String, Vec<u8>> = HashMap::new();
297 let mut tensor_shapes: HashMap<String, Vec<usize>> = HashMap::new();
298
299 for (name, entry) in state_dict.entries() {
300 let data_bytes: Vec<u8> = entry
301 .data
302 .values
303 .iter()
304 .flat_map(|f| f.to_le_bytes())
305 .collect();
306 tensor_data.insert(name.clone(), data_bytes);
307 tensor_shapes.insert(name.clone(), entry.data.shape.clone());
308 }
309
310 let views: Vec<(String, TensorView<'_>)> = tensor_data
312 .iter()
313 .map(|(name, data)| {
314 let shape = tensor_shapes.get(name).expect("shape missing");
315 (
316 name.clone(),
317 TensorView::new(Dtype::F32, shape.clone(), data).expect("invalid tensor view"),
318 )
319 })
320 .collect();
321
322 let view_refs: Vec<(&str, TensorView<'_>)> = views
323 .iter()
324 .map(|(name, view)| (name.as_str(), view.clone()))
325 .collect();
326
327 let bytes =
328 safetensors::tensor::serialize(&view_refs, &None).map_err(|e| Error::InvalidOperation {
329 message: format!("SafeTensors serialize failed: {e}"),
330 })?;
331
332 std::fs::write(path, bytes).map_err(|e| Error::InvalidOperation {
333 message: e.to_string(),
334 })
335}
336
337#[cfg(feature = "safetensors")]
338fn load_safetensors<P: AsRef<Path>>(path: P) -> Result<StateDict> {
339 let bytes = std::fs::read(path).map_err(|e| Error::InvalidOperation {
340 message: e.to_string(),
341 })?;
342
343 let tensors =
344 safetensors::SafeTensors::deserialize(&bytes).map_err(|e| Error::InvalidOperation {
345 message: e.to_string(),
346 })?;
347
348 let mut state_dict = StateDict::new();
349
350 for (name, tensor) in tensors.tensors() {
351 let data = tensor.data();
352 let shape: Vec<usize> = tensor.shape().to_vec();
353
354 let dtype = tensor.dtype();
356 let values: Vec<f32> = match dtype {
357 safetensors::Dtype::F32 => data
358 .chunks_exact(4)
359 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
360 .collect(),
361 safetensors::Dtype::F16 => data
362 .chunks_exact(2)
363 .map(|c| {
364 let h = half::f16::from_le_bytes([c[0], c[1]]);
365 h.to_f32()
366 })
367 .collect(),
368 safetensors::Dtype::BF16 => data
369 .chunks_exact(2)
370 .map(|c| {
371 let h = half::bf16::from_le_bytes([c[0], c[1]]);
372 h.to_f32()
373 })
374 .collect(),
375 safetensors::Dtype::F64 => data
376 .chunks_exact(8)
377 .map(|c| {
378 let v = f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]);
379 v as f32
380 })
381 .collect(),
382 other => {
383 return Err(Error::InvalidOperation {
384 message: format!(
385 "Unsupported safetensors dtype: {:?} for tensor '{}'",
386 other, name
387 ),
388 });
389 }
390 };
391
392 state_dict.insert(name.to_string(), TensorData { shape, values });
393 }
394
395 Ok(state_dict)
396}
397
398#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_format_detection() {
408 assert_eq!(detect_format("model.axonml"), Format::Axonml);
409 assert_eq!(detect_format("model.json"), Format::Json);
410 assert_eq!(detect_format("model.safetensors"), Format::SafeTensors);
411 assert_eq!(detect_format("model.bin"), Format::Axonml); }
413
414 #[test]
415 fn test_state_dict_creation() {
416 let state_dict = StateDict::new();
417 assert!(state_dict.is_empty());
418 assert_eq!(state_dict.len(), 0);
419 }
420
421 #[test]
422 fn test_state_dict_insert_get() {
423 let mut state_dict = StateDict::new();
424 let data = TensorData {
425 shape: vec![2, 3],
426 values: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
427 };
428
429 state_dict.insert("layer.weight".to_string(), data);
430
431 assert_eq!(state_dict.len(), 1);
432 assert!(state_dict.contains("layer.weight"));
433
434 let retrieved = state_dict.get("layer.weight").unwrap();
435 assert_eq!(retrieved.data.shape, vec![2, 3]);
436 }
437
438 #[test]
439 fn test_tensor_data_to_tensor() {
440 let data = TensorData {
441 shape: vec![2, 2],
442 values: vec![1.0, 2.0, 3.0, 4.0],
443 };
444
445 let tensor = data.to_tensor().unwrap();
446 assert_eq!(tensor.shape(), &[2, 2]);
447 assert_eq!(tensor.to_vec(), vec![1.0, 2.0, 3.0, 4.0]);
448 }
449
450 #[test]
451 fn test_state_dict_file_roundtrip_axonml() {
452 let mut sd = StateDict::new();
453 sd.insert(
454 "layer.weight".to_string(),
455 TensorData {
456 shape: vec![3, 2],
457 values: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
458 },
459 );
460 sd.insert(
461 "layer.bias".to_string(),
462 TensorData {
463 shape: vec![3],
464 values: vec![0.1, 0.2, 0.3],
465 },
466 );
467
468 let path = std::env::temp_dir().join("axonml_test_roundtrip.axonml");
469 save_state_dict(&sd, &path, Format::Axonml).expect("save failed");
470
471 let loaded = load_state_dict(&path).expect("load failed");
472 assert_eq!(loaded.len(), 2);
473 assert!(loaded.contains("layer.weight"));
474 assert!(loaded.contains("layer.bias"));
475
476 let w = loaded.get("layer.weight").unwrap();
477 assert_eq!(w.data.shape, vec![3, 2]);
478 assert_eq!(w.data.values, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
479
480 std::fs::remove_file(&path).ok();
481 }
482
483 #[test]
484 fn test_state_dict_file_roundtrip_json() {
485 let mut sd = StateDict::new();
486 sd.insert(
487 "fc.weight".to_string(),
488 TensorData {
489 shape: vec![2, 2],
490 values: vec![1.5, -2.5, 3.5, -4.5],
491 },
492 );
493
494 let path = std::env::temp_dir().join("axonml_test_roundtrip.json");
495 save_state_dict(&sd, &path, Format::Json).expect("save failed");
496
497 let loaded = load_state_dict(&path).expect("load failed");
498 assert_eq!(loaded.len(), 1);
499 let w = loaded.get("fc.weight").unwrap();
500 assert_eq!(w.data.values, vec![1.5, -2.5, 3.5, -4.5]);
501
502 std::fs::remove_file(&path).ok();
503 }
504
505 #[test]
506 fn test_checkpoint_file_roundtrip() {
507 let mut state = TrainingState::new();
508 state.epoch = 10;
509 state.global_step = 5000;
510 state.record_loss(0.5);
511 state.record_loss(0.3);
512 state.update_best("loss", 0.3, false);
513
514 let checkpoint = Checkpoint::builder()
515 .training_state(state)
516 .epoch(10)
517 .global_step(5000)
518 .config("lr", "0.001")
519 .build();
520
521 let path = std::env::temp_dir().join("axonml_test_checkpoint.axonml");
522 save_checkpoint(&checkpoint, &path).expect("save failed");
523
524 let loaded = load_checkpoint(&path).expect("load failed");
525 assert_eq!(loaded.epoch(), 10);
526 assert_eq!(loaded.global_step(), 5000);
527 assert_eq!(loaded.best_metric(), Some(0.3));
528 assert!(loaded.config.contains_key("lr"));
529 assert!(loaded.timestamp.contains('T')); std::fs::remove_file(&path).ok();
532 }
533
534 #[test]
539 fn test_save_load_model_roundtrip() {
540 use axonml_nn::Linear;
541
542 let model = Linear::new(4, 3);
543 let original_data: Vec<f32> = model
544 .parameters()
545 .iter()
546 .flat_map(|p| p.data().to_vec())
547 .collect();
548
549 let path = std::env::temp_dir().join("axonml_test_model_rt.axonml");
551 save_model(&model, &path).expect("save_model failed");
552
553 let model2 = Linear::new(4, 3);
555 let state_dict = load_state_dict(&path).expect("load failed");
556
557 let params2 = model2.named_parameters();
559 for (name, param) in ¶ms2 {
560 if let Some(entry) = state_dict.get(name) {
561 if let Ok(tensor) = entry.data.to_tensor() {
562 param.update_data(tensor);
563 }
564 }
565 }
566
567 let loaded_data: Vec<f32> = model2
568 .parameters()
569 .iter()
570 .flat_map(|p| p.data().to_vec())
571 .collect();
572
573 assert_eq!(original_data.len(), loaded_data.len());
575 for (a, b) in original_data.iter().zip(loaded_data.iter()) {
576 assert!(
577 (a - b).abs() < 1e-7,
578 "Model weights should survive save/load roundtrip: {} vs {}",
579 a,
580 b
581 );
582 }
583
584 std::fs::remove_file(&path).ok();
585 }
586
587 #[test]
588 fn test_state_dict_from_module() {
589 use axonml_nn::Linear;
590
591 let model = Linear::new(5, 3);
592 let sd = StateDict::from_module(&model);
593
594 assert!(
596 sd.len() >= 2,
597 "Linear should have at least 2 params, got {}",
598 sd.len()
599 );
600
601 for (name, entry) in sd.entries() {
603 let tensor = entry.data.to_tensor().expect("Should reconstruct tensor");
604 assert!(
605 tensor.to_vec().iter().all(|v| v.is_finite()),
606 "All values should be finite for param {}",
607 name
608 );
609 }
610 }
611
612 #[test]
613 fn test_checkpoint_with_model_state_roundtrip() {
614 use axonml_nn::Linear;
615
616 let model = Linear::new(3, 2);
617 let sd = StateDict::from_module(&model);
618
619 let mut state = TrainingState::new();
620 state.epoch = 5;
621 state.record_loss(0.8);
622 state.record_loss(0.5);
623 state.record_val_loss(0.6);
624 state.record_metric("accuracy", 0.92);
625 state.update_best("accuracy", 0.92, true);
626
627 let checkpoint = Checkpoint::builder()
628 .model_state(sd)
629 .training_state(state)
630 .epoch(5)
631 .global_step(1000)
632 .config("model", "linear_3_2")
633 .config("optimizer", "adam")
634 .build();
635
636 let path = std::env::temp_dir().join("axonml_test_full_checkpoint.axonml");
637 save_checkpoint(&checkpoint, &path).expect("save failed");
638
639 let loaded = load_checkpoint(&path).expect("load failed");
640
641 assert_eq!(loaded.epoch(), 5);
643 assert_eq!(loaded.global_step(), 1000);
644 assert_eq!(loaded.best_metric(), Some(0.92));
645 assert_eq!(loaded.training_state.loss_history, vec![0.8, 0.5]);
646 assert_eq!(loaded.training_state.val_loss_history, vec![0.6]);
647 assert_eq!(loaded.config.get("model"), Some(&"linear_3_2".to_string()));
648 assert_eq!(loaded.config.get("optimizer"), Some(&"adam".to_string()));
649
650 assert!(loaded.model_state.len() >= 2);
652
653 std::fs::remove_file(&path).ok();
654 }
655
656 #[test]
657 fn test_training_state_custom_metrics() {
658 let mut state = TrainingState::new();
659 state.record_metric("auc", 0.85);
660 state.record_metric("auc", 0.90);
661 state.record_metric("f1", 0.75);
662
663 assert_eq!(state.custom_metrics.get("auc").unwrap().len(), 2);
664 assert_eq!(state.custom_metrics.get("f1").unwrap().len(), 1);
665
666 assert!(state.update_best("auc", 0.90, true));
667 assert!(!state.update_best("auc", 0.85, true)); assert!(state.update_best("auc", 0.95, true));
669 assert_eq!(state.best_metric, Some(0.95));
670 }
671}