| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use pyo3::prelude::*; |
| use std::fs::File; |
| use std::io::{BufWriter, Write}; |
| use std::path::Path; |
| use crate::errors::KernelError; |
|
|
| |
|
|
| |
| const SAIL_MAGIC: &[u8] = b"SAIL"; |
| |
| const FORMAT_VERSION: u8 = 1; |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[pyfunction] |
| #[pyo3(signature = (token_batches, output_path, pad_id = 0, max_length = 2048))] |
| pub fn write_arrow_shard( |
| py: Python<'_>, |
| token_batches: Vec<Vec<u32>>, |
| output_path: String, |
| pad_id: u32, |
| max_length: usize, |
| ) -> PyResult<usize> { |
| py.allow_threads(move || { |
| write_shard_internal(&token_batches, &output_path, pad_id, max_length) |
| .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string())) |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| #[pyfunction] |
| pub fn merge_shards( |
| py: Python<'_>, |
| shard_paths: Vec<String>, |
| output_path: String, |
| ) -> PyResult<usize> { |
| py.allow_threads(move || { |
| merge_shards_internal(&shard_paths, &output_path) |
| .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string())) |
| }) |
| } |
|
|
| |
|
|
| fn write_shard_internal( |
| token_batches: &[Vec<u32>], |
| output_path: &str, |
| pad_id: u32, |
| max_length: usize, |
| ) -> Result<usize, KernelError> { |
| if token_batches.is_empty() { |
| return Ok(0); |
| } |
|
|
| let path = Path::new(output_path); |
| if let Some(parent) = path.parent() { |
| std::fs::create_dir_all(parent) |
| .map_err(|e| KernelError::IoError(format!("Cannot create dir: {}", e)))?; |
| } |
|
|
| let file = File::create(path) |
| .map_err(|e| KernelError::IoError(format!("Cannot create {}: {}", output_path, e)))?; |
| let mut writer = BufWriter::with_capacity(4 * 1024 * 1024, file); |
|
|
| let n_records = token_batches.len(); |
|
|
| |
| writer.write_all(SAIL_MAGIC) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| writer.write_all(&[FORMAT_VERSION]) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| writer.write_all(&(n_records as u32).to_le_bytes()) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| writer.write_all(&(max_length as u32).to_le_bytes()) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
|
|
| |
| for seq in token_batches { |
| let real_len = seq.len().min(max_length); |
|
|
| |
| writer.write_all(&(real_len as u32).to_le_bytes()) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
|
|
| |
| for &id in &seq[..real_len] { |
| writer.write_all(&id.to_le_bytes()) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| } |
|
|
| |
| let pad_count = max_length - real_len; |
| for _ in 0..pad_count { |
| writer.write_all(&pad_id.to_le_bytes()) |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
| } |
| } |
|
|
| writer.flush() |
| .map_err(|e| KernelError::IoError(e.to_string()))?; |
|
|
| Ok(n_records) |
| } |
|
|
| fn merge_shards_internal( |
| shard_paths: &[String], |
| output_path: &str, |
| ) -> Result<usize, KernelError> { |
| if shard_paths.is_empty() { |
| return Ok(0); |
| } |
|
|
| |
| |
| let all_batches: Result<Vec<Vec<Vec<u32>>>, KernelError> = shard_paths.iter() |
| .map(|p| read_shard_records(p)) |
| .collect(); |
|
|
| let all_batches = all_batches?; |
|
|
| |
| let max_length = all_batches.iter() |
| .flat_map(|b| b.iter()) |
| .map(|r| r.len()) |
| .max() |
| .unwrap_or(2048); |
|
|
| let flat: Vec<Vec<u32>> = all_batches.into_iter().flatten().collect(); |
| let n = flat.len(); |
|
|
| write_shard_internal(&flat, output_path, 0, max_length)?; |
| Ok(n) |
| } |
|
|
| |
| fn read_shard_records(path: &str) -> Result<Vec<Vec<u32>>, KernelError> { |
| use std::io::Read; |
|
|
| let bytes = std::fs::read(path) |
| .map_err(|e| KernelError::IoError(format!("Cannot read {}: {}", path, e)))?; |
|
|
| if bytes.len() < 13 { |
| return Err(KernelError::InvalidInput("Shard file too small".to_string())); |
| } |
|
|
| |
| if &bytes[..4] != SAIL_MAGIC { |
| return Err(KernelError::InvalidInput("Invalid SAIL magic bytes".to_string())); |
| } |
|
|
| let n_records = u32::from_le_bytes(bytes[5..9].try_into().unwrap()) as usize; |
| let max_length = u32::from_le_bytes(bytes[9..13].try_into().unwrap()) as usize; |
|
|
| let mut records = Vec::with_capacity(n_records); |
| let mut offset = 13usize; |
|
|
| for _ in 0..n_records { |
| if offset + 4 > bytes.len() { |
| break; |
| } |
| let real_len = u32::from_le_bytes(bytes[offset..offset+4].try_into().unwrap()) as usize; |
| offset += 4; |
|
|
| let mut ids = Vec::with_capacity(real_len); |
| for j in 0..max_length { |
| if offset + 4 > bytes.len() { break; } |
| let id = u32::from_le_bytes(bytes[offset..offset+4].try_into().unwrap()); |
| if j < real_len { |
| ids.push(id); |
| } |
| offset += 4; |
| } |
| records.push(ids); |
| } |
|
|
| Ok(records) |
| } |
|
|
| |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn test_write_read_roundtrip() { |
| let dir = tempfile::tempdir().expect("create temp dir"); |
| let shard = dir.path().join("test.shard"); |
| let shard_str = shard.to_str().unwrap(); |
|
|
| let batches: Vec<Vec<u32>> = vec![ |
| vec![1, 2, 3, 4, 5], |
| vec![10, 20, 30], |
| vec![100, 200], |
| ]; |
|
|
| let n = write_shard_internal(&batches, shard_str, 0, 8).unwrap(); |
| assert_eq!(n, 3); |
|
|
| let read_back = read_shard_records(shard_str).unwrap(); |
| assert_eq!(read_back.len(), 3); |
| assert_eq!(read_back[0], vec![1, 2, 3, 4, 5]); |
| assert_eq!(read_back[1], vec![10, 20, 30]); |
| } |
|
|
| #[test] |
| fn test_truncation_on_write() { |
| let dir = tempfile::tempdir().expect("create temp dir"); |
| let shard = dir.path().join("trunc.shard"); |
| let long_seq: Vec<u32> = (0..1000).collect(); |
| write_shard_internal(&[long_seq], shard.to_str().unwrap(), 0, 10).unwrap(); |
| let read_back = read_shard_records(shard.to_str().unwrap()).unwrap(); |
| assert_eq!(read_back[0].len(), 10, "Should be truncated to max_length"); |
| } |
| } |
|
|