use std::path::PathBuf;
use std::time::Instant;
use super::utils::{
ensure_dir, path_exists, print_header, print_info, print_kv, print_success, spinner,
};
use crate::cli::ExportArgs;
use crate::error::{CliError, CliResult};
use axonml_serialize::{Format, StateDict, load_state_dict, save_state_dict};
const SUPPORTED_FORMATS: &[&str] = &["onnx", "torchscript", "safetensors", "tflite", "coreml"];
const SUPPORTED_TARGETS: &[&str] = &["cpu", "cuda", "wasm", "arm", "x86"];
pub fn execute(args: ExportArgs) -> CliResult<()> {
print_header("Model Export");
let model_path = PathBuf::from(&args.model);
if !path_exists(&model_path) {
return Err(CliError::Model(format!(
"Model file not found: {}",
args.model
)));
}
if !SUPPORTED_FORMATS.contains(&args.format.to_lowercase().as_str()) {
return Err(CliError::UnsupportedFormat(format!(
"{}. Supported: {}",
args.format,
SUPPORTED_FORMATS.join(", ")
)));
}
if !SUPPORTED_TARGETS.contains(&args.target.to_lowercase().as_str()) {
return Err(CliError::InvalidArgument(format!(
"Unsupported target: {}. Supported: {}",
args.target,
SUPPORTED_TARGETS.join(", ")
)));
}
print_header("Export Configuration");
print_kv("Model", &args.model);
print_kv("Output", &args.output);
print_kv("Format", &args.format);
print_kv("Target", &args.target);
print_kv("Quantize", &args.quantize.to_string());
if args.quantize {
print_kv("Precision", &args.precision);
}
let output_path = PathBuf::from(&args.output);
if let Some(parent) = output_path.parent() {
ensure_dir(parent)?;
}
println!();
let start_time = Instant::now();
let sp = spinner("Exporting model...");
let result = export_model(&model_path, &args);
sp.finish_and_clear();
let elapsed = start_time.elapsed();
match result {
Ok(info) => {
print_success("Export completed successfully");
println!();
print_header("Export Summary");
print_kv("Output file", &args.output);
print_kv("Output size", &format_size(info.output_size));
print_kv("Parameters", &format_number(info.num_parameters));
if args.quantize {
print_kv("Original precision", "fp32");
print_kv("Quantized precision", &args.precision);
print_kv(
"Size reduction",
&format!("{:.1}%", info.size_reduction * 100.0),
);
}
print_kv("Export time", &format!("{:.2}s", elapsed.as_secs_f64()));
println!();
print_deployment_instructions(&args);
}
Err(e) => {
return Err(CliError::Conversion(e));
}
}
Ok(())
}
struct ExportInfo {
output_size: u64,
num_parameters: u64,
size_reduction: f64,
}
fn export_model(model_path: &PathBuf, args: &ExportArgs) -> Result<ExportInfo, String> {
let input_size = std::fs::metadata(model_path).map_or(0, |m| m.len());
let result = match args.format.to_lowercase().as_str() {
"onnx" => export_to_onnx(
model_path,
&args.output,
&args.target,
args.quantize,
&args.precision,
),
"torchscript" => export_to_torchscript(model_path, &args.output, &args.target),
"safetensors" => export_to_safetensors(model_path, &args.output),
"tflite" => export_to_tflite(model_path, &args.output, args.quantize, &args.precision),
"coreml" => export_to_coreml(model_path, &args.output),
_ => Err(format!("Unsupported export format: {}", args.format)),
};
result.map(|params| {
let output_size = std::fs::metadata(&args.output).map_or(0, |m| m.len());
let size_reduction = if args.quantize && input_size > 0 {
1.0 - (output_size as f64 / input_size as f64)
} else {
0.0
};
ExportInfo {
output_size,
num_parameters: params,
size_reduction,
}
})
}
fn count_parameters(state_dict: &StateDict) -> u64 {
let mut total = 0u64;
for (_, entry) in state_dict.entries() {
let count: u64 = entry.data.shape.iter().map(|&s| s as u64).product();
total += count;
}
total
}
fn export_to_onnx(
model_path: &PathBuf,
output_path: &str,
_target: &str,
_quantize: bool,
_precision: &str,
) -> Result<u64, String> {
let (header, _bundle) = axonml_serialize::load_bundle(model_path).map_err(|e| {
format!(
"ONNX export requires a model saved via `ModelBundle`/`save_bundle` \
(the `.axonml` format with architecture + hyperparameters). \
Got: {e}. If you have a bare StateDict, rebuild the model in code \
and call `save_bundle` instead of `save_state_dict`."
)
})?;
let num_params = header.num_parameters as u64;
let script = std::env::var("AXONML_CONVERTER_SCRIPT")
.ok()
.map(PathBuf::from)
.or_else(|| {
std::env::var("CARGO_MANIFEST_DIR").ok().and_then(|m| {
let candidate = PathBuf::from(m)
.parent()?
.parent()?
.join("tools/model_converter/convert.py");
candidate.exists().then_some(candidate)
})
})
.unwrap_or_else(|| PathBuf::from("/opt/AxonML/tools/model_converter/convert.py"));
if !script.exists() {
return Err(format!(
"ONNX converter script not found at {}. \
Install the AxonML tools directory or set AXONML_CONVERTER_SCRIPT.",
script.display()
));
}
let python = std::env::var("AXONML_CONVERTER_PYTHON").unwrap_or_else(|_| "python3".to_string());
let output = std::process::Command::new(&python)
.arg(&script)
.arg(model_path)
.arg("--format")
.arg("onnx")
.arg("--output")
.arg(output_path)
.output()
.map_err(|e| {
format!(
"Failed to launch `{python} {}`: {e}. \
Install the converter venv (see tools/model_converter/README.md) \
or set AXONML_CONVERTER_PYTHON.",
script.display()
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let last_err = stderr
.lines()
.rev()
.find(|l| {
!l.trim().is_empty()
&& !l.contains("Warning")
&& !l.contains("DeprecationWarning")
&& !l.starts_with(' ')
})
.unwrap_or_else(|| stderr.trim());
return Err(format!(
"ONNX export failed (architecture={}, params={}): {last_err}",
header.architecture, header.num_parameters
));
}
Ok(num_params)
}
fn export_to_torchscript(
model_path: &PathBuf,
output_path: &str,
_target: &str,
) -> Result<u64, String> {
let state_dict =
load_state_dict(model_path).map_err(|e| format!("Failed to load model: {}", e))?;
let num_params = count_parameters(&state_dict);
save_state_dict(&state_dict, output_path, Format::Axonml)
.map_err(|e| format!("Failed to save: {}", e))?;
Ok(num_params)
}
fn export_to_safetensors(model_path: &PathBuf, output_path: &str) -> Result<u64, String> {
let state_dict =
load_state_dict(model_path).map_err(|e| format!("Failed to load model: {}", e))?;
let num_params = count_parameters(&state_dict);
save_state_dict(&state_dict, output_path, Format::SafeTensors)
.map_err(|e| format!("Failed to save SafeTensors: {}", e))?;
Ok(num_params)
}
fn export_to_tflite(
model_path: &PathBuf,
output_path: &str,
_quantize: bool,
_precision: &str,
) -> Result<u64, String> {
let state_dict =
load_state_dict(model_path).map_err(|e| format!("Failed to load model: {}", e))?;
let num_params = count_parameters(&state_dict);
save_state_dict(&state_dict, output_path, Format::Json)
.map_err(|e| format!("Failed to save: {}", e))?;
Ok(num_params)
}
fn export_to_coreml(model_path: &PathBuf, output_path: &str) -> Result<u64, String> {
let state_dict =
load_state_dict(model_path).map_err(|e| format!("Failed to load model: {}", e))?;
let num_params = count_parameters(&state_dict);
ensure_dir(output_path).map_err(|e| e.to_string())?;
let weights_path = PathBuf::from(output_path).join("weights.json");
save_state_dict(&state_dict, &weights_path, Format::Json)
.map_err(|e| format!("Failed to save weights: {}", e))?;
let spec = serde_json::json!({
"format": "coreml_export",
"num_parameters": num_params,
"weights_file": "weights.json",
"note": "Use coremltools to convert to .mlmodel format"
});
let spec_path = PathBuf::from(output_path).join("spec.json");
std::fs::write(&spec_path, serde_json::to_string_pretty(&spec).unwrap())
.map_err(|e| format!("Failed to write spec: {}", e))?;
Ok(num_params)
}
fn print_deployment_instructions(args: &ExportArgs) {
print_header("Deployment Instructions");
match args.format.to_lowercase().as_str() {
"onnx" => {
print_info("ONNX Runtime deployment:");
println!(" Python: onnxruntime.InferenceSession('{}')", args.output);
println!(" Rust: ort::Session::new('{}')", args.output);
if args.target == "cuda" {
println!();
print_info("For GPU inference, use ONNX Runtime with CUDA provider");
}
}
"torchscript" => {
print_info("TorchScript deployment:");
println!(" Python: torch.jit.load('{}')", args.output);
println!(" C++: torch::jit::load('{}')", args.output);
}
"safetensors" => {
print_info("SafeTensors deployment:");
println!(" Python: safetensors.torch.load_file('{}')", args.output);
println!(" Rust: safetensors::deserialize(&file_bytes)");
}
"tflite" => {
print_info("TensorFlow Lite deployment:");
println!(
" Python: tf.lite.Interpreter(model_path='{}')",
args.output
);
println!(" Mobile: Use TFLite runtime SDK");
if args.target == "arm" {
println!();
print_info("Optimized for ARM devices (mobile, Raspberry Pi)");
}
}
"coreml" => {
print_info("Core ML deployment (Apple devices):");
println!(
" Swift: MLModel(contentsOf: URL(fileURLWithPath: '{}'))",
args.output
);
println!(" Xcode: Drag and drop into your project");
}
_ => {}
}
if args.target == "wasm" {
println!();
print_info("WebAssembly deployment:");
println!(" Use wasm-pack to build the inference runtime");
println!(" Load model via JavaScript fetch API");
}
}
fn format_size(bytes: u64) -> String {
if bytes >= 1_073_741_824 {
format!("{:.2} GB", bytes as f64 / 1_073_741_824.0)
} else if bytes >= 1_048_576 {
format!("{:.2} MB", bytes as f64 / 1_048_576.0)
} else if bytes >= 1024 {
format!("{:.2} KB", bytes as f64 / 1024.0)
} else {
format!("{bytes} bytes")
}
}
fn format_number(n: u64) -> String {
if n >= 1_000_000 {
format!("{:.2}M", n as f64 / 1_000_000.0)
} else if n >= 1000 {
format!("{:.2}K", n as f64 / 1000.0)
} else {
format!("{n}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use axonml_serialize::{ModelBundle, save_bundle};
use tempfile::tempdir;
#[test]
fn test_export_to_onnx_rejects_non_bundle() {
let temp = tempdir().unwrap();
let garbage = temp.path().join("not_a_bundle.axonml");
std::fs::write(&garbage, b"not an axonml bundle").unwrap();
let output = temp.path().join("model.onnx");
let err = export_to_onnx(&garbage, output.to_str().unwrap(), "cpu", false, "fp16")
.expect_err("should refuse non-bundle input");
assert!(err.contains("ModelBundle") || err.contains("save_bundle"));
}
#[test]
fn test_export_to_onnx_accepts_bundle() {
let temp = tempdir().unwrap();
let bundle_path = temp.path().join("sentinel.axonml");
let bundle = ModelBundle::new("sentinel", 11, vec![0.0f32; 128])
.with_hyperparam("hidden_dim", 128)
.with_hyperparam("num_layers", 2);
save_bundle(&bundle, &bundle_path).unwrap();
let (header, _) = axonml_serialize::load_bundle(&bundle_path).unwrap();
assert_eq!(header.architecture, "sentinel");
assert_eq!(header.input_features, 11);
}
}