Samudra Rust data loading¶
The optional samudra-rust-loader extension loads local flat and compact OM4
stores for training and validation. Python plans batches and deduplicates physical
planes across each autoregressive rollout. Rust reads those planes concurrently
into host buffers, then Python transfers and prepares them for the model.
Select it with:
Components¶
RustDataLoadingConfig constructs the source readers and NativeBatchLoader.
Each data bundle creates one RustIoRuntime, shared by its training and validation
readers within the process.
| Component | Responsibility in the Rust loader |
|---|---|
RustDataLoadingConfig |
Configure read concurrency and prefetch; construct the runtime and loader. |
TrainingWindows |
Define input history, forecast targets, stride, masks, and source pairing; produce read plans for the sampled windows. |
NativeBatchLoader (native_loader.py) |
Follow the sampler schedule, deduplicate batch reads, prefetch, transfer, and gather model tensors. |
NativeOm4Reader (native_reader.py) |
Map canonical channels and sliced time indices to physical OM4 arrays and depth selections. |
RustIoRuntime (rust_reader.py) |
Open the extension's flat or compact readers and supply their shared ZarrReadPool. |
_PinnedTensorPool |
Reuse host buffers after their CUDA consumer events complete. Each loader owns its pool. |
BatchPreparer |
Supply model-batch grid context and apply the input, boundary, and label preparation policies. |
BatchPreprocessor |
Normalize and mask unique planes using cached device statistics and masks. |
Pipeline and concurrency¶
This diagram shows CUDA training with prefetch_to_device: true for one DDP
rank. The host, CUDA preparation, and model lanes can work on different batches
concurrently. The CUDA boxes name the Python components that queue work on each
stream.
flowchart LR
subgraph HOST["CPU host: one process per DDP rank"]
direction TB
L["NativeBatchLoader<br/>queue depth: prefetch_batches"]
subgraph PRODUCER["One Python producer thread per iterator"]
W["TrainingWindows<br/>plan sampled windows"]
D["_NativeBatchReader<br/>deduplicate rollout planes"]
R["NativeOm4Reader<br/>map channels and times"]
W --> D --> R
end
L --> W
subgraph IO["RustIoRuntime / ZarrReadPool"]
READ["FlatOm4Reader / CompactOm4Reader<br/>parallel reads and decompression<br/>max_concurrent_reads Rayon threads"]
end
R -->|"read_into; GIL released"| READ
READ -->|"fill"| P["_PinnedTensorPool<br/>unique planes in host RAM"]
end
subgraph PREFETCH["CUDA prefetch stream: batch N+1"]
direction TB
B["BatchPreparer<br/>grid context and preparation policy"]
C["BatchPreprocessor<br/>normalize and mask unique planes"]
G["NativeBatchLoader<br/>gather ModelBatch tensors"]
B --> C --> G
end
subgraph COMPUTE["CUDA model stream: batch N"]
M["Model<br/>forward / backward"]
end
HOST -->|"copy unique planes to CUDA"| PREFETCH
PREFETCH -->|"wait for ready event<br/>when consuming N+1"| COMPUTE
The producer can read a later batch while the CUDA prefetch stream prepares the
next batch and the model stream computes the current one. prefetch_batches
limits the host queue; the producer executes one batch read at a time. Within
that read, the Rayon pool performs up to max_concurrent_reads tasks concurrently.
The Python thread waits for read_into to finish with the GIL released.
_CudaPrefetchIterator queues copies and preparation on a dedicated stream, one
batch ahead of the model. The model stream waits for that batch's ready event
before using its tensors. NativeBatchLoader coordinates host reads, device
copies, and the final gather.
Each active training or validation iterator has its own producer thread and CUDA
prefetch stream. Their loaders have separate pinned-buffer pools and share the
rank's ZarrReadPool. Results are yielded in sampler order.
Host buffers and device transfer¶
The producer allocates one float32 tensor per unique read group, usually one for
prognostic planes and one for boundary planes. Input and label reads share a
group when they reference the same physical store and channels. The tensors have
shape (unique_time, channel, y, x).
read_into takes integer positions in the source's current time slice.
NativeOm4Reader maps these positions to storage rows using a cached index array.
Input and label sources have matching time axes, so the loader can deduplicate
their positions before reading.
For CUDA training, _PinnedTensorPool supplies page-locked host memory. CPU
training uses ordinary unpinned buffers. The loader selects pinning automatically
from the training device. Rust fills a NumPy view of that allocation directly.
read_into completes all writes before returning or raising, so the caller can
safely reuse the destination after a failed read.
NativeBatchLoader copies each unique plane to CUDA once. BatchPreparer and
BatchPreprocessor apply normalization and masking, then the loader gathers
repeated history and rollout positions into ModelBatch tensors.
HostPrefetch selects disk-to-RAM read-ahead for CPU training or CUDA training
with prefetch_to_device: false. On CUDA, transfer and preparation happen on the
model's current stream when the batch is consumed, using pinned host buffers.
This mode holds fewer prepared batches on the GPU.
Buffer reuse and cleanup¶
After queuing CUDA copies and preparation, the loader records an event and returns
the host buffers to _PinnedTensorPool with that event. On each acquisition,
the pool queries pending events:
- completed event: move the buffer to the free list;
- incomplete event: keep the buffer pending and allocate or reuse another;
- free buffer with sufficient capacity: reuse it for the next read.
The pool retains the three largest free buffers. Iterator exhaustion, early close, and producer or preparation errors close the producer and reclaim completed buffers. Queued CUDA operations retain their event-protected buffer leases even when preparation fails. A read failure returns its partial batch's buffers immediately.
Host memory use depends on the prefetch depth, the batch being prepared, reusable buffers, and native read scratch. Flat reads hold one decompressed plane per active Rayon task. Compact reads group requested levels by physical array and retain at most one physical array's scratch per concurrent time index.
Store requirements and format translation¶
- Flat OM4: canonical depth channels such as
thetao_4are physical array names passed directly to Rust. - Compact OM4:
NativeOm4Readermapsthetao_4to arraythetao, level4. Rust groups requested levels backed by the same physical array. - Values: native reads require unscaled float32 values and NaN missing-value
sentinels. Arrays with
scale_factor,add_offset, or non-NaN_FillValue/missing_valuesentinels are rejected before the native reader opens them. - Channels: the configured channels must map to stored physical variables.
Derived anomaly channels such as
hfds_anomaliesare rejected before datasets are opened or canonicalized.
Each batch belongs to one set of TrainingWindows, preserving a single source
pair and preparation policy throughout the batch.
Installation and tests¶
In a source checkout, install the optional extension with uv sync --extra rust.
The native crate and its standalone test environment live in
rust/loader:
uv sync --project rust/loader --python 3.12 --locked --group test
uv run --project rust/loader --locked --group test pytest -c rust/loader/pyproject.toml rust/loader/tests
# Embedded Python needs the test environment's NumPy on its import path.
export PYTHONPATH="$(uv run --project rust/loader --locked python -c 'import site; print(site.getsitepackages()[0])')"
PYO3_PYTHON="$PWD/rust/loader/.venv/bin/python" cargo test --manifest-path rust/loader/Cargo.toml --lib
cargo fmt --manifest-path rust/loader/Cargo.toml -- --check
cargo clippy --manifest-path rust/loader/Cargo.toml --all-targets -- -D warnings
Native Rust tests link Python. Wheel builds enable the extension-module feature
for the Python shared library (cdylib). The Rust CI workflow runs the Samudra
integration suite with the Rust extra installed; GPU CI covers pinned buffers
and CUDA prefetch.
Integration tests compare native and Python ModelBatch values for flat and
compact stores, history windows, rollout steps, strides, masks, normalization,
and different input/label grids. They also cover deterministic schedules and
cleanup after exhaustion, early exit, and read or preparation errors.
Native code and CPU targets¶
The repository's Cargo configuration sets a modern Linux
server baseline: x86-64-v3 on x86_64, and Neoverse V1 on aarch64. Rust flags tune
the extension and Rust dependencies; target-specific CFLAGS tune bundled native
codecs. Wheels built with these settings require compatible CPUs.
zarrs builds bundled C-Blosc sources through blosc-src. Read concurrency is
bounded by the shared Rayon pool.
Recorded performance¶
Environment: Torch gr101, 2x RTX6000, full v2 high-resolution Samudra model
with 84,002,154 parameters, /scratch/jr7309/data/om4_quarterdeg_v2, one year
of training samples, batch size 1, gradient accumulation 4, rollout steps [4],
and boundary variables tauuo,tauvo,hfds. Both runs used the same container
image, ghcr.io/m2lines/ocean-emulator-physicsnemo:26.05-ca4d907eb936d37641511f5adb40c3270dd5e6ee,
with NCCL_P2P_DISABLE=1.
The attempted two-epoch CPU/Rust matched job (14115987) was stopped after CPU
epoch 1 and part of CPU epoch 2 because CPU validation and epoch-2 first-batch
loads would not leave enough time for the Rust half in the one-hour allocation.
The table therefore compares the completed CPU epoch 1 from that job with a
completed Rust one-epoch run (14117615) submitted immediately afterward on the
same node.
| Loader | Job / run | Train epoch total | Mean iter excluding step 0 | Mean iter excluding step 0 and data waits >=1s | Median data wait excluding step 0 | Validation total | Peak CPU / GPU memory |
|---|---|---|---|---|---|---|---|
| CPU | 14115987 / qdeg-2gpu-ca4d907e-cpu-20260717-154900 |
22.31 s/it | 14.59 s | 4.12 s | 0.003 s | 81.61 s/it | 41.7 GB / 49.1 GB |
| Rust | 14117615 / qdeg-2gpu-ca4d907e-rust-only-20260717-163700 |
4.13 s/it | 3.24 s | 3.21 s | 0.002 s | 0.59 s/it | 16.3 GB / 53.9 GB |
This is a 5.4x train-epoch improvement and a 139x validation improvement for this specific one-epoch quarter-degree run. The narrower non-stall training number is only 1.3x faster, which matches the profile evidence: the Rust win is mostly eliminating CPU-loader cold loads and periodic stalls while keeping steady GPU compute roughly the same.