I/O Optimization for Scientific Data Formats
dask_setup provides two levels of xarray chunking recommendations:
recommend_chunks()— simple, cluster-aware chunking based on worker memory and workload typerecommend_io_chunks()— storage-format-aware chunking optimised for Zarr, NetCDF, Zarr v3, Kerchunk, Parquet, and cloud storage
recommend_chunks() — Simple Chunking
recommend_chunks() targets 256–512 MiB chunks and respects the cluster’s per-worker memory limit (60% safety factor).
from dask_setup import setup_dask_client, recommend_chunks
client, cluster, dask_tmp = setup_dask_client("cpu")
# Cluster-aware recommendations
chunks = recommend_chunks(ds, client, verbose=True)
ds = ds.chunk(chunks)
# Without a client (uses psutil for memory)
chunks = recommend_chunks(ds, workload_type="cpu")
Enable chunking guidance at cluster setup time:
client, cluster, dask_tmp = setup_dask_client("cpu", suggest_chunks=True)
Or pass a dataset to get recommendations tuned to its structure:
ds = xr.open_zarr("era5.zarr")
client, cluster, dask_tmp, chunks = setup_dask_client(ds=ds, suggest_chunks=True)
ds_opt = ds.chunk(chunks)
Strategies by workload type:
Type |
Strategy |
|---|---|
|
Square-ish chunks — equal size across spatial dims for compute efficiency |
|
Stream-friendly — chunk along the record/time dimension, keep spatial dims whole |
|
Balanced — chunk spatial dims while keeping time dimension large |
|
Detect from dataset dimensions (time-like dim → |
This table is about chunk shape, which is a separate question from the worker topology the same
workload_typeselects. A"io"chunk shape is often right for NetCDF input even though an"io"topology is not — see Workload Types.
chunk_domain — Pin a Dimension Group to Memory
chunk_domain lets you declare which set of dimensions should be chunked and which should be fully loaded into memory (one chunk spanning the entire dimension, equivalent to ds.chunk({dim: -1})):
# chunk_domain="spatial" — chunk lat/lon, load all time steps at once
chunks = recommend_chunks(ds, chunk_domain="spatial")
ds = ds.chunk(chunks)
# → {"time": -1, "lat": 45, "lon": 90}
# chunk_domain="temporal" — chunk time, load full spatial grid per chunk
chunks = recommend_chunks(ds, chunk_domain="temporal")
ds = ds.chunk(chunks)
# → {"time": 30, "lat": -1, "lon": -1}
Value |
Dimensions chunked |
Dimensions fully loaded |
|---|---|---|
|
|
|
|
|
|
|
All dimensions |
None |
When to use chunk_domain="spatial": Workflows that reduce or aggregate across all time steps but need fine spatial resolution — e.g. computing a per-pixel climatology, fitting a trend at every grid point, or writing a spatially-partitioned Zarr store.
When to use chunk_domain="temporal": Workflows that process the full spatial domain at each time step — e.g. spatial statistics (global mean, field norm), regridding, or applying a spatial filter to each time slice.
recommend_chunks() classifies dimension names automatically:
Distinctive names match anywhere in the dimension name. Temporal:
"time","date","step","record","month","year","hour". Spatial:"lat","lon","level","depth","height","pressure","north"/"south"/"east"/"west". SonTimeis temporal andlatitude_1is spatial.Short names must match the whole dimension name, optionally with an
n/numprefix or a numeric suffix:"x","y","z","ni","nj","lev". Sox,nx,x_2andlev1are spatial — butproxy,flux,max,zoneandsizeare not.
A warning is emitted if no dimensions in the dataset match the requested domain.
Changed in v2.2. The short names were matched by substring too, which classified any dimension containing an
x,yorzas spatial. On a dataset with afluxorzonedimension that silently drove chunk sizing for an axis that has nothing to do with space.
Tip:
chunk_domaincan be combined with anyworkload_typeand withverbose=Trueto see the locked dimensions in the printed report.
Chunk Validation
validate_chunks() warns when existing chunking will produce chunks that are too large or too small relative to the cluster’s per-worker memory limit:
from dask_setup import validate_chunks
warnings = validate_chunks(ds, client)
for w in warnings:
print(w)
Chunk size is estimated as the product of the chunk sizes across all dimensions — i.e. the actual in-memory footprint of one chunk from the Dask task graph. For a dataset chunked {time: 24, level: 1, lat: 90, lon: 180} at float32, the estimate is 24 × 1 × 90 × 180 × 4 bytes ≈ 1.5 MiB per chunk, regardless of the full dataset extents.
Both validate_chunks() and the pre-recommendation check inside recommend_chunks() use this same logic — so datasets that are already well-chunked will not trigger spurious OOM warnings after calling recommend_chunks().
recommend_io_chunks() — Storage-Format-Aware Chunking
recommend_io_chunks() dispatches to a format-specific optimizer and returns an IORecommendation with chunks, compression settings, storage options, and throughput estimates.
from dask_setup import recommend_io_chunks
rec = recommend_io_chunks(ds, path_or_url="s3://bucket/climate.zarr", verbose=True)
print(rec.chunks)
print(rec.compression)
print(rec.estimated_throughput_mb_s)
ds_opt = ds.chunk(rec.chunks)
Supported Formats
Format key |
Detected by |
Optimizer |
|---|---|---|
|
|
|
|
|
|
|
fsspec |
|
|
|
|
|
|
see |
Format detection order: kerchunk → zarr_v3 → zarr → netcdf (kerchunk is checked first because it presents a zarr-like interface).
Zarr Optimization
Zarr (v2): 128–512 MiB chunks, zstd for cloud, lz4/blosc for local.
rec = recommend_io_chunks(
ds,
path_or_url="s3://climate-data/era5.zarr",
access_pattern="compute",
target_chunk_mb=(256, 512),
verbose=True,
)
Zarr v3: ZarrV3Optimizer handles the new zarr-python ≥ 3.0 API, including sharding via zarr.codecs.ShardingCodec. When the outer chunk exceeds 64 MiB, a sharding config (outer/inner shapes, index codec) is returned in rec.extra["sharding"].
rec = recommend_io_chunks(ds, path_or_url="data.zarr")
if "sharding" in rec.extra:
sharding_cfg = rec.extra["sharding"]
# sharding_cfg contains outer_chunks, inner_chunks, index_codec
ZarrV3Optimizer automatically selects blosc2:zstd or blosc2:lz4 when the blosc2 package is installed.
Best practices for Zarr:
Use 256–512 MiB target chunks for cloud; 128–256 MiB for local
Keep time dimension large for temporal analysis
Use consolidated metadata for faster initialisation
Consider
zstdfor archival,lz4for interactive analysis
NetCDF Optimization
NetCDF (HDF5 backend) is more conservative with chunk sizes due to HDF5 overhead.
rec = recommend_io_chunks(
ds,
path_or_url="data.nc4",
access_pattern="streaming",
target_chunk_mb=(64, 256),
)
Best practices for NetCDF:
64–256 MiB chunks (HDF5 performance degrades with very large chunks)
Be careful with unlimited dimensions (usually
time)Enable shuffle filter and
zlibcompression for good ratiosConsider least-significant-digit rounding for floating-point data
Kerchunk / VirtualiZarr
KerchunkOptimizer detects datasets opened via a Kerchunk or VirtualiZarr reference filesystem (fsspec.ReferenceFileSystem, ManifestArray, .json reference files). It returns the existing chunk layout unchanged — rechunking a Kerchunk dataset requires a full data copy and is usually not advisable.
import xarray as xr
from dask_setup import recommend_io_chunks
# Dataset opened via Kerchunk reference
ds = xr.open_zarr("combined.json", consolidated=False)
rec = recommend_io_chunks(ds, path_or_url="combined.json")
# rec.warnings will contain a note about fixed byte-range boundaries
print(rec.warnings)
Parquet / Arrow Recommendations
For Dask DataFrame workloads, recommend_parquet_chunks() estimates optimal rows_per_partition based on per-worker memory limits and row byte-size:
import dask.dataframe as dd
from dask_setup import setup_dask_client, recommend_parquet_chunks
client, cluster, _ = setup_dask_client("cpu")
df = dd.read_parquet("data/*.parquet")
rows = recommend_parquet_chunks(df, client)
df_opt = df.repartition(npartitions=len(df) // rows)
# For full details:
from dask_setup import ParquetRecommendation
rec = recommend_parquet_chunks(df, client, verbose=True)
print(rec.summary())
# rows_per_partition, compression, estimated_partition_mb,
# extra["row_group_size"], extra["write_metadata_file"]
Auto-selects compression: snappy for local storage, zstd for cloud. Warns on very wide tables (>500 columns).
Cloud Storage
AWS S3
rec = recommend_io_chunks(
ds,
path_or_url="s3://my-bucket/data.zarr",
access_pattern="sequential",
)
# rec.storage_options includes anon, default_cache_type, default_block_size
Google Cloud Storage
rec = recommend_io_chunks(ds, path_or_url="gs://my-bucket/data.zarr")
Azure Blob Storage
rec = recommend_io_chunks(ds, path_or_url="azure://container/data.zarr")
Access Patterns
Pattern |
Chunk guidance |
Use case |
|---|---|---|
|
Medium–large |
Time-series scans, linear pipelines |
|
Small–medium |
Interactive analysis, point lookups |
|
Small |
Real-time processing |
|
Large |
CPU-intensive reductions |
Chunk Size Reference
Storage |
Format |
Recommended Range |
|---|---|---|
Local SSD |
NetCDF |
64–256 MiB |
Local SSD |
Zarr |
128–512 MiB |
Network (NFS/Lustre) |
NetCDF |
32–128 MiB |
Network (NFS/Lustre) |
Zarr |
64–256 MiB |
Cloud (S3/GCS) |
NetCDF |
64–128 MiB |
Cloud (S3/GCS) |
Zarr |
128–512 MiB |
Rechunking
rechunk_dataset() rewrites an xarray Dataset to a new Zarr store with different chunk sizes. It handles temp store placement, error messaging, and cleanup automatically:
from dask_setup import rechunk_dataset
ds_rechunked = rechunk_dataset(
ds,
target_chunks={"time": 90, "lat": 360, "lon": 720},
client=client,
dask_tmp=dask_tmp, # uses fast local storage for intermediate store
)
Pass
dask_tmpas the temp store so the shuffle stays on fast local storage (e.g.$PBS_JOBFS) rather than shared filesystem.
rechunker compatibility
rechunk_dataset() uses the rechunker library when it is available and compatible. Newer xarray versions (≥ 2024.x) added a required zarr_format argument to an internal function that older rechunker releases do not pass. When this incompatibility is detected, rechunk_dataset() automatically falls back to a native xarray.to_zarr() path with no change to the user-facing API:
WARNING [rechunk] rechunker is incompatible with the installed xarray version
(extract_zarr_variable_encoding requires zarr_format).
Falling back to native xarray.to_zarr() rechunking.
INFO [rechunk] Rechunking complete (native fallback) ...
The native path is equally memory-safe for Dask-backed datasets — the scheduler writes one chunk at a time without materialising the full dataset. The max_mem parameter is ignored in fallback mode (Dask manages memory via its normal spill thresholds).
Supported Compression Algorithms
VALID_COMPRESSION_ALGORITHMS includes:
Standard:
"lz4","zstd","snappy","gzip","blosc","zlib","bz2","lzma","false"(disabled)blosc2 (zarr ≥ 3.0):
"blosc2","blosc2:lz4","blosc2:lz4hc","blosc2:blosclz","blosc2:zstd","blosc2:zlib","blosc2:snappy"
Integration Example — Climate Data Pipeline
import xarray as xr
from dask_setup import setup_dask_client, recommend_io_chunks, rechunk_dataset
client, cluster, dask_tmp = setup_dask_client(workload_type="io")
ds = xr.open_zarr("s3://climate-data/era5.zarr")
rec = recommend_io_chunks(
ds,
path_or_url="s3://climate-data/era5.zarr",
access_pattern="compute",
verbose=True,
)
ds_opt = ds.chunk(rec.chunks)
# Persist to a rechunked local store using fast scratch storage
ds_rechunked = rechunk_dataset(
ds_opt,
target_chunks=rec.chunks,
client=client,
dask_tmp=dask_tmp,
)
result = ds_rechunked.mean(["lat", "lon"]).compute()