io.AsyncZarrBackend¶
Import path: earth2studio.io.AsyncZarrBackend
Documentation¶
Async Zarr v3 IO Backend
An asynchronous Zarr backend for inference pipelines that produce
data faster than a store can absorb it synchronously. Iteratively generated dimensions
(time, lead_time, ensemble) go into parallel_coords with their complete value sets,
each inference step writes one slice along them, and close() is called at the end
to drain pending writes and write out any incomplete shard. Remote stores are supported
through fs_factory, and Zarr v3 sharding via shard_coords keeps the file count of
large campaigns low.
Warning
This IO backend presently does not support overwritting existing Zarr stores. Only creation of new arrays or writing to existing.
Warning
Enabling sharding via shard_coords buffers chunks in host memory until a shard is
complete, trading host memory for a smaller file count. Budget roughly
max_inflight_shards * 4 * prod(shard_shape) * itemsize + pool_size * write_bytes
per process, since a flushing shard costs several times its own size once Zarr's
encoded copy is counted. Sharded writes are also slower than unsharded ones, which
is hidden as long as the model takes longer to produce a step than the store takes
to absorb it. This latency hiding only applies in non-blocking mode; with
blocking=True each shard flush runs synchronously.
Warning
When sharding, a shard must not contain data owned by more than one process. This
backend keeps every shard object to a single write by buffering its chunks, but that
only holds within a process, separate ranks have separate buffers. If two ranks each
hold part of the same shard they will both write it in full and the later write
wins, silently discarding the other's data. Shard along a coordinate that each rank
owns entirely (typically lead_time, since a rank runs a whole forecast), not along
the coordinate the work is distributed over.
Parameters:
-
file_name(str) –Path location to place zarr store
-
parallel_coords(CoordSystem) –Coordinates that enable parallel writes during inference. These coordinates specify which dimensions will be written in parallel via async operations, typically representing dimensions that are iteratively generated (such as time or lead_time). The chunk size for each of these dimensions will be set to 1. These coordinates should contain the complete set of values needed for the entire inference pipeline. The remaining coordinates of a given array will be populated upon the first write to the respective array.
-
fs_factory(Callable[..., AbstractFileSystem], default:LocalFileSystem) –FSSpec file system factory method. This is a callable object that should return an instance of the desired filesystem to use, by default LocalFileSystem
-
blocking(bool, default:True) –Blocking write calls in the synchronous API. When set to false, the IO backend will execute write calls in separate threads. Users should call the
close()API to ensure all threads have finished / cleaned up, by default True -
pool_size(int, default:8) –The thread / async loop pool used with the synchronous write API in non-blocking mode, by default 8
-
async_timeout(int, default:600) –Async operation timeout for a given write operation, by default 600. When sharding, the write that completes a shard carries the entire flush plus any wait for a concurrency slot, so this should be scaled with shard size and expected store throughput.
-
zarr_kwargs(dict[str, Any], default:{'mode': 'a'}) –Additional keyword arguments to provide to the
zarr.api.asynchronous.openfunction, by default {"mode": "a"} -
zarr_codecs(CompressorsLike, default:None) –Compression codec to use when creating any new arrays. If None, will use no compressor, by default None
-
chunked_coords(dict[str, int], default:{}) –Chunk sizes for coordinates that are not in
parallel_coords. By default any such coordinate is stored as a single chunk spanning its full length. Keys not present in a given array are ignored, by default {} -
shard_coords(dict[str, int], default:{}) –Number of elements per shard along the given coordinates, enabling Zarr v3 sharding. Each value must be a multiple of that coordinate's chunk size, and any coordinate not listed uses a shard size equal to its chunk size. See the Sharding notes below. By default, {} (unsharded).
-
max_inflight_shards(int, default:4) –Maximum number of shard flushes allowed to run at once. Concurrent flushes are what keep sharded write throughput up, at the cost of holding that many shards in memory. Lower it if memory is tight, raise it if writes are the bottleneck and the store has bandwidth to spare, by default 4
Raises:
-
ImportError–If Zarr 2.0 is installed. This io backend only supports Zarr 3.0
-
TypeError–If fs_factory is not a callable, this should be a callable method not an object
-
ValueError–If a
shard_coordsvalue is not positive
Notes
Relation to ZarrBackend
Exposes the same surface as ZarrBackend and can be used as a drop-in
replacement, with a few behavioral differences:
add_arraytakes adtypeinstead of a templatedatatensor and is idempotent, so it is safe to call from every rank of a distributed job.- In non-blocking mode a failed write raises at a later
write,flushorcloserather than at the failing call, and inputs are copied so callers may freely reuse their buffers. coordsis read back from the store, and__getitem__flushes pending writes first; intended for inspection, not reads in a write loop.- Consolidated metadata is not maintained; consolidate at the end of a
pipeline if desired, e.g.
zarr.consolidate_metadata(io.store).
Sharding
Because every coordinate in parallel_coords is chunked with a size of 1, a large
inference campaign can produce an enormous number of small files, which is a common
way to exhaust an inode quota on a parallel filesystem. Sharding packs many chunks
into a single storage object to avoid that. The chunk layout is unchanged, so
readers still fetch one chunk at a time and only the file count changes.
A shard is one object, so writing part of one would force Zarr to read, modify and rewrite all of it. To keep every shard to a single write, this backend accumulates a shard's chunks in host memory and writes it once complete, hence the memory and throughput tradeoffs in the warnings above.
Shard sizes need not divide evenly into a coordinate. close() writes out any shard
that never filled, using the array fill value where nothing was supplied, which
reads back exactly as an unwritten chunk would. Writing into a shard already present
in the store still works but falls back to a read-modify-write of the whole shard
and logs a warning. That happens when close() or flush() is called mid run and
the same shards are written again, or when restarting into a store left with
incomplete shards, so aligning restart boundaries with the shard size keeps writes
on the fast path.
Sharding composes with zarr_codecs, which compresses the inner chunks within a
shard, and with chunked_coords, which sets the chunk size of coordinates outside
parallel_coords.
Examples:
Write a forecast one lead time at a time, hiding the IO behind the model steps:
>>> times = np.array([np.datetime64("2024-01-01")])
>>> lead_times = np.array([np.timedelta64(6 * i, "h") for i in range(4)])
>>> io = AsyncZarrBackend(
... "forecast.zarr",
... parallel_coords={"time": times, "lead_time": lead_times},
... blocking=False,
... shard_coords={"lead_time": 4}, # optional: 4 chunks per storage object
... )
>>> total_coords = OrderedDict(
... {
... "time": times,
... "lead_time": lead_times,
... "lat": np.linspace(-90, 90, 721),
... "lon": np.linspace(0, 360, 1440, endpoint=False),
... }
... )
>>> io.add_array(total_coords, ["t2m", "z500"])
>>> for i in range(len(lead_times)):
... x = torch.randn(1, 1, 721, 1440) # model output for this step
... step_coords = total_coords.copy()
... step_coords["lead_time"] = lead_times[i : i + 1]
... io.write([x, x], step_coords, ["t2m", "z500"])
>>> io.close() # drain pending writes, write out any incomplete shard
add_array ¶
add_array(
coords: CoordSystem,
array_name: str | list[str],
dtype: Any = float32,
**kwargs: Any
) -> None
Create arrays and their coordinate arrays in the store
Arrays are otherwise created lazily on first write, which is per process: several processes writing the same array for the first time all see it as absent and race on creation. Creating the schema up front, from one process, avoids that race. It also allows arrays with different dimension sets, which no single first write could imply.
Parameters:
-
coords(CoordSystem) –Coordinate system of the array(s).
-
array_name(str | list[str]) –Name(s) of the array(s) to create.
-
dtype(dtype, default:float32) –Data type of the array(s), by default np.float32 (matching
ZarrBackend.add_arraywithout data). Arrays created lazily bywriteinstead use the written tensor's dtype. -
kwargs(Any, default:{}) –Accepted for
IOBackendcompatibility (e.g.ZarrBackend'sdata=) but not supported here, array content comes fromwriteand creation options from the constructor. Ignored with a warning.