10. Entrées/Sorties et Sérialisation (Serde)
Rust gère les fichiers via le trait
Read/Writedansstd::io. La sérialisation JSON, binaire, etc. est déléguée à Serde, le framework de sérialisation standard. Comprendre ces deux aspects est essentiel pour sauvegarder des gradients, lire des configs, et logger des résultats.
10.1 File I/O — Lire et écrire des fichiers
Lire un fichier
use std::fs;
// Tout le fichier en une String (simple, petits fichiers)
let data = fs::read_to_string("config.toml")?;
// Tout le fichier en Vec<u8> (binaire)
let data = fs::read("model.bin")?;
// Line by line (gros fichiers)
use std::io::{BufRead, BufReader};
let file = fs::File::open("large_gradients.csv")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
println!("{line}");
}Écrire un fichier
use std::fs;
use std::io::Write;
// Écrire tout d'un coup
fs::write("output.txt", b"hello world")?;
// Écrire ligne par ligne
let mut file = fs::File::create("results.csv")?;
writeln!(file, "worker_id,loss,accuracy")?;
writeln!(file, "0,0.345,0.912")?;
writeln!(file, "1,0.421,0.887")?;Trait Read et Write
use std::io::{Read, Write};
// Trait Read : lire depuis n'importe quelle source
fn read_gradients(mut source: impl Read) -> std::io::Result<Vec<f64>> {
let mut buffer = [0u8; 8]; // f64 = 8 octets
source.read_exact(&mut buffer)?;
let value = f64::from_le_bytes(buffer);
Ok(vec![value])
}
// Trait Write : écrire vers n'importe quelle destination
fn write_gradients(grads: &[f64], mut dest: impl Write) -> std::io::Result<()> {
for &g in grads {
dest.write_all(&g.to_le_bytes())?;
}
Ok(())
}
// Utilisation : fichier, socket, buffer mémoire, etc.
let data = vec![1.0, 2.0, 3.0];
let mut buf = Vec::new();
write_gradients(&data, &mut buf)?; // vers un buffer mémoire
let mut file = fs::File::create("gradients.bin")?;
write_gradients(&data, &mut file)?; // vers un fichierBufReader et BufWriter
Toujours utiliser BufReader/BufWriter pour les fichiers :
use std::io::{BufReader, BufWriter};
// Sans buffer : 1 syscall par read → 1000× plus lent
let file = fs::File::open("big_file.bin")?;
// let mut reader = file; // ❌ pas de buffer
// Avec buffer : lecture par blocs de 8KB
let reader = BufReader::new(file); // ✅
// Écriture bufferisée
let file = fs::File::create("output.bin")?;
let mut writer = BufWriter::new(file);10.2 Serde — Sérialisation
Serde (SERialization/DEserialization) est le framework standard. Il supporte JSON, BSON, CBOR, MessagePack, YAML, TOML, et des formats binaires (bincode, rmp).
Installation
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1" # JSON
bincode = "1" # Binaire compact
toml = "0.8" # TOML (config)Définir des types sérialisables
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct GradientBatch {
round: usize,
worker_id: usize,
values: Vec<f64>,
metadata: Metadata,
}
#[derive(Debug, Serialize, Deserialize)]
struct Metadata {
n_samples: usize,
loss: f64,
timestamp: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ExperimentConfig {
learning_rate: f64,
momentum: f64,
gar: String, // "median", "krum", etc.
f: usize, // nombre de byzantins supposés
n_workers: usize,
dimension: usize,
dataset: String,
seed: Option<u64>,
}JSON
// Sérialisation
let grads = GradientBatch {
round: 0,
worker_id: 42,
values: vec![1.0, 2.0, 3.0],
metadata: Metadata { n_samples: 128, loss: 0.345, timestamp: "2026-05-19".into() },
};
let json = serde_json::to_string(&grads)?;
let json_pretty = serde_json::to_string_pretty(&grads)?;
println!("{json}");
// Désérialisation
let parsed: GradientBatch = serde_json::from_str(&json)?;
// Depuis un fichier
let config: ExperimentConfig = serde_json::from_reader(
BufReader::new(fs::File::open("config.json")?)
)?;
// JSON formaté
println!("{}", serde_json::to_string_pretty(&config)?);Binaire (bincode)
// Binaire compact et rapide (pas de parsing textuel)
let bytes = bincode::serialize(&grads)?;
println!("taille JSON: {} octets", json.len());
println!("taille bincode: {} octets", bytes.len());
// JSON: ~150 octets
// bincode: ~40 octets (f64 natifs, pas d'overhead textuel)
// Relecture
let decoded: GradientBatch = bincode::deserialize(&bytes)?;
// Écriture et lecture directe
bincode::serialize_into(
BufWriter::new(fs::File::create("gradients.bin")?),
&grads,
)?;
let loaded: GradientBatch = bincode::deserialize_from(
BufReader::new(fs::File::open("gradients.bin")?),
)?;Attributs Serde avancés
#[derive(Debug, Serialize, Deserialize)]
struct Config {
#[serde(default = "default_lr")]
learning_rate: f64,
#[serde(skip_serializing_if = "Option::is_none")]
seed: Option<u64>,
#[serde(rename = "num_workers")]
n_workers: usize,
#[serde(default)]
tags: Vec<String>, // valeur par défaut = vec![]
}
fn default_lr() -> f64 { 0.001 }
// Utilisation
let config = Config {
learning_rate: 0.01,
seed: None,
n_workers: 10,
tags: vec![],
};
// JSON : {"learning_rate": 0.01, "num_workers": 10, "tags": []}
// Note : seed est absent (None), learning_rate a son nom originalPersonnellement
Un pattern utile pour la thèse : une config avec des valeurs par défaut et override par JSON.
# config.toml — lisible par l'utilisateur
learning_rate = 0.01
gar = "median"
n_workers = 50
f = 10let config: ExperimentConfig = toml::from_str(
&fs::read_to_string("config.toml")?
)?;
println!("GAR: {}, workers: {}", config.gar, config.n_workers);10.3 Path et Dossiers
use std::path::{Path, PathBuf};
// PathBuf = propriétaire (comme String)
let mut path = PathBuf::new();
path.push("data");
path.push("experiment_1");
path.push("gradients.bin");
// Path = emprunt (comme &str)
let p: &Path = path.as_path();
// Opérations
println!("fichier: {:?}", path.file_name()); // "gradients.bin"
println!("extension: {:?}", path.extension()); // "bin"
println!("parent: {:?}", path.parent()); // "data/experiment_1"
// Itérer sur les composants
for component in path.components() {
println!("{component:?}");
}
// Créer des dossiers
fs::create_dir("results")?;
fs::create_dir_all("results/2026/05/19")?; // crée toute l'arborescence
// Lister un dossier
for entry in fs::read_dir("data")? {
let entry = entry?;
println!("{} (taille: {})", entry.path().display(), entry.metadata()?.len());
}10.4 Exemple Complet
use std::fs;
use std::io::BufReader;
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct ExperimentResult {
config: ExperimentConfig,
final_accuracy: f64,
rounds_to_converge: usize,
worker_history: Vec<WorkerRound>,
}
#[derive(Debug, Serialize, Deserialize)]
struct WorkerRound {
round: usize,
worker_id: usize,
gradient_norm: f64,
}
fn save_results(path: &Path, result: &ExperimentResult) -> Result<(), Box<dyn std::error::Error>> {
let file = fs::File::create(path)?;
serde_json::to_writer_pretty(BufWriter::new(file), result)?;
Ok(())
}
fn load_results(path: &Path) -> Result<ExperimentResult, Box<dyn std::error::Error>> {
let file = fs::File::open(path)?;
let result: ExperimentResult = serde_json::from_reader(BufReader::new(file))?;
Ok(result)
}
// Binaire compact pour les gros volumes
fn save_gradients_binary(path: &Path, gradients: &[Vec<f64>]) -> Result<(), Box<dyn std::error::Error>> {
let file = fs::File::create(path)?;
bincode::serialize_into(BufWriter::new(file), gradients)?;
Ok(())
}
fn load_gradients_binary(path: &Path) -> Result<Vec<Vec<f64>>, Box<dyn std::error::Error>> {
let file = fs::File::open(path)?;
let gradients: Vec<Vec<f64>> = bincode::deserialize_from(BufReader::new(file))?;
Ok(gradients)
}10.5 Logging et Tracing
Pour les expériences de thèse (logs structurés avec niveau, fichier, rotation) :
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-appender = "0.2"use tracing::{info, warn, error, debug, span, Level};
fn setup_logging() {
tracing_subscriber::fmt()
.with_env_filter("gradient_core=debug,my_crate=info")
.with_file(true)
.with_line_number(true)
.init();
}
fn aggregate_experiment(grads: &[Vec<f64>], gar_name: &str) -> Result<Vec<f64>, GarError> {
let span = span!(Level::INFO, "aggregation", gar = gar_name, n = grads.len());
let _guard = span.enter();
info!("début agrégation avec {gar_name}");
debug!("dimension: {}", grads[0].len());
// ...
info!("agrégation terminée");
Ok(result)
}
// Log dans un fichier avec rotation
use tracing_appender::rolling;
fn setup_file_logging() {
let file_appender = rolling::daily("logs", "experiments.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_ansi(false)
.init();
}Macro tracing vs log
| Crate | Usage | Avantage |
|---|---|---|
log | info!("msg") | Standard, simple |
tracing | info!(field = val, "msg") | Span context, structuré, async |
tracing est recommandé pour les pipelines d’expérimentation (traçabilité de chaque run).
🔗 Voir aussi
- 7-Modules et Organisation
- 8-Tests et Documentation
- 9-String et Collections
- 14-Projet GAR Rust — intégration tracing dans les GARs
- Rust