10. Entrées/Sorties et Sérialisation (Serde)

Rust gère les fichiers via le trait Read/Write dans std::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 fichier

BufReader 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 original

Personnellement

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 = 10
let 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

CrateUsageAvantage
loginfo!("msg")Standard, simple
tracinginfo!(field = val, "msg")Span context, structuré, async

tracing est recommandé pour les pipelines d’expérimentation (traçabilité de chaque run).


🔗 Voir aussi