#[derive(Debug, Clone)]
pub struct GradientChunk {
pub session_id: String,
pub chunk_index: u32,
pub total_chunks: u32,
pub arrow_ipc_bytes: Vec<u8>,
pub checksum: u32,
}
impl GradientChunk {
pub fn compute_checksum(data: &[u8]) -> u32 {
crc32fast::hash(data)
}
pub fn verify_checksum(&self) -> bool {
Self::compute_checksum(&self.arrow_ipc_bytes) == self.checksum
}
}
#[derive(Debug, thiserror::Error)]
pub enum GradientStreamError {
#[error("Arrow IPC error: {0}")]
ArrowIpc(#[from] anyhow::Error),
#[error(
"Checksum mismatch on chunk {chunk_index}: expected {expected:#010x}, got {actual:#010x}"
)]
ChecksumMismatch {
chunk_index: u32,
expected: u32,
actual: u32,
},
#[error("Incomplete chunk stream: received {received} of {total} chunks")]
IncompleteStream { received: usize, total: usize },
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub struct GradientStreamSession {
session_id: String,
chunk_size: usize,
compression: bool,
}
impl GradientStreamSession {
pub fn new(session_id: &str, chunk_size: usize) -> Self {
Self {
session_id: session_id.to_string(),
chunk_size: chunk_size.max(1),
compression: false,
}
}
pub fn with_defaults(session_id: &str) -> Self {
Self::new(session_id, 65_536)
}
pub fn with_compression(mut self, enabled: bool) -> Self {
self.compression = enabled;
self
}
pub fn is_compression_enabled(&self) -> bool {
self.compression
}
pub fn session_id(&self) -> &str {
&self.session_id
}
pub fn chunk_size(&self) -> usize {
self.chunk_size
}
pub fn encode_gradient(
&self,
gradient: &[f32],
) -> std::result::Result<Vec<GradientChunk>, GradientStreamError> {
use ipfrs_tensorlogic::gradient::arrow_ipc::store_gradient_as_arrow;
let total_chunks = if gradient.is_empty() {
1
} else {
gradient.len().div_ceil(self.chunk_size)
};
let mut chunks = Vec::with_capacity(total_chunks);
if gradient.is_empty() {
let ipc_bytes = store_gradient_as_arrow(&[]).map_err(GradientStreamError::ArrowIpc)?;
let checksum = GradientChunk::compute_checksum(&ipc_bytes);
chunks.push(GradientChunk {
session_id: self.session_id.clone(),
chunk_index: 0,
total_chunks: 1,
arrow_ipc_bytes: ipc_bytes,
checksum,
});
return Ok(chunks);
}
for (idx, window) in gradient.chunks(self.chunk_size).enumerate() {
let ipc_bytes =
store_gradient_as_arrow(window).map_err(GradientStreamError::ArrowIpc)?;
let checksum = GradientChunk::compute_checksum(&ipc_bytes);
chunks.push(GradientChunk {
session_id: self.session_id.clone(),
chunk_index: idx as u32,
total_chunks: total_chunks as u32,
arrow_ipc_bytes: ipc_bytes,
checksum,
});
}
Ok(chunks)
}
pub fn decode_chunks(
&self,
mut chunks: Vec<GradientChunk>,
) -> std::result::Result<Vec<f32>, GradientStreamError> {
use ipfrs_tensorlogic::gradient::arrow_ipc::load_gradient_from_arrow;
if chunks.is_empty() {
return Ok(Vec::new());
}
chunks.sort_by_key(|c| c.chunk_index);
let total = chunks[0].total_chunks as usize;
if chunks.len() != total {
return Err(GradientStreamError::IncompleteStream {
received: chunks.len(),
total,
});
}
let mut gradient = Vec::new();
for chunk in &chunks {
let actual = GradientChunk::compute_checksum(&chunk.arrow_ipc_bytes);
if actual != chunk.checksum {
return Err(GradientStreamError::ChecksumMismatch {
chunk_index: chunk.chunk_index,
expected: chunk.checksum,
actual,
});
}
let slice = load_gradient_from_arrow(&chunk.arrow_ipc_bytes)
.map_err(GradientStreamError::ArrowIpc)?;
gradient.extend_from_slice(&slice);
}
Ok(gradient)
}
pub async fn stream_to<S>(
&self,
gradient: &[f32],
stream: &mut S,
) -> std::result::Result<usize, GradientStreamError>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
use tokio::io::AsyncWriteExt;
let chunks = self.encode_gradient(gradient)?;
let n = chunks.len();
for chunk in &chunks {
let payload_len = chunk.arrow_ipc_bytes.len() as u32;
let mut header = [0u8; 16];
header[0..4].copy_from_slice(&chunk.chunk_index.to_le_bytes());
header[4..8].copy_from_slice(&chunk.total_chunks.to_le_bytes());
header[8..12].copy_from_slice(&payload_len.to_le_bytes());
header[12..16].copy_from_slice(&chunk.checksum.to_le_bytes());
stream.write_all(&header).await?;
stream.write_all(&chunk.arrow_ipc_bytes).await?;
}
stream.flush().await?;
Ok(n)
}
pub async fn receive_from<S>(
&self,
stream: &mut S,
) -> std::result::Result<Vec<f32>, GradientStreamError>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
use tokio::io::{AsyncReadExt, BufReader};
let mut reader = BufReader::new(stream);
let mut chunks: Vec<GradientChunk> = Vec::new();
loop {
let mut header = [0u8; 16];
match reader.read_exact(&mut header).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
if chunks.is_empty() {
tracing::debug!(
session_id = %self.session_id,
"Gradient stream closed before first frame — no network"
);
return Ok(Vec::new());
}
break;
}
Err(e) => return Err(GradientStreamError::Io(e)),
}
let chunk_index = u32::from_le_bytes(header[0..4].try_into().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "bad header bytes [0..4]")
})?);
let total_chunks = u32::from_le_bytes(header[4..8].try_into().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "bad header bytes [4..8]")
})?);
let payload_len = u32::from_le_bytes(header[8..12].try_into().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "bad header bytes [8..12]")
})?) as usize;
let checksum = u32::from_le_bytes(header[12..16].try_into().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "bad header bytes [12..16]")
})?);
let mut payload = vec![0u8; payload_len];
reader.read_exact(&mut payload).await?;
chunks.push(GradientChunk {
session_id: self.session_id.clone(),
chunk_index,
total_chunks,
arrow_ipc_bytes: payload,
checksum,
});
if chunks.len() >= total_chunks as usize {
break;
}
}
self.decode_chunks(chunks)
}
}