Eliminated 7 downloads:
- dqn.rs dead neuron: GPU abs→le→sum (test-only, marked #[cold])
- ppo.rs compute_losses: GPU gather_rows kernel for per-action log-prob
(eliminated 3 full-batch downloads)
- ppo.rs update_gpu: GPU gather_rows + GpuTensor::symlog()
(sign(x)*ln(|x|+1) via 6 elementwise GPU kernels)
- Test assertions: annotated with // test-only readback
Marked #[cold] + annotated 8 checkpoint/API methods:
- CudaLinear::get_weights(), CudaVec::to_vec(), GpuTensor::to_host(),
GpuVarStore::{all_vars,flatten,export_to_host},
GpuLinear::{weight_to_vec,bias_to_vec}
New GPU infrastructure:
- ElementwiseKernels: gather_rows + gather_rows_u32 CUDA kernels
- GpuTensor::symlog() — fully GPU-native sign*log transform
Every remaining memcpy_dtoh is annotated: // gpu-exit: or // test-only readback
Verification: `rg "memcpy_dtoh" | grep -v "gpu-exit\|test.*readback"` = 0
1,116 tests pass across 5 sub-crates. Zero failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
5.5 KiB
Rust
159 lines
5.5 KiB
Rust
//! Safetensors save/load for `GpuVarStore`.
|
|
//!
|
|
//! Uses the `safetensors` crate directly (not candle's wrapper) to serialize
|
|
//! GPU parameters to disk and reload them.
|
|
//!
|
|
//! ## Format
|
|
//!
|
|
//! Each parameter is stored as a named F32 tensor in the safetensors file.
|
|
//! Metadata includes an optional JSON blob for model-level information
|
|
//! (model type, epoch, step, etc.).
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use safetensors::Dtype as StDtype;
|
|
use safetensors::tensor::TensorView;
|
|
// Import SafeTensors from tensor module explicitly to avoid name conflicts
|
|
use safetensors::tensor::SafeTensors as SafeTensorsReader;
|
|
use safetensors::serialize_to_file;
|
|
|
|
use crate::MLError;
|
|
|
|
#[cfg(feature = "cuda")]
|
|
use cudarc::driver::CudaStream;
|
|
|
|
#[cfg(feature = "cuda")]
|
|
use crate::cuda_autograd::var_store::GpuVarStore;
|
|
|
|
/// Save all parameters from a `GpuVarStore` to a safetensors file.
|
|
///
|
|
/// Parameters are read from GPU to host and written as F32 tensors.
|
|
/// The optional `metadata` map is stored in the safetensors header.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `MLError` if any GPU readback or file I/O fails.
|
|
#[cfg(feature = "cuda")]
|
|
pub fn save_safetensors<P: AsRef<Path>>(
|
|
store: &GpuVarStore,
|
|
path: P,
|
|
stream: &Arc<CudaStream>,
|
|
metadata: Option<&BTreeMap<String, String>>,
|
|
) -> Result<(), MLError> {
|
|
let _ = stream; // stream used implicitly by export_to_host (store holds its own stream)
|
|
|
|
// Export all parameters to host
|
|
let params = store.export_to_host()?; // gpu-exit: checkpoint export
|
|
|
|
// Build safetensors data: name -> (shape_as_vec_usize, f32_bytes)
|
|
// We need to hold the host data in scope while building TensorViews.
|
|
let mut host_data: Vec<(String, Vec<usize>, Vec<u8>)> = Vec::with_capacity(params.len());
|
|
for (name, (shape, values)) in ¶ms {
|
|
// Convert f32 slice to bytes
|
|
let bytes: Vec<u8> = values.iter()
|
|
.flat_map(|v| v.to_le_bytes())
|
|
.collect();
|
|
host_data.push((name.clone(), shape.clone(), bytes));
|
|
}
|
|
|
|
// Build tensor views
|
|
let views: Vec<(String, TensorView<'_>)> = host_data.iter()
|
|
.map(|(name, shape, bytes)| {
|
|
let view = TensorView::new(StDtype::F32, shape.clone(), bytes)
|
|
.map_err(|e| MLError::ModelError(format!(
|
|
"Failed to create TensorView for '{name}': {e}"
|
|
)));
|
|
view.map(|v| (name.clone(), v))
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
// Build metadata as HashMap (safetensors API requirement)
|
|
let meta: Option<std::collections::HashMap<String, String>> = metadata.map(|m| {
|
|
m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
|
|
});
|
|
|
|
// serialize_to_file expects IntoIterator<Item = (S: AsRef<str>, V: View)>
|
|
serialize_to_file(views.into_iter(), meta, path.as_ref())
|
|
.map_err(|e| MLError::CheckpointError(format!(
|
|
"Failed to write safetensors to {}: {e}",
|
|
path.as_ref().display()
|
|
)))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Load parameters from a safetensors file into a `GpuVarStore`.
|
|
///
|
|
/// Only loads parameters whose names match existing entries in the store.
|
|
/// Shape must match exactly.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns `MLError` if the file cannot be read, parsed, or if shapes mismatch.
|
|
#[cfg(feature = "cuda")]
|
|
pub fn load_safetensors<P: AsRef<Path>>(
|
|
store: &mut GpuVarStore,
|
|
path: P,
|
|
_stream: &Arc<CudaStream>,
|
|
) -> Result<BTreeMap<String, String>, MLError> {
|
|
let file_bytes = std::fs::read(path.as_ref()).map_err(|e| {
|
|
MLError::CheckpointError(format!(
|
|
"Failed to read safetensors from {}: {e}",
|
|
path.as_ref().display()
|
|
))
|
|
})?;
|
|
|
|
// Parse header to get metadata, then deserialize for tensor access
|
|
let (_header_size, parsed_metadata) = SafeTensorsReader::read_metadata(&file_bytes).map_err(|e| {
|
|
MLError::CheckpointError(format!(
|
|
"Failed to parse safetensors metadata from {}: {e}",
|
|
path.as_ref().display()
|
|
))
|
|
})?;
|
|
|
|
let metadata: BTreeMap<String, String> = match parsed_metadata.metadata() {
|
|
Some(m) => m.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
|
|
None => BTreeMap::new(),
|
|
};
|
|
|
|
let tensors = SafeTensorsReader::deserialize(&file_bytes).map_err(|e| {
|
|
MLError::CheckpointError(format!(
|
|
"Failed to parse safetensors from {}: {e}",
|
|
path.as_ref().display()
|
|
))
|
|
})?;
|
|
|
|
// Build host data map
|
|
let mut host_data: BTreeMap<String, (Vec<usize>, Vec<f32>)> = BTreeMap::new();
|
|
|
|
for (name, _) in store.iter() {
|
|
if let Ok(view) = tensors.tensor(name) {
|
|
let shape: Vec<usize> = view.shape().to_vec();
|
|
let data_bytes = view.data();
|
|
// Convert bytes to f32 (little-endian)
|
|
let numel = data_bytes.len() / 4;
|
|
let mut values = Vec::with_capacity(numel);
|
|
for chunk_idx in 0..numel {
|
|
let offset = chunk_idx * 4;
|
|
if offset + 4 <= data_bytes.len() {
|
|
let bytes = [
|
|
data_bytes[offset],
|
|
data_bytes[offset + 1],
|
|
data_bytes[offset + 2],
|
|
data_bytes[offset + 3],
|
|
];
|
|
values.push(f32::from_le_bytes(bytes));
|
|
}
|
|
}
|
|
host_data.insert(name.to_owned(), (shape, values));
|
|
}
|
|
}
|
|
|
|
// Import into store
|
|
store.import_from_host(&host_data)?;
|
|
|
|
Ok(metadata)
|
|
}
|