Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1353 lines
44 KiB
Rust
1353 lines
44 KiB
Rust
//! Model storage and checkpoint utilities
|
|
//!
|
|
//! This module provides utilities for storing and loading ML model checkpoints,
|
|
//! weights, and associated metadata with versioning support.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use tracing::{debug, info, warn};
|
|
use uuid::Uuid;
|
|
|
|
use crate::error::{StorageError, StorageResult};
|
|
use crate::Storage;
|
|
|
|
/// Model checkpoint metadata
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ModelCheckpoint {
|
|
/// Unique identifier for this checkpoint
|
|
pub checkpoint_id: Uuid,
|
|
/// Model name/identifier
|
|
pub model_name: String,
|
|
/// Model version (semantic versioning recommended)
|
|
pub model_version: String,
|
|
/// Training epoch or iteration number
|
|
pub epoch: u64,
|
|
/// Training step/batch number
|
|
pub step: u64,
|
|
/// Validation loss at this checkpoint
|
|
pub validation_loss: Option<f64>,
|
|
/// Training loss at this checkpoint
|
|
pub training_loss: Option<f64>,
|
|
/// Model accuracy metrics
|
|
pub accuracy_metrics: HashMap<String, f64>,
|
|
/// Model architecture description
|
|
pub architecture: String,
|
|
/// Hyperparameters used during training
|
|
pub hyperparameters: HashMap<String, serde_json::Value>,
|
|
/// Size of the model in bytes
|
|
pub model_size_bytes: u64,
|
|
/// Compression type used for storage
|
|
pub compression_type: Option<String>,
|
|
/// Timestamp when checkpoint was created
|
|
pub created_at: DateTime<Utc>,
|
|
/// Training duration up to this checkpoint
|
|
pub training_duration: Duration,
|
|
/// Git commit hash (if available)
|
|
pub git_commit: Option<String>,
|
|
/// Custom tags for categorization
|
|
pub tags: HashMap<String, String>,
|
|
/// Storage path for the model data
|
|
pub storage_path: String,
|
|
/// Checksum for integrity verification
|
|
pub checksum: String,
|
|
}
|
|
|
|
impl ModelCheckpoint {
|
|
/// Create a new model checkpoint
|
|
pub fn new(
|
|
model_name: String,
|
|
model_version: String,
|
|
epoch: u64,
|
|
step: u64,
|
|
architecture: String,
|
|
storage_path: String,
|
|
model_size_bytes: u64,
|
|
checksum: String,
|
|
) -> Self {
|
|
Self {
|
|
checkpoint_id: Uuid::new_v4(),
|
|
model_name,
|
|
model_version,
|
|
epoch,
|
|
step,
|
|
validation_loss: None,
|
|
training_loss: None,
|
|
accuracy_metrics: HashMap::new(),
|
|
architecture,
|
|
hyperparameters: HashMap::new(),
|
|
model_size_bytes,
|
|
compression_type: None,
|
|
created_at: Utc::now(),
|
|
training_duration: Duration::from_secs(0),
|
|
git_commit: None,
|
|
tags: HashMap::new(),
|
|
storage_path,
|
|
checksum,
|
|
}
|
|
}
|
|
|
|
/// Set validation and training losses
|
|
pub fn with_losses(mut self, validation_loss: Option<f64>, training_loss: Option<f64>) -> Self {
|
|
self.validation_loss = validation_loss;
|
|
self.training_loss = training_loss;
|
|
self
|
|
}
|
|
|
|
/// Add accuracy metrics
|
|
pub fn with_accuracy_metrics(mut self, metrics: HashMap<String, f64>) -> Self {
|
|
self.accuracy_metrics = metrics;
|
|
self
|
|
}
|
|
|
|
/// Add hyperparameters
|
|
pub fn with_hyperparameters(mut self, hyperparams: HashMap<String, serde_json::Value>) -> Self {
|
|
self.hyperparameters = hyperparams;
|
|
self
|
|
}
|
|
|
|
/// Add training duration
|
|
pub fn with_training_duration(mut self, duration: Duration) -> Self {
|
|
self.training_duration = duration;
|
|
self
|
|
}
|
|
|
|
/// Add git commit hash
|
|
pub fn with_git_commit(mut self, commit: String) -> Self {
|
|
self.git_commit = Some(commit);
|
|
self
|
|
}
|
|
|
|
/// Add custom tags
|
|
pub fn with_tags(mut self, tags: HashMap<String, String>) -> Self {
|
|
self.tags = tags;
|
|
self
|
|
}
|
|
|
|
/// Check if this checkpoint is better than another based on validation loss
|
|
pub fn is_better_than(&self, other: &ModelCheckpoint) -> bool {
|
|
match (self.validation_loss, other.validation_loss) {
|
|
(Some(self_loss), Some(other_loss)) => self_loss < other_loss,
|
|
(Some(_), None) => true, // Has validation loss vs none
|
|
(None, Some(_)) => false, // No validation loss vs has one
|
|
(None, None) => self.step > other.step, // Compare by step if no losses
|
|
}
|
|
}
|
|
|
|
/// Get a human-readable description
|
|
pub fn description(&self) -> String {
|
|
let loss_str = match (self.validation_loss, self.training_loss) {
|
|
(Some(val), Some(train)) => {
|
|
format!(" (val_loss: {:.4}, train_loss: {:.4})", val, train)
|
|
},
|
|
(Some(val), None) => format!(" (val_loss: {:.4})", val),
|
|
(None, Some(train)) => format!(" (train_loss: {:.4})", train),
|
|
(None, None) => String::new(),
|
|
};
|
|
|
|
format!(
|
|
"{} v{} - epoch {} step {}{}",
|
|
self.model_name, self.model_version, self.epoch, self.step, loss_str
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Model storage configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ModelStorageConfig {
|
|
/// Base path for model storage (relative to storage root)
|
|
pub base_path: String,
|
|
/// Enable compression for model data
|
|
pub enable_compression: bool,
|
|
/// Maximum number of checkpoints to retain per model
|
|
pub max_checkpoints_per_model: usize,
|
|
/// Enable automatic cleanup of old checkpoints
|
|
pub enable_auto_cleanup: bool,
|
|
/// Metadata cache size
|
|
pub metadata_cache_size: usize,
|
|
}
|
|
|
|
impl Default for ModelStorageConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
base_path: "models".to_owned(),
|
|
enable_compression: true,
|
|
max_checkpoints_per_model: 10,
|
|
enable_auto_cleanup: true,
|
|
metadata_cache_size: 100,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Model storage and management utilities
|
|
pub struct ModelStorage<S: Storage> {
|
|
storage: S,
|
|
config: ModelStorageConfig,
|
|
/// Metadata cache for faster lookups
|
|
metadata_cache: std::sync::Arc<std::sync::Mutex<lru::LruCache<String, ModelCheckpoint>>>,
|
|
}
|
|
|
|
impl<S: Storage> ModelStorage<S> {
|
|
/// Create a new model storage instance
|
|
pub fn new(storage: S, config: ModelStorageConfig) -> Self {
|
|
// SAFETY: 100 is always non-zero, so this is safe
|
|
let default_cache_size = unsafe { std::num::NonZeroUsize::new_unchecked(100) };
|
|
let cache_size =
|
|
std::num::NonZeroUsize::new(config.metadata_cache_size).unwrap_or(default_cache_size);
|
|
let metadata_cache =
|
|
std::sync::Arc::new(std::sync::Mutex::new(lru::LruCache::new(cache_size)));
|
|
|
|
Self {
|
|
storage,
|
|
config,
|
|
metadata_cache,
|
|
}
|
|
}
|
|
|
|
/// Store a model checkpoint
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails
|
|
pub async fn store_checkpoint(
|
|
&self,
|
|
checkpoint: &ModelCheckpoint,
|
|
model_data: &[u8],
|
|
) -> StorageResult<()> {
|
|
let start = std::time::Instant::now();
|
|
|
|
info!(
|
|
"Storing model checkpoint: {} ({} bytes)",
|
|
checkpoint.description(),
|
|
model_data.len()
|
|
);
|
|
|
|
// Store the model data
|
|
let model_path = format!("{}/{}", self.config.base_path, checkpoint.storage_path);
|
|
self.storage.store(&model_path, model_data).await?;
|
|
|
|
// Store the metadata
|
|
let metadata_path = format!("{}.metadata.json", model_path);
|
|
let metadata_json = serde_json::to_vec_pretty(checkpoint).map_err(|e| {
|
|
StorageError::SerializationError {
|
|
message: format!("Failed to serialize checkpoint metadata: {}", e),
|
|
}
|
|
})?;
|
|
|
|
self.storage.store(&metadata_path, &metadata_json).await?;
|
|
|
|
// Cache the metadata
|
|
let cache_key = format!("{}:{}", checkpoint.model_name, checkpoint.checkpoint_id);
|
|
if let Ok(mut cache) = self.metadata_cache.lock() {
|
|
cache.put(cache_key, checkpoint.clone());
|
|
}
|
|
|
|
// Cleanup old checkpoints if enabled
|
|
if self.config.enable_auto_cleanup {
|
|
self.cleanup_old_checkpoints(&checkpoint.model_name).await?;
|
|
}
|
|
|
|
let duration = start.elapsed();
|
|
crate::metrics::get_metrics().record_operation("store_checkpoint", "model", duration, true);
|
|
crate::metrics::get_metrics().record_transfer(
|
|
"store_checkpoint",
|
|
"model",
|
|
model_data.len() as u64,
|
|
duration,
|
|
);
|
|
|
|
info!(
|
|
"Successfully stored model checkpoint: {} in {:?}",
|
|
checkpoint.description(),
|
|
duration
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Load a model checkpoint by ID
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails or checkpoint not found
|
|
pub async fn load_checkpoint(
|
|
&self,
|
|
checkpoint_id: Uuid,
|
|
) -> StorageResult<(ModelCheckpoint, Vec<u8>)> {
|
|
let start = std::time::Instant::now();
|
|
|
|
debug!("Loading model checkpoint: {}", checkpoint_id);
|
|
|
|
// Try to find the checkpoint metadata first
|
|
let checkpoint = self.find_checkpoint_by_id(checkpoint_id).await?;
|
|
|
|
// Load the model data
|
|
let model_path = format!("{}/{}", self.config.base_path, checkpoint.storage_path);
|
|
let model_data = self.storage.retrieve(&model_path).await?;
|
|
|
|
// Verify checksum if available
|
|
if !checkpoint.checksum.is_empty() {
|
|
let calculated_checksum = Self::calculate_checksum(&model_data);
|
|
if calculated_checksum != checkpoint.checksum {
|
|
return Err(StorageError::IntegrityError {
|
|
path: model_path,
|
|
expected: checkpoint.checksum.clone(),
|
|
actual: calculated_checksum,
|
|
});
|
|
}
|
|
}
|
|
|
|
let duration = start.elapsed();
|
|
crate::metrics::get_metrics().record_operation("load_checkpoint", "model", duration, true);
|
|
crate::metrics::get_metrics().record_transfer(
|
|
"load_checkpoint",
|
|
"model",
|
|
model_data.len() as u64,
|
|
duration,
|
|
);
|
|
|
|
info!(
|
|
"Successfully loaded model checkpoint: {} ({} bytes) in {:?}",
|
|
checkpoint.description(),
|
|
model_data.len(),
|
|
duration
|
|
);
|
|
|
|
Ok((checkpoint, model_data))
|
|
}
|
|
|
|
/// Load the latest checkpoint for a model
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails or no checkpoints exist
|
|
pub async fn load_latest_checkpoint(
|
|
&self,
|
|
model_name: &str,
|
|
) -> StorageResult<(ModelCheckpoint, Vec<u8>)> {
|
|
debug!("Loading latest checkpoint for model: {}", model_name);
|
|
|
|
let checkpoints = self.list_checkpoints(model_name).await?;
|
|
if checkpoints.is_empty() {
|
|
return Err(StorageError::NotFound {
|
|
path: format!("model:{}", model_name),
|
|
});
|
|
}
|
|
|
|
// Find the best checkpoint (highest step number, or best validation loss)
|
|
let latest = checkpoints
|
|
.into_iter()
|
|
.max_by(|a, b| {
|
|
// First compare by step number
|
|
match a.step.cmp(&b.step) {
|
|
std::cmp::Ordering::Equal => {
|
|
// If steps are equal, compare by validation loss (lower is better)
|
|
match (a.validation_loss, b.validation_loss) {
|
|
(Some(a_loss), Some(b_loss)) => b_loss
|
|
.partial_cmp(&a_loss)
|
|
.unwrap_or(std::cmp::Ordering::Equal),
|
|
(Some(_), None) => std::cmp::Ordering::Greater,
|
|
(None, Some(_)) => std::cmp::Ordering::Less,
|
|
(None, None) => std::cmp::Ordering::Equal,
|
|
}
|
|
},
|
|
other => other,
|
|
}
|
|
})
|
|
.ok_or_else(|| StorageError::NotFound {
|
|
path: format!("model:{}", model_name),
|
|
})?;
|
|
|
|
self.load_checkpoint(latest.checkpoint_id).await
|
|
}
|
|
|
|
/// List all checkpoints for a model
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails
|
|
pub async fn list_checkpoints(&self, model_name: &str) -> StorageResult<Vec<ModelCheckpoint>> {
|
|
debug!("Listing checkpoints for model: {}", model_name);
|
|
|
|
let model_prefix = format!("{}/{}", self.config.base_path, model_name);
|
|
let paths = self.storage.list(&model_prefix).await?;
|
|
|
|
let mut checkpoints = Vec::new();
|
|
|
|
for path in paths {
|
|
if path.ends_with(".metadata.json") {
|
|
if let Ok(metadata) = self.load_checkpoint_metadata(&path).await {
|
|
if metadata.model_name == model_name {
|
|
checkpoints.push(metadata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort by step number (latest first)
|
|
checkpoints.sort_by(|a, b| b.step.cmp(&a.step));
|
|
|
|
debug!(
|
|
"Found {} checkpoints for model: {}",
|
|
checkpoints.len(),
|
|
model_name
|
|
);
|
|
Ok(checkpoints)
|
|
}
|
|
|
|
/// Delete a checkpoint
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails
|
|
pub async fn delete_checkpoint(&self, checkpoint_id: Uuid) -> StorageResult<bool> {
|
|
debug!("Deleting checkpoint: {}", checkpoint_id);
|
|
|
|
let checkpoint = self.find_checkpoint_by_id(checkpoint_id).await?;
|
|
|
|
// Delete model data
|
|
let model_path = format!("{}/{}", self.config.base_path, checkpoint.storage_path);
|
|
let model_deleted = self.storage.delete(&model_path).await.unwrap_or(false);
|
|
|
|
// Delete metadata
|
|
let metadata_path = format!("{}.metadata.json", model_path);
|
|
let metadata_deleted = self.storage.delete(&metadata_path).await.unwrap_or(false);
|
|
|
|
// Remove from cache
|
|
let cache_key = format!("{}:{}", checkpoint.model_name, checkpoint.checkpoint_id);
|
|
if let Ok(mut cache) = self.metadata_cache.lock() {
|
|
cache.pop(&cache_key);
|
|
}
|
|
|
|
let deleted = model_deleted || metadata_deleted;
|
|
if deleted {
|
|
info!(
|
|
"Successfully deleted checkpoint: {}",
|
|
checkpoint.description()
|
|
);
|
|
}
|
|
|
|
Ok(deleted)
|
|
}
|
|
|
|
/// List all models
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails
|
|
pub async fn list_models(&self) -> StorageResult<Vec<String>> {
|
|
let paths = self.storage.list(&self.config.base_path).await?;
|
|
|
|
let mut models = std::collections::HashSet::new();
|
|
for path in paths {
|
|
if path.ends_with(".metadata.json") {
|
|
if let Ok(metadata) = self.load_checkpoint_metadata(&path).await {
|
|
models.insert(metadata.model_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut model_list: Vec<String> = models.into_iter().collect();
|
|
model_list.sort();
|
|
|
|
debug!("Found {} models", model_list.len());
|
|
Ok(model_list)
|
|
}
|
|
|
|
/// Get storage statistics for models
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `StorageError` if storage operation fails
|
|
pub async fn get_storage_stats(&self) -> StorageResult<ModelStorageStats> {
|
|
let paths = self.storage.list(&self.config.base_path).await?;
|
|
|
|
let mut total_checkpoints = 0;
|
|
let mut total_size_bytes = 0u64;
|
|
let mut models_by_name: HashMap<String, u32> = HashMap::new();
|
|
let mut size_by_model: HashMap<String, u64> = HashMap::new();
|
|
|
|
for path in paths {
|
|
if path.ends_with(".metadata.json") {
|
|
if let Ok(metadata) = self.load_checkpoint_metadata(&path).await {
|
|
total_checkpoints += 1;
|
|
total_size_bytes += metadata.model_size_bytes;
|
|
|
|
*models_by_name
|
|
.entry(metadata.model_name.clone())
|
|
.or_insert(0) += 1;
|
|
*size_by_model.entry(metadata.model_name).or_insert(0) +=
|
|
metadata.model_size_bytes;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(ModelStorageStats {
|
|
total_checkpoints,
|
|
total_size_bytes,
|
|
total_models: models_by_name.len() as u32,
|
|
checkpoints_by_model: models_by_name,
|
|
size_by_model,
|
|
})
|
|
}
|
|
|
|
/// Private helper methods
|
|
/// Find a checkpoint by ID
|
|
async fn find_checkpoint_by_id(&self, checkpoint_id: Uuid) -> StorageResult<ModelCheckpoint> {
|
|
// Check cache first
|
|
let cache_key_pattern = format!(":{}", checkpoint_id);
|
|
if let Ok(cache) = self.metadata_cache.lock() {
|
|
for (key, checkpoint) in cache.iter() {
|
|
if key.ends_with(&cache_key_pattern) {
|
|
return Ok(checkpoint.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Search in storage
|
|
let paths = self.storage.list(&self.config.base_path).await?;
|
|
|
|
for path in paths {
|
|
if path.ends_with(".metadata.json") {
|
|
if let Ok(metadata) = self.load_checkpoint_metadata(&path).await {
|
|
if metadata.checkpoint_id == checkpoint_id {
|
|
// Cache for future use
|
|
let cache_key = format!("{}:{}", metadata.model_name, checkpoint_id);
|
|
if let Ok(mut cache) = self.metadata_cache.lock() {
|
|
cache.put(cache_key, metadata.clone());
|
|
}
|
|
return Ok(metadata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(StorageError::NotFound {
|
|
path: format!("checkpoint:{}", checkpoint_id),
|
|
})
|
|
}
|
|
|
|
/// Load checkpoint metadata from a path
|
|
async fn load_checkpoint_metadata(
|
|
&self,
|
|
metadata_path: &str,
|
|
) -> StorageResult<ModelCheckpoint> {
|
|
let full_path = if metadata_path.starts_with(&self.config.base_path) {
|
|
metadata_path.to_string()
|
|
} else {
|
|
format!("{}/{}", self.config.base_path, metadata_path)
|
|
};
|
|
|
|
let metadata_json = self.storage.retrieve(&full_path).await?;
|
|
let checkpoint: ModelCheckpoint = serde_json::from_slice(&metadata_json).map_err(|e| {
|
|
StorageError::SerializationError {
|
|
message: format!("Failed to deserialize checkpoint metadata: {}", e),
|
|
}
|
|
})?;
|
|
|
|
Ok(checkpoint)
|
|
}
|
|
|
|
/// Cleanup old checkpoints for a model
|
|
async fn cleanup_old_checkpoints(&self, model_name: &str) -> StorageResult<()> {
|
|
if self.config.max_checkpoints_per_model == 0 {
|
|
return Ok(()); // No limit
|
|
}
|
|
|
|
let mut checkpoints = self.list_checkpoints(model_name).await?;
|
|
if checkpoints.len() <= self.config.max_checkpoints_per_model {
|
|
return Ok(()); // Within limit
|
|
}
|
|
|
|
// Sort by step (keep latest)
|
|
checkpoints.sort_by(|a, b| b.step.cmp(&a.step));
|
|
|
|
// Delete excess checkpoints
|
|
let to_delete = checkpoints.split_off(self.config.max_checkpoints_per_model);
|
|
|
|
for checkpoint in to_delete {
|
|
if let Err(e) = self.delete_checkpoint(checkpoint.checkpoint_id).await {
|
|
warn!(
|
|
"Failed to delete old checkpoint {}: {}",
|
|
checkpoint.checkpoint_id, e
|
|
);
|
|
} else {
|
|
debug!("Cleaned up old checkpoint: {}", checkpoint.description());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Calculate checksum for model data
|
|
fn calculate_checksum(data: &[u8]) -> String {
|
|
use sha2::{Digest, Sha256};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(data);
|
|
format!("{:x}", hasher.finalize())
|
|
}
|
|
}
|
|
|
|
/// Storage statistics for models
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ModelStorageStats {
|
|
/// Total number of checkpoints
|
|
pub total_checkpoints: u32,
|
|
/// Total size in bytes
|
|
pub total_size_bytes: u64,
|
|
/// Total number of unique models
|
|
pub total_models: u32,
|
|
/// Number of checkpoints per model
|
|
pub checkpoints_by_model: HashMap<String, u32>,
|
|
/// Storage size per model
|
|
pub size_by_model: HashMap<String, u64>,
|
|
}
|
|
|
|
/// Model loading utilities for common formats
|
|
pub struct ModelLoader;
|
|
|
|
impl ModelLoader {
|
|
/// Load a PyTorch model checkpoint (Python pickled format)
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if PyTorch checkpoint parsing fails
|
|
pub async fn load_pytorch_checkpoint(_data: &[u8]) -> Result<HashMap<String, Vec<u8>>> {
|
|
// Note: This would typically require Python integration or a Rust-based
|
|
// PyTorch checkpoint reader. For now, this is a placeholder.
|
|
warn!("PyTorch checkpoint loading not implemented - would require Python integration");
|
|
Ok(HashMap::new())
|
|
}
|
|
|
|
/// Load ONNX model format
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if ONNX model validation fails
|
|
pub async fn load_onnx_model(data: &[u8]) -> Result<Vec<u8>> {
|
|
// ONNX models are protobuf-based and could be parsed with prost
|
|
warn!("ONNX model loading not implemented - would require protobuf parsing");
|
|
Ok(data.to_vec())
|
|
}
|
|
|
|
/// Load TensorFlow SavedModel format
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if TensorFlow model validation fails
|
|
pub async fn load_tensorflow_model(data: &[u8]) -> Result<Vec<u8>> {
|
|
// TensorFlow SavedModel is a directory structure with protobuf files
|
|
warn!("TensorFlow model loading not implemented - would require protobuf parsing");
|
|
Ok(data.to_vec())
|
|
}
|
|
|
|
/// Load generic binary model data
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns error if binary model validation fails
|
|
pub async fn load_binary_model(data: &[u8]) -> Result<Vec<u8>> {
|
|
Ok(data.to_vec())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::local::{LocalStorage, LocalStorageConfig};
|
|
use tempfile::TempDir;
|
|
|
|
///
|
|
/// # Errors
|
|
/// Returns error if the operation fails
|
|
async fn create_test_model_storage() -> (ModelStorage<LocalStorage>, TempDir) {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let storage_config = LocalStorageConfig {
|
|
base_path: temp_dir.path().to_path_buf(),
|
|
..Default::default()
|
|
};
|
|
let storage = LocalStorage::new(storage_config).await.unwrap();
|
|
let model_config = ModelStorageConfig::default();
|
|
let model_storage = ModelStorage::new(storage, model_config);
|
|
(model_storage, temp_dir)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_store_and_load_checkpoint() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"fake model data";
|
|
let checksum = "c4928585ac684a63148634c0655c561d94260f841aceb618ef21b6492e8a1da8";
|
|
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
10,
|
|
1000,
|
|
"transformer".to_owned(),
|
|
"test_model/checkpoint_1000.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
checksum.to_string(),
|
|
);
|
|
|
|
// Store checkpoint
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Load checkpoint
|
|
let (loaded_checkpoint, loaded_data) = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(loaded_checkpoint.checkpoint_id, checkpoint.checkpoint_id);
|
|
assert_eq!(loaded_checkpoint.model_name, checkpoint.model_name);
|
|
assert_eq!(loaded_data, model_data);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_list_checkpoints() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"fake model data";
|
|
|
|
// Store multiple checkpoints
|
|
for i in 1..=3 {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
i * 100,
|
|
"transformer".to_owned(),
|
|
format!("test_model/checkpoint_{}.bin", i * 100),
|
|
model_data.len() as u64,
|
|
format!("checksum_{}", i),
|
|
);
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let checkpoints = model_storage.list_checkpoints("test_model").await.unwrap();
|
|
assert_eq!(checkpoints.len(), 3);
|
|
|
|
// Should be sorted by step (latest first)
|
|
assert_eq!(checkpoints[0].step, 300);
|
|
assert_eq!(checkpoints[1].step, 200);
|
|
assert_eq!(checkpoints[2].step, 100);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_load_latest_checkpoint() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"fake model data";
|
|
let mut latest_checkpoint_id = Uuid::new_v4();
|
|
|
|
// Store multiple checkpoints
|
|
for i in 1..=3 {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
i * 100,
|
|
"transformer".to_owned(),
|
|
format!("test_model/checkpoint_{}.bin", i * 100),
|
|
model_data.len() as u64,
|
|
"c4928585ac684a63148634c0655c561d94260f841aceb618ef21b6492e8a1da8".to_owned(),
|
|
);
|
|
|
|
if i == 3 {
|
|
latest_checkpoint_id = checkpoint.checkpoint_id;
|
|
}
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let (latest_checkpoint, _) = model_storage
|
|
.load_latest_checkpoint("test_model")
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(latest_checkpoint.checkpoint_id, latest_checkpoint_id);
|
|
assert_eq!(latest_checkpoint.step, 300);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_checkpoint() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"fake model data";
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
10,
|
|
1000,
|
|
"transformer".to_owned(),
|
|
"test_model/checkpoint_1000.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
"checksum".to_owned(),
|
|
);
|
|
|
|
// Store and then delete
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
let deleted = model_storage
|
|
.delete_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(deleted);
|
|
|
|
// Should not be able to load deleted checkpoint
|
|
let result = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await;
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_comparison() {
|
|
let checkpoint1 = ModelCheckpoint::new(
|
|
"test".to_owned(),
|
|
"1.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
)
|
|
.with_losses(Some(0.5), Some(0.6));
|
|
|
|
let checkpoint2 = ModelCheckpoint::new(
|
|
"test".to_owned(),
|
|
"1.0".to_owned(),
|
|
1,
|
|
200,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
)
|
|
.with_losses(Some(0.4), Some(0.5));
|
|
|
|
// checkpoint2 has lower validation loss, so it's better
|
|
assert!(checkpoint2.is_better_than(&checkpoint1));
|
|
assert!(!checkpoint1.is_better_than(&checkpoint2));
|
|
}
|
|
|
|
// CHECKSUM AND INTEGRITY TESTS
|
|
|
|
#[tokio::test]
|
|
async fn test_checksum_verification_success() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"test model data";
|
|
|
|
// Calculate correct checksum
|
|
use sha2::{Digest, Sha256};
|
|
let mut hasher = Sha256::new();
|
|
hasher.update(model_data);
|
|
let correct_checksum = format!("{:x}", hasher.finalize());
|
|
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"test_model/checkpoint.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
correct_checksum,
|
|
);
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should load successfully with matching checksum
|
|
let (loaded, _) = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(loaded.checkpoint_id, checkpoint.checkpoint_id);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checksum_verification_failure() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"test model data";
|
|
let wrong_checksum = "wrong_checksum_hash";
|
|
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"test_model/checkpoint.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
wrong_checksum.to_string(),
|
|
);
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should fail with integrity error
|
|
let result = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert!(matches!(result, Err(StorageError::IntegrityError { .. })));
|
|
}
|
|
|
|
// MODEL VERSIONING TESTS
|
|
|
|
#[tokio::test]
|
|
async fn test_multiple_model_versions() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"model data";
|
|
|
|
// Store multiple versions
|
|
for version in &["1.0.0", "1.0.1", "1.1.0", "2.0.0"] {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"versioned_model".to_owned(),
|
|
version.to_string(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
format!("versioned_model/v{}.bin", version),
|
|
model_data.len() as u64,
|
|
format!("hash_{}", version),
|
|
);
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let checkpoints = model_storage
|
|
.list_checkpoints("versioned_model")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(checkpoints.len(), 4);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_with_metadata() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"model data";
|
|
let mut hyperparams = std::collections::HashMap::new();
|
|
hyperparams.insert("learning_rate".to_owned(), serde_json::json!(0.001));
|
|
hyperparams.insert("batch_size".to_owned(), serde_json::json!(32));
|
|
|
|
let mut metrics = std::collections::HashMap::new();
|
|
metrics.insert("accuracy".to_owned(), 0.95);
|
|
metrics.insert("f1_score".to_owned(), 0.92);
|
|
|
|
let mut tags = std::collections::HashMap::new();
|
|
tags.insert("experiment".to_owned(), "baseline".to_owned());
|
|
tags.insert("dataset".to_owned(), "v1".to_owned());
|
|
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"rich_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
10,
|
|
1000,
|
|
"transformer".to_owned(),
|
|
"rich_model/checkpoint.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
"6dbdb6a147ad4d808455652bf5a10120161678395f6bfbd21eb6fe4e731aceeb".to_owned(),
|
|
)
|
|
.with_hyperparameters(hyperparams)
|
|
.with_accuracy_metrics(metrics)
|
|
.with_tags(tags)
|
|
.with_losses(Some(0.05), Some(0.08))
|
|
.with_training_duration(std::time::Duration::from_secs(3600))
|
|
.with_git_commit("abc123def456".to_owned());
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
|
|
let (loaded, _) = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(loaded.hyperparameters.len(), 2);
|
|
assert_eq!(loaded.accuracy_metrics.len(), 2);
|
|
assert_eq!(loaded.tags.len(), 2);
|
|
assert_eq!(loaded.git_commit, Some("abc123def456".to_owned()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_auto_cleanup() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let storage_config = LocalStorageConfig {
|
|
base_path: temp_dir.path().to_path_buf(),
|
|
..Default::default()
|
|
};
|
|
let storage = LocalStorage::new(storage_config).await.unwrap();
|
|
|
|
let model_config = ModelStorageConfig {
|
|
max_checkpoints_per_model: 3,
|
|
enable_auto_cleanup: true,
|
|
..Default::default()
|
|
};
|
|
let model_storage = ModelStorage::new(storage, model_config);
|
|
|
|
let model_data = b"model data";
|
|
|
|
// Store 5 checkpoints (should keep only 3 latest)
|
|
for i in 1..=5 {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"cleanup_test".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
i * 100,
|
|
"arch".to_owned(),
|
|
format!("cleanup_test/checkpoint_{}.bin", i),
|
|
model_data.len() as u64,
|
|
format!("hash_{}", i),
|
|
);
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
// Should have only 3 checkpoints (latest ones)
|
|
let checkpoints = model_storage
|
|
.list_checkpoints("cleanup_test")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(checkpoints.len(), 3);
|
|
assert_eq!(checkpoints[0].step, 500);
|
|
assert_eq!(checkpoints[1].step, 400);
|
|
assert_eq!(checkpoints[2].step, 300);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_no_cleanup() {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let storage_config = LocalStorageConfig {
|
|
base_path: temp_dir.path().to_path_buf(),
|
|
..Default::default()
|
|
};
|
|
let storage = LocalStorage::new(storage_config).await.unwrap();
|
|
|
|
let model_config = ModelStorageConfig {
|
|
max_checkpoints_per_model: 3,
|
|
enable_auto_cleanup: false, // Disabled
|
|
..Default::default()
|
|
};
|
|
let model_storage = ModelStorage::new(storage, model_config);
|
|
|
|
let model_data = b"model data";
|
|
|
|
// Store 5 checkpoints (should keep all)
|
|
for i in 1..=5 {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"no_cleanup_test".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
i * 100,
|
|
"arch".to_owned(),
|
|
format!("no_cleanup_test/checkpoint_{}.bin", i),
|
|
model_data.len() as u64,
|
|
format!("hash_{}", i),
|
|
);
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
// Should have all 5 checkpoints
|
|
let checkpoints = model_storage
|
|
.list_checkpoints("no_cleanup_test")
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(checkpoints.len(), 5);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_list_models() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"data";
|
|
|
|
// Store checkpoints for multiple models
|
|
for model_name in &["model_a", "model_b", "model_c"] {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
model_name.to_string(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
format!("{}/checkpoint.bin", model_name),
|
|
model_data.len() as u64,
|
|
"3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7".to_owned(),
|
|
);
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let models = model_storage.list_models().await.unwrap();
|
|
assert_eq!(models.len(), 3);
|
|
assert!(models.contains(&"model_a".to_owned()));
|
|
assert!(models.contains(&"model_b".to_owned()));
|
|
assert!(models.contains(&"model_c".to_owned()));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_storage_stats() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"test data with some content";
|
|
|
|
// Store multiple checkpoints across models
|
|
for model_num in 1..=3 {
|
|
for checkpoint_num in 1..=2 {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
format!("model_{}", model_num),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
checkpoint_num * 100,
|
|
"arch".to_owned(),
|
|
format!("model_{}/checkpoint_{}.bin", model_num, checkpoint_num),
|
|
model_data.len() as u64,
|
|
"58c7782f0bc82df754cadee10fd1cb82c80fb8103a0b5c7986a221751f67f188".to_owned(),
|
|
);
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
}
|
|
|
|
let stats = model_storage.get_storage_stats().await.unwrap();
|
|
assert_eq!(stats.total_checkpoints, 6);
|
|
assert_eq!(stats.total_models, 3);
|
|
assert!(stats.total_size_bytes > 0);
|
|
assert_eq!(stats.checkpoints_by_model.len(), 3);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_description() {
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"test_model".to_owned(),
|
|
"2.1.0".to_owned(),
|
|
5,
|
|
1234,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
)
|
|
.with_losses(Some(0.123), Some(0.234));
|
|
|
|
let desc = checkpoint.description();
|
|
assert!(desc.contains("test_model"));
|
|
assert!(desc.contains("2.1.0"));
|
|
assert!(desc.contains("epoch 5"));
|
|
assert!(desc.contains("step 1234"));
|
|
assert!(desc.contains("0.123"));
|
|
assert!(desc.contains("0.234"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_comparison_no_losses() {
|
|
let checkpoint1 = ModelCheckpoint::new(
|
|
"test".to_owned(),
|
|
"1.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
);
|
|
|
|
let checkpoint2 = ModelCheckpoint::new(
|
|
"test".to_owned(),
|
|
"1.0".to_owned(),
|
|
1,
|
|
200,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
);
|
|
|
|
// checkpoint2 has higher step, so it's "better"
|
|
assert!(checkpoint2.is_better_than(&checkpoint1));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_checkpoint_comparison_mixed_losses() {
|
|
let checkpoint_with_loss = ModelCheckpoint::new(
|
|
"test".to_owned(),
|
|
"1.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
)
|
|
.with_losses(Some(0.5), None);
|
|
|
|
let checkpoint_without_loss = ModelCheckpoint::new(
|
|
"test".to_owned(),
|
|
"1.0".to_owned(),
|
|
1,
|
|
200,
|
|
"arch".to_owned(),
|
|
"path".to_owned(),
|
|
1000,
|
|
"hash".to_owned(),
|
|
);
|
|
|
|
// Checkpoint with loss is "better" than one without
|
|
assert!(checkpoint_with_loss.is_better_than(&checkpoint_without_loss));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_load_nonexistent_checkpoint() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let fake_id = Uuid::new_v4();
|
|
let result = model_storage.load_checkpoint(fake_id).await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(matches!(result, Err(StorageError::NotFound { .. })));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_load_latest_no_checkpoints() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let result = model_storage
|
|
.load_latest_checkpoint("nonexistent_model")
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
assert!(matches!(result, Err(StorageError::NotFound { .. })));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_metadata_cache() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"cached data";
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"cached_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"cached_model/checkpoint.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
"005990c21ec5a6959371ee4beb18a79237edee42873b6691c45d6907eb496ef5".to_owned(),
|
|
);
|
|
|
|
// Store checkpoint (should cache metadata)
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Load multiple times (should hit cache)
|
|
for _ in 0..3 {
|
|
let (loaded, _) = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(loaded.checkpoint_id, checkpoint.checkpoint_id);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_empty_checksum_skips_verification() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
let model_data = b"test data";
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"no_checksum_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"no_checksum_model/checkpoint.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
String::new(), // Empty checksum
|
|
);
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, model_data)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should load successfully even though checksum is empty
|
|
let (loaded, _) = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(loaded.checkpoint_id, checkpoint.checkpoint_id);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_large_model_checkpoint() {
|
|
let (model_storage, _temp_dir) = create_test_model_storage().await;
|
|
|
|
// Simulate a large model (10MB)
|
|
let model_data = vec![0u8; 10 * 1024 * 1024];
|
|
let checkpoint = ModelCheckpoint::new(
|
|
"large_model".to_owned(),
|
|
"1.0.0".to_owned(),
|
|
1,
|
|
100,
|
|
"arch".to_owned(),
|
|
"large_model/checkpoint.bin".to_owned(),
|
|
model_data.len() as u64,
|
|
"e5b844cc57f57094ea4585e235f36c78c1cd222262bb89d53c94dcb4d6b3e55d".to_owned(),
|
|
);
|
|
|
|
model_storage
|
|
.store_checkpoint(&checkpoint, &model_data)
|
|
.await
|
|
.unwrap();
|
|
let (_, loaded_data) = model_storage
|
|
.load_checkpoint(checkpoint.checkpoint_id)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(loaded_data.len(), model_data.len());
|
|
}
|
|
}
|