Model Integration Guide¶
This guide describes how to integrate AMIO into an Earth system model (ESM) or coupler component for high-performance parallel I/O.
Integration Pattern¶
AMIO is designed as a drop-in I/O backend for model output and input. The typical integration pattern:
sequenceDiagram
participant M as Model (Fortran/C)
participant A as AMIO
participant S as Storage
Note over M,A: Initialization (once per run)
M->>A: amio_init(manifest)
M->>A: amio_open_dataset(write_config, WRITE)
M->>A: amio_open_dataset(read_config, READ)
Note over M,A: Timestep loop
loop Every output timestep
M->>A: amio_write(ds, "T", field, F32, shape)
M->>A: amio_write(ds, "U", field, F32, shape)
Note over M: Model continues computing immediately
end
loop Every input timestep
M->>A: amio_read(ds, "forcing", t, bbox, &view)
M->>A: amio_view_data(view, &ptr, &size)
Note over M: Use ptr directly (zero-copy from staging)
M->>A: amio_release_view(view)
end
Note over M,A: Finalization
M->>A: amio_flush(ds, 0)
M->>A: amio_close_dataset(ds)
M->>A: amio_finalize(core)
MPI Considerations¶
AMIO's NetCDF-4 backend uses parallel HDF5 with MPI-IO. The host
application must initialize MPI before calling amio_init:
MPI_Init_thread(&argc, &argv, MPI_THREAD_MULTIPLE, &provided);
// ... later ...
amio_init("manifest.yaml", &core);
AMIO does not call MPI_Init or MPI_Finalize — that is the host's
responsibility. The MPI_COMM_WORLD communicator is used for parallel
I/O by default.
Write Integration¶
Snapshot Semantics¶
The key property of amio_write is synchronous snapshot: the host
data is deep-copied into a staging buffer before the call returns. This
means:
- The host can immediately reuse or deallocate its buffer
- No coordination is needed between the model's compute loop and I/O
- The actual disk write happens asynchronously on worker threads
Minimal C Example¶
#include <amio/amio.h>
void write_model_output(amio_dataset_handle ds,
const float *temperature,
int nx, int ny, int nz) {
amio_shape_t shape = {0};
shape.rank = 3;
shape.extents[0] = nz;
shape.extents[1] = ny;
shape.extents[2] = nx;
amio_io_handle io = NULL;
amio_status_t rc = amio_write(ds, "temperature",
temperature, AMIO_DTYPE_F32,
&shape, &io);
if (rc != AMIO_OK) {
fprintf(stderr, "Write failed: %s\n", amio_strerror(rc));
}
// temperature buffer is safe to overwrite NOW
}
Minimal Fortran Example¶
subroutine write_model_output(ds, temperature, nx, ny, nz)
use amio_mod
use iso_c_binding
type(c_ptr), intent(in) :: ds
real(c_float), intent(in) :: temperature(nx, ny, nz)
integer, intent(in) :: nx, ny, nz
type(c_ptr) :: io_handle
integer(c_int32_t) :: rc
! amio_write_array is a Fortran convenience wrapper
call amio_write_array(ds, "temperature" // c_null_char, &
temperature, io_handle, rc)
if (rc /= AMIO_OK) then
print *, "Write failed: ", amio_strerror_f(rc)
end if
end subroutine
Read Integration¶
Prefetch Look-Ahead¶
AMIO prefetches upcoming timesteps in the background. When your model reads sequentially (t=0, 1, 2, ...), the data for t+N is already being fetched while you process t. This hides I/O latency:
Time →
Model: [compute t=0] [compute t=1] [compute t=2] ...
I/O: [fetch t=0,1,2,3] [fetch t=4] [fetch t=5] ...
↑ returned instantly (cached)
Selective Reads (Bounding Box)¶
For regional models or domain decomposition, use a bounding box to read only the sub-region you need:
amio_bbox_t bbox = {0};
bbox.rank = 2;
bbox.offsets[0] = my_y_start;
bbox.offsets[1] = my_x_start;
bbox.extents[0] = my_ny;
bbox.extents[1] = my_nx;
bbox.strides[0] = 1;
bbox.strides[1] = 1;
amio_view_handle view = NULL;
rc = amio_read(ds, "sst", timestep, &bbox, &view);
const void *data = NULL;
size_t nbytes = 0;
rc = amio_view_data(view, &data, &nbytes);
// data contains only the requested sub-region (my_ny × my_nx)
// nbytes == my_ny * my_nx * sizeof(float)
amio_release_view(view);
Strided reads are also supported (bbox.strides[d] > 1) for
coarsening/subsampling without transferring the full resolution.
Mixed-Format Translation¶
AMIO supports reading from one format and writing to another in the same run without external ETL:
// Read from NetCDF-4
amio_dataset_handle src = NULL;
amio_open_dataset(core, "netcdf_source.yaml", AMIO_MODE_READ, &src);
// Write to Zarr v3
amio_dataset_handle dst = NULL;
amio_open_dataset(core, "zarr_target.yaml", AMIO_MODE_WRITE, &dst);
// Read a variable
amio_view_handle view = NULL;
amio_read(src, "temperature", 0, NULL, &view);
const void *data = NULL;
size_t nbytes = 0;
amio_view_data(view, &data, &nbytes);
// Write the same bytes to the target format
amio_shape_t shape = {.rank = 2, .extents = {ny, nx}};
amio_io_handle io = NULL;
amio_write(dst, "temperature", data, AMIO_DTYPE_F32, &shape, &io);
amio_release_view(view);
amio_flush(dst, 0);
Both datasets share the same staging pool and worker threads. No intermediate copies or format conversions are needed.
Error Handling Best Practices¶
amio_status_t rc = amio_write(ds, var, data, dtype, &shape, &io);
switch (rc) {
case AMIO_OK:
break; // success
case AMIO_ERR_STAGING_BACKPRESSURE:
// Staging pool full — model is producing faster than I/O
// can drain. Options: increase buffer_count, or retry.
break;
case AMIO_ERR_BACKEND_FAILURE:
// Storage error — disk full, network down, etc.
break;
default:
fprintf(stderr, "Unexpected: %s\n", amio_strerror(rc));
}
Configuration for Model Runs¶
A typical HPC model run uses:
# manifest.yaml
staging_pool:
buffer_count: 16 # 2× concurrent variables
buffer_capacity_bytes: 52428800 # 50 MiB (fits one 3D field)
worker_pool:
threads: 4 # Dedicated I/O threads
prefetch:
depth: 8 # Look-ahead 8 timesteps
read_timeout_s: 120 # 2 min timeout for slow storage
staging_timeout_ms: 30000 # 30s backpressure timeout
backend: netcdf4
path: /scratch/model_output.nc
data_model: classic
codec:
active_codec: blosc
lossless_allow_list:
- blosc
Thread Safety¶
amio_init/amio_finalize: Call from a single thread (typically the main rank).amio_write: Thread-safe across different variables. Same variable writes are serialized by the worker pool's per-(dataset, variable) ordering.amio_read: Thread-safe for different variables. Same variable reads from multiple threads are serialized by the per-variable mutex.amio_flush/amio_close: Call from the same thread that opened the dataset.
Performance Tuning¶
| Parameter | Effect | Guidance |
|---|---|---|
buffer_count |
Concurrency headroom | 2× max overlapping writes |
buffer_capacity_bytes |
Max single-field size | ≥ largest variable payload |
worker_pool.threads |
I/O parallelism | 2–4 for local disk, 4–8 for cloud |
prefetch.depth |
Read look-ahead | 4–16 depending on access pattern |
staging_timeout_ms |
Backpressure patience | 5000–30000 ms |