o41-training-data 0.1.0

Deterministic, resumable training batch streams over Parquet shards and data manifests, with a C API
Documentation
use super::*;
use arrow::{array::Int64Array, ffi_stream::ArrowArrayStreamReader};
use std::{
    io::Write,
    time::{Duration, Instant},
};

fn write_jsonl(path: &std::path::Path, ids: std::ops::Range<i64>) {
    let mut gzip = flate2::write::GzEncoder::new(
        std::fs::File::create(path).unwrap(),
        flate2::Compression::default(),
    );
    for id in ids {
        writeln!(gzip, "{{\"id\":{id}}}").unwrap();
    }
    gzip.finish().unwrap();
}

unsafe fn pull(handle: *mut Loader) -> (u64, u64, Vec<i64>) {
    let mut stream = FFI_ArrowArrayStream::empty();
    let mut stage = 0;
    let mut index = 0;
    assert_eq!(
        unsafe { loader_next(handle, &mut stream, &mut stage, &mut index) },
        1
    );
    let mut reader = ArrowArrayStreamReader::try_new(stream).unwrap();
    let batch = reader.next().unwrap().unwrap();
    let ids = batch
        .column(0)
        .as_any()
        .downcast_ref::<Int64Array>()
        .unwrap()
        .values()
        .to_vec();
    (stage, index, ids)
}

#[test]
fn advance_with_exactly_eight_queued_batches() {
    let root = std::env::temp_dir().join("o41-training-data-prefetch");
    std::fs::create_dir_all(&root).unwrap();
    let path = root.join("prefetch.jsonl.gz");
    write_jsonl(&path, 0..9);
    let stage = serde_json::json!({"encoding":"jsonl.gz", "sources":[{"files":[{"uri":path}]}], "batchSize":2,"shuffle":{"bufferRows":5},"infinite":true});
    let mut answers = Vec::new();
    for prefetch in [0, 8] {
        let config = serde_json::json!({"storage":{},"fields":[{"name":"id","from":"id","dtype":"i64"}],"stages":[stage,stage],"seed":7,"rank":0,"worldSize":1,"threads":2,"prefetchBatches":prefetch});
        let config = CString::new(config.to_string()).unwrap();
        // The handle and Arrow outputs live until their corresponding releases.
        unsafe {
            let handle = loader_open(config.as_ptr());
            assert!(!handle.is_null());
            for _ in 0..3 {
                pull(handle);
            }
            let deadline = Instant::now() + Duration::from_secs(5);
            while (*handle).worker.as_ref().unwrap().batches.len() < prefetch
                && Instant::now() < deadline
            {
                std::thread::sleep(Duration::from_millis(5));
            }
            assert_eq!((*handle).worker.as_ref().unwrap().batches.len(), prefetch);
            assert_eq!(loader_advance(handle, 1), 1);
            let result: Vec<_> = (0..12).map(|_| pull(handle)).collect();
            assert!(result.iter().all(|(stage, _, _)| *stage == 1));
            answers.push(result);
            loader_close(handle);
        }
    }
    assert_eq!(answers[0], answers[1]);
}

#[test]
fn resolve_checkpoint_and_resume_are_exact() {
    let root = std::env::temp_dir().join("o41-training-data-resume");
    std::fs::create_dir_all(&root).unwrap();
    let a = root.join("a.jsonl.gz");
    let b = root.join("b.jsonl.gz");
    write_jsonl(&a, 0..7);
    write_jsonl(&b, 7..13);
    let shard = |path: &std::path::Path, records: usize| serde_json::json!({"uri": path, "records": records, "size_bytes": std::fs::metadata(path).unwrap().len(), "etag": ""});
    let manifest = root.join("data-manifest-v1.json");
    std::fs::write(
        &manifest,
        serde_json::json!({"version": 1, "data_format": "jsonl.gz", "splits": {"train": {
            "records": 13, "shards": [shard(&a, 7), shard(&b, 6)],
            "curriculum": {"easy": {"records": 7, "shards": [shard(&a, 7)]},
                           "all": {"records": 13, "shards": [shard(&a, 7), shard(&b, 6)]}}}}})
        .to_string(),
    )
    .unwrap();
    let config = serde_json::json!({
        "storage": {},
        "fields": [{"name":"id","from":"id","dtype":"i64"}],
        "stages": [{"sources": [{"manifestUri": manifest, "split": "train", "curriculumStages": ["easy", "all"]}],
                    "batchSize": 4, "infinite": true}],
        "seed": 1, "rank": 0, "worldSize": 1, "threads": 2, "prefetchBatches": 2
    });
    let config = CString::new(config.to_string()).unwrap();
    unsafe {
        let resolved = loader_resolve(config.as_ptr());
        assert!(
            !resolved.is_null(),
            "{:?}",
            CStr::from_ptr(loader_last_error(ptr::null_mut()))
        );
        let parsed: Config =
            serde_json::from_str(CStr::from_ptr(resolved).to_str().unwrap()).unwrap();
        let files: Vec<_> = parsed.stages[0].sources[0]
            .files
            .iter()
            .map(|f| (f.uri.clone(), f.rows))
            .collect();
        assert_eq!(
            files,
            vec![
                (a.to_string_lossy().into_owned(), Some(7)),
                (b.to_string_lossy().into_owned(), Some(6))
            ]
        );
        assert_eq!(
            parsed.stages[0].sources[0].encoding.as_deref(),
            Some("jsonl.gz")
        );

        let handle = loader_open(resolved);
        assert!(
            !handle.is_null(),
            "{:?}",
            CStr::from_ptr(loader_last_error(ptr::null_mut()))
        );
        loader_free_string(resolved);
        let first: Vec<_> = (0..3).map(|_| pull(handle)).collect();
        assert_eq!(first[0].2, vec![0, 1, 2, 3]);
        let mut out: *const c_char = ptr::null();
        assert_eq!(loader_checkpoint(handle, &mut out), 1);
        let checkpoint = CStr::from_ptr(out).to_str().unwrap().to_owned();
        let expected: Vec<_> = (0..5).map(|_| pull(handle)).collect();
        loader_close(handle);

        let document: serde_json::Value = serde_json::from_str(&checkpoint).unwrap();
        assert_eq!(document["batchesYielded"], 3);
        assert_eq!(document["rowsYielded"], 12);
        assert!(document["config"]["storage"].get("accessKeyId").is_none());
        let checkpoint = CString::new(checkpoint).unwrap();
        let storage = CString::new("{}").unwrap();
        let resumed = loader_resume(checkpoint.as_ptr(), storage.as_ptr());
        assert!(
            !resumed.is_null(),
            "{:?}",
            CStr::from_ptr(loader_last_error(ptr::null_mut()))
        );
        let actual: Vec<_> = (0..5).map(|_| pull(resumed)).collect();
        assert_eq!(actual, expected);
        loader_close(resumed);
    }
}