use ipfrs_tensorlogic::gradient::arrow_ipc::{load_gradient_from_arrow, store_gradient_as_arrow};
use ipfrs_tensorlogic::gradient::backward_pass::BackwardPassConfig;
use ipfrs_tensorlogic::gradient::federated::DistributedGradientAccumulator;
#[derive(Debug, Clone)]
pub struct GradientSyncRequest {
pub session_id: String,
pub local_gradient: Vec<u8>,
pub min_peers: u32,
pub timeout_secs: u64,
}
#[derive(Debug, Clone)]
pub struct GradientChunkResponse {
pub session_id: String,
pub chunk_index: u32,
pub total_chunks: u32,
pub data: Vec<u8>,
pub peer_id: String,
}
pub struct GradientSyncService {
store: std::sync::Arc<dyn ipfrs_storage::traits::BlockStore>,
}
impl GradientSyncService {
pub fn new(store: std::sync::Arc<dyn ipfrs_storage::traits::BlockStore>) -> Self {
Self { store }
}
pub async fn sync_gradients(
&self,
request: GradientSyncRequest,
chunk_tx: tokio::sync::mpsc::Sender<GradientChunkResponse>,
) -> anyhow::Result<()> {
let local_gradient = load_gradient_from_arrow(&request.local_gradient)
.map_err(|e| anyhow::anyhow!("failed to decode local gradient: {e}"))?;
tracing::debug!(
session_id = %request.session_id,
gradient_len = local_gradient.len(),
min_peers = request.min_peers,
timeout_secs = request.timeout_secs,
"GradientSyncService: starting sync session"
);
let mut accumulator =
DistributedGradientAccumulator::new(&request.session_id, BackwardPassConfig::default());
let local_cid = accumulator
.commit_local(local_gradient.clone(), self.store.as_ref())
.await
.map_err(|e| anyhow::anyhow!("commit_local failed: {e}"))?;
tracing::debug!(
session_id = %request.session_id,
cid = %local_cid,
"GradientSyncService: local gradient committed"
);
let chunk_size = 65_536usize;
let total_chunks = local_gradient.len().div_ceil(chunk_size).max(1);
if local_gradient.is_empty() {
let ipc = store_gradient_as_arrow(&[])
.map_err(|e| anyhow::anyhow!("Arrow IPC encode: {e}"))?;
chunk_tx
.send(GradientChunkResponse {
session_id: request.session_id.clone(),
chunk_index: 0,
total_chunks: 1,
data: ipc,
peer_id: "local".to_string(),
})
.await
.map_err(|_| anyhow::anyhow!("chunk_tx receiver dropped"))?;
return Ok(());
}
for (idx, window) in local_gradient.chunks(chunk_size).enumerate() {
let ipc = store_gradient_as_arrow(window)
.map_err(|e| anyhow::anyhow!("Arrow IPC encode chunk {idx}: {e}"))?;
chunk_tx
.send(GradientChunkResponse {
session_id: request.session_id.clone(),
chunk_index: idx as u32,
total_chunks: total_chunks as u32,
data: ipc,
peer_id: "local".to_string(),
})
.await
.map_err(|_| anyhow::anyhow!("chunk_tx receiver dropped at chunk {idx}"))?;
}
tracing::debug!(
session_id = %request.session_id,
total_chunks,
"GradientSyncService: streamed all local chunks"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ipfrs_storage::{BlockStoreConfig, SledBlockStore};
use std::sync::Arc;
fn make_store(suffix: &str) -> Arc<SledBlockStore> {
let path = std::env::temp_dir().join(format!("ipfrs-test-gradsync-{suffix}"));
let _ = std::fs::remove_dir_all(&path);
let config = BlockStoreConfig {
path,
cache_size: 16 * 1024 * 1024,
};
Arc::new(SledBlockStore::new(config).expect("SledBlockStore::new"))
}
#[test]
fn test_gradient_sync_service_new() {
let store = make_store("new");
let _service = GradientSyncService::new(store);
}
#[tokio::test]
async fn test_gradient_sync_service_streams_chunks() {
let store = make_store("streams-chunks");
let service = GradientSyncService::new(store);
let gradient: Vec<f32> = (0u32..128).map(|i| i as f32 * 0.1).collect();
let local_gradient_bytes =
store_gradient_as_arrow(&gradient).expect("store_gradient_as_arrow");
let request = GradientSyncRequest {
session_id: "test-sync-session".to_string(),
local_gradient: local_gradient_bytes,
min_peers: 0,
timeout_secs: 5,
};
let (tx, mut rx) = tokio::sync::mpsc::channel(64);
service
.sync_gradients(request, tx)
.await
.expect("sync_gradients");
let mut received = Vec::new();
while let Some(chunk) = rx.recv().await {
assert_eq!(chunk.session_id, "test-sync-session");
assert_eq!(chunk.peer_id, "local");
received.push(chunk);
}
assert!(
!received.is_empty(),
"at least one chunk must be streamed back"
);
assert_eq!(received[0].chunk_index, 0);
assert!(
!received[0].data.is_empty(),
"chunk data must be non-empty Arrow IPC bytes"
);
}
#[tokio::test]
async fn test_gradient_sync_service_empty_gradient() {
let store = make_store("empty-grad");
let service = GradientSyncService::new(store);
let local_gradient_bytes =
store_gradient_as_arrow(&[]).expect("store_gradient_as_arrow on empty");
let request = GradientSyncRequest {
session_id: "empty-session".to_string(),
local_gradient: local_gradient_bytes,
min_peers: 0,
timeout_secs: 1,
};
let (tx, mut rx) = tokio::sync::mpsc::channel(64);
service
.sync_gradients(request, tx)
.await
.expect("sync_gradients");
let first = rx.recv().await.expect("must receive one chunk");
assert_eq!(first.chunk_index, 0);
assert_eq!(first.total_chunks, 1);
assert_eq!(first.peer_id, "local");
assert!(rx.recv().await.is_none());
}
#[tokio::test]
async fn test_gradient_sync_service_large_gradient() {
let store = make_store("large-grad");
let service = GradientSyncService::new(store);
let gradient: Vec<f32> = (0u32..131_072).map(|i| i as f32 * 1e-5).collect();
let local_gradient_bytes =
store_gradient_as_arrow(&gradient).expect("store_gradient_as_arrow");
let request = GradientSyncRequest {
session_id: "large-session".to_string(),
local_gradient: local_gradient_bytes,
min_peers: 0,
timeout_secs: 10,
};
let (tx, mut rx) = tokio::sync::mpsc::channel(64);
service
.sync_gradients(request, tx)
.await
.expect("sync_gradients");
let mut received = Vec::new();
while let Some(chunk) = rx.recv().await {
received.push(chunk);
}
assert_eq!(received.len(), 2, "131_072 elements / 65_536 = 2 chunks");
assert_eq!(received[0].chunk_index, 0);
assert_eq!(received[1].chunk_index, 1);
assert_eq!(received[0].total_chunks, 2);
assert_eq!(received[1].total_chunks, 2);
}
}