Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Documentation
The documentation is now a Read the Docs–style site at https://21centuryweather.github.io/dask_setup/, built with Sphinx from
docs/and published by GitHub Actions. It adds an API reference generated from the docstrings and the changelog. The GitHub wiki now points to it.The tutorial and the ten recipe notebooks in
examples/are on the site, executed at build time so every page shows real output. A notebook that raises fails the docs build.
Fixed
examples/recipes/notebooks/05_xarray_chunking.ipynbhad three duplicate cell IDs, which nbformat rewrites with a warning on every load.
[2.3.0] - 2026-10-01
Added
setup_dask_client(ds=...)now warns whenworkload_type="io"is paired with NetCDF/HDF5 input."io"runs one process with many threads, which only helps when the reading library releases the GIL; HDF5 is usually not built thread-safe, so xarray serialises every read through a single process-wide lock and the threads queue instead of reading in parallel. The warning names"cpu"as the fix. Zarr input is unaffected — that is what"io"is for.dask_setup.xarray.detect_storage_format(ds)returns"netcdf","zarr"orNone, reading the source paths from both the dataset and per-variable encodings (open_mfdatasetonly records them on the variables).Python 3.14 is now tested in CI and listed in the package classifiers.
Changed
setup_dask_client()now defaults tomode="interactive"instead of"auto". A bare call uses the resources already allocated to the current job and never submits new ones. Under"auto", a call from inside a PBS or SLURM batch job resolved to"pbs"/"slurm"and submitted worker jobs via dask-jobqueue, which queued behind the job that was waiting for them. On a multi-node allocation you get anSSHClusteracross the allocated nodes. On a single node, or outside any job (a laptop, a login node), behaviour is unchanged: the sameLocalClusterasmode="local". Passmode="auto"to get the old behaviour back.recommend_chunks()(andsetup_dask_client(ds=...)) now grows under-chunked input forworkload_type="cpu"and"mixed". Both strategies started from the dataset’s current chunks and could only halve them, so a dataset chunked one day per step (4 MiB) was recommended{"time": 1}, the chunking it already had, alongside a warning that it was too small. Chunks are now doubled up to the lower bound oftarget_chunk_mb(never past the per-worker memory cap). Doubling keeps each recommendation a whole multiple of the existing chunk, so reads stay aligned with on-disk chunks. Over-chunked and unchunked input get the same recommendations as before.The “large task graph” chunk warning now fires above 100,000 chunks per variable instead of
max(n_workers * 4, 32). The old threshold flagged correctly sized chunks: a 28 GiB dataset split into 60 chunks of about 360 MiB triggered it.
Fixed
mode="interactive"on a single allocated node silently ignoredfallback_on_detection_failure,adaptive_memory, and dataset-basedworkload_type="auto"inference. It now takes the local path directly, so these apply.The
InsufficientResourcesErrorraised when nothing is left afterreserve_mem_gbgave impossible advice, e.g. “Reduce reserve_mem_gb from 999.0 GB to -294.9 GB” and “Limit max_workers” (which can’t help, because workers are already fitted to memory by then). It also ignoredmax_mem_gbwhen reporting available memory. It now suggests the largest reserve that would work, or raisingmax_mem_gbwhen that cap is the cause. The shortfall it reports is never negative.The dashboard SSH tunnel hint no longer prints a reverse-DNS name like
1.0.0.[...].ip6.arpaas the login host.socket.getfqdn()returns one on macOS and on hosts without forward DNS. These now get the<login-node>placeholder, as hosts without a domain already did.Anything that summed or counted workers saw at most 5 of them.
distributed2025 changedClient.scheduler_info()to describe only the first 5 workers unless asked for all. On any node running more workers,cluster_report()undercounted memory and spill,tune_memory_thresholds()decided from partial spill figures,recommend_chunks(),recommend_parquet_chunks()and the benchmarks reported 5 workers, and the multi-node backends logged 5 connected workers. All of these now go throughdask_setup.reporting.scheduler_workers(), which fetches every worker (and still works on olderdistributed).cluster_report()counted spilled data twice, once as memory and once as spill, becausemanaged_bytesincludes data already on disk. After heavy spilling it reported workers holding more than their memory limit.memory_per_worker_gibnow covers only data held in memory (dask_setup.reporting.worker_in_memory_bytes()).rechunk_dataset()failed on zarr 3 instead of falling back toxarray.to_zarr(). rechunker 0.5 uses the zarr 2 API, and the fallback only recognised the older xarrayzarr_formatincompatibility. Real failures (disk full, out of memory) still raise.The “Starting Dask client setup” log line reported the library default
workload_type="io"whenever the type came fromconfig=orprofile=. It is now logged after configuration is resolved.The sdist now ships
tests/conftest.pyandtests/__init__.py. setuptools only auto-includestests/test*.py, so running the suite from the sdist (as downstream packagers do) failed with missing fixtures.tests/test_error_handling.pyimportedsrc.dask_setupinstead ofdask_setup, so it failed against an installed package, and in a checkout it loaded a second copy of the package — itspatch()targeted that copy rather than the module under test.Documentation recommended
"io"for “opening many NetCDF/Zarr files concurrently”, which is right for Zarr and backwards for NetCDF; andInternals.mddescribed the"auto"classifier’s signals the wrong way round (CF dimension names liketime/lat/lonscore toward"cpu", not"io").The recipe notebooks (
examples/recipes/notebooks/) all run again against current dask, xarray and zarr 3, and no longer contradict the library. Among other fixes, they no longer recommend"io"for NetCDF or call h5netcdf GIL-free.reserve_mem_gbvalues that exceeded a laptop’s RAM are gone, andlz4, which stops workers starting unless installed, is replaced byspill_compression="auto". The memory recipe now measures real spill instead of filesystem usage, and the HPC recipe usesMultiNodeConfigandgenerate_pbs_script()instead of re-implementing them.examples/recipes/configs/cpu_profile.yamlhad its settings at the top level instead of underconfig:, so loading it silently ignored every one of them and used the defaults.
[2.2.0] - 2026-08-27
Correctness release. Most of these are settings that were accepted, validated and then silently discarded — the library reported doing one thing and did another. Several fixes therefore change behaviour; they are listed separately below.
Fixed
Configuration reaching the cluster
Explicit arguments to
setup_dask_client()were discarded when they equalled a default. Whether an argument had been supplied was decided by comparing it against the default value, soworkload_type="io",dashboard=Trueorreserve_mem_gb=50.0were indistinguishable from “not supplied” and were dropped in favour of the profile. Every configuration parameter now defaults toNoneas a real sentinel, andDaskSetupConfig.merge_overrides()applies only the keys the caller actually named.profile=andconfig=never reached the PBS, SLURM or interactive backends. Those modes returned before configuration was resolved.setup_dask_client(ds=...)raisedValueError: not enough values to unpackon the multi-node paths, which returned a 3-tuple even when a dataset was supplied. All modes now return a 4-tuple withds=and a 3-tuple without.get_profile()checked builtins before user profiles, sodask-setup show <name>displayed your profile whilesetup_dask_client(profile=<name>)used the builtin. Both now resolve user → site → builtin.MultiNodeConfigoverrides droppedenv_extraandscheduler_options, because they round-tripped throughto_dict(), which omits both.module loadlines vanished from generated worker job scripts.
Memory and cluster sizing
Multi-node jobs under-provisioned workers by a factor of
processes.coresandmemoryare per-job indask-jobqueue, which divides both byprocesses; the per-worker figures were being passed instead. A 4×12-core, 4×32 GiB job started workers with--nthreads 3 --memory-limit 7.45GiBrather than--nthreads 12 --memory-limit 29.80GiB, against thencpus=48,mem=128GBit had reserved.reserve_mem_gbwas defeated by the per-worker memory floor. The 1 GiB minimum was applied after the split, so 64 workers on a 64 GiB node with a 50 GiB reserve each got 1 GiB — 64 GiB committed against 14 GiB usable. The worker count is now fitted to memory first, and the reduction is logged.tune_memory_thresholds()had no effect on running workers. It wrotedask.config, butWorkerMemoryManagerreads the thresholds once at construction and sizes theSpillBuffer’s eviction threshold from them at the same moment. Thresholds are now applied to the live manager and buffer.Spill volume always read
0.0. The worker metric keys queried (spilled_memory,spill) are not published by currentdistributed; the key isspilled_bytes, whose value is a{"memory", "disk"}mapping. This also meanttune_memory_thresholds(strategy="auto")could never reach itsloosenbranch.SLURM_MEM_PER_NODE=0— SLURM’s encoding for “no memory limit requested” — parsed to a literal zero-byte budget and suppressed the psutil fallback, which was guarded onis None. Same bug on the PBS path. Non-positive core counts are likewise rejected so detection falls through instead of producing an unusable spec.
Hangs, crashes and dead code
recommend_chunks()could loop forever on amixedworkload when every spatial dimension was already at a chunk size of 1: the reduction step chose from unfiltered candidates andmax(1, 1 // 2) == 1never made progress.Circular profile inheritance recursed until
RecursionError, producing a 57 KB error message. The chain was threaded intoload_profile_from_filebut parents resolved viaget_profile, which restarted it empty, leaving both the cycle check and the depth cap dead. Cycles now raiseInvalidConfigurationError: Circular profile inheritance detected: a -> b -> a.configure_logging()was a no-op after import, so there was no way to enable debug logging: it returned early if logging was already configured, and importing anydask_setupmodule configures it. The documentedDASK_SETUP_LOG_LEVEL,DASK_SETUP_LOG_FORMATandDASK_SETUP_LOG_COLORvariables were never read by anything.ErrorContextcrashed while building an error report if zarr or netCDF4 were installed but broken — onlyImportErrorwas caught, and a missing shared library raisesOSError.create_dask_temp_dir()nested on repeat calls (dask-<pid>/dask-<pid>) because it overwroteTMPDIRwith its own result and read it back next time.
Benchmarking
scaling_analysis()swept a topology that cannot scale. The default config leftworkload_type="io", for whichdecide_topology()pinsn_workers=1regardless ofmax_workers— so every point built the same one-worker cluster and the “scaling curve” was timing noise. The default is now"cpu", and a sweep whose worker count never changes logs a warning.Parallel efficiency divided by the absolute worker count rather than the worker ratio, so a perfectly scaling
(4, 8)sweep scored0.25at its own baseline instead of1.0.peak_memory_gibwas not a peak. It came from a cluster report taken after.compute()returned, by which point the workers had released the data. Memory is now sampled every 0.2 s during the timed runs.
Smaller fixes
Generated job scripts quote interpolated paths (
shlex.quote), so ashared_tmp_diror script path containing a space no longer breaks the script.dask-setup create-profile --from-profilecopied by reference and renamed the source profile in place — for a builtin, for the rest of the process.Dimension classification no longer matches
"x","y","z","ni","nj"and"lev"as substrings, which madeproxy,flux,maxandzonespatial dimensions and let them drive chunk sizing.The dashboard SSH tunnel hint no longer hardcodes
gadi.nci.org.au; it is inferred from the node’s DNS domain and can be set with$DASK_SETUP_LOGIN_HOST._rechunk_native()no longer claims memory safety it cannot provide, and warns when a rechunk enlarges chunks (the unbounded case).PBS cluster construction no longer emits a
FutureWarningon every launch (project→account,env_extra→job_script_prologue).
Changed
These change observable behaviour. Explicit settings you already pass still win in every case.
reserve_mem_gbnow defaults to 20% of RAM, clamped to [4, 50] GiB, capped at half the machine with a 1 GiB floor. The machine-aware default was written in v1.1 but never called, so every host got a flat 50 GiB — which on a 16 GiB laptop reserves more than the whole machine.silence_logs=False(the default) now leaves Dask at its ownWARNINGlevel instead ofERROR. The setting was collected and validated but never passed tocreate_cluster, so worker warnings — memory pressure, paused workers — were always suppressed.env_extralines are emitted verbatim in generated job scripts, matching howdask-jobqueuetreats them. They used to be prefixed withexport, which turnedmodule load condaintoexport module load condaon that path while thedask-jobqueuepath ran it correctly. If you relied on the implicit prefix for bareFOO=barentries, addexportyourself.A user profile now shadows a builtin of the same name in
get_profile(), matching whatlist_profiles()anddask-setup listalready displayed.PBS/SLURM modes warn about settings the backend cannot apply (
reserve_mem_gb, spill compression, …) instead of dropping them silently.
Documentation
Corrected the claim that
profile=andconfig=compose. They occupy the same precedence layer: whichever is passed supplies the base, and if both are passed the profile wins outright. Layering them would require knowing which fields a profile’s YAML actually set, whichConfigProfiledoes not record.README and wiki updated for every behaviour change above; added documentation for log configuration,
env_extrasemantics and the per-job resource model.
Internal
100 new tests (946 total, up from 784), including new suites for
reporting,tune,loggingandrechunk, which had none.Release workflow now runs the test matrix, verifies the tag matches
pyproject.toml, runstwine check --strict, and creates a GitHub Release before publishing to PyPI — all triggered by pushing av*tag.New
multinodeoptional extra:pip install dask_setup[multinode]pulls indask-jobqueue, which previously had to be installed separately and was not declared anywhere. It is now also adevdependency, so CI actually exercises the multi-node path instead of silently skipping it.Tests no longer read
Worker.nthreads/Nanny.memory_limit, which recentdistributedremoved after deprecating them; they usestate.nthreadsandmemory_manager.memory_limitwith a fallback to the old names. (Note thatWorker.threadsis the thread registry dict, not a replacement count.)ruffis pinned exactly (== 0.16.4) in thedevextra. It was~= 0.4, which permits everything below 1.0, so CI installed whatever had been released that morning; because ruff’s formatter changes between minor versions, theruff format --checkgate could fail on a commit that touched nothing.
[2.0.0] - 2026-03-23
Added
MultiNodeConfigdataclass (multinode.py). Companion toDaskSetupConfigfor multi-node PBS/SLURM jobs. Fields includeworkload_type,workers_per_node,cores_per_worker,mem_per_worker_gb,walltime,queue,project,job_extra_directives,n_nodes,shared_tmp_dir,env_extra,adaptive,min_jobs, andmax_jobs. Providestotal_cores_per_job,total_mem_gb_per_jobproperties and ato_dict()serialiser. Full validation on construction.setup_pbs_cluster(config, **kwargs)function (multinode.py). Wrapsdask-jobqueue.PBSClusterwithMultiNodeConfigdefaults, shared temp directory support, and adaptive scaling. Returns(client, cluster, shared_tmp).setup_slurm_cluster(config, **kwargs)function (multinode.py). Mirrorssetup_pbs_clusterfor SLURM viaSLURMCluster.SharedTempDirdataclass (multinode.py). Wraps a Lustre/GPFS shared filesystem path for stores that must be visible to all workers (e.g. Rechunker targets). Optionally creates a job-specific subdirectory (dask_tmp_<JOBID>) and supports cleanup on close. Implements__fspath__for transparent use as aPath.detect_cluster_mode()function (multinode.py). Inspects the environment for SLURM (SLURM_JOB_ID, etc.) and PBS (PBS_JOBID, etc.) indicators and returns"slurm","pbs", or"local".generate_pbs_script(cfg, script_path, …)function (multinode.py). Generates a ready-to-run#PBSjob script string from aMultiNodeConfig.generate_slurm_script(cfg, script_path, …)function (multinode.py). Generates a#SBATCHjob script string.mode=parameter onsetup_dask_client()(client.py). Accepts"local","pbs","slurm", or"auto"(default). In"auto"mode, callsdetect_cluster_mode()and dispatches to the appropriate backend. Multi-node modes return a(client, cluster, tmp_path)3-tuple.multi_node_config=parameter onsetup_dask_client()(client.py). AMultiNodeConfiginstance to use when a multi-node mode is selected. WhenNone, a minimal config is built fromworkload_type.workload_type="gpu"topology (topology.py). New topology for CUDA-accelerated workloads. Detects GPU count viaCUDA_VISIBLE_DEVICESorcupy. Configures one worker process per GPU withceil(total_cores / n_gpus)CPU threads (clamped to 2–8). Falls back gracefully to a single-threaded worker when no GPUs are detected, with a warning.DaskSetupConfig.VALID_WORKLOAD_TYPESupdated to include"gpu"._count_gpus()helper (topology.py). Counts CUDA-capable GPUs fromCUDA_VISIBLE_DEVICES(falls back tocupy.cuda.runtime.getDeviceCount()).dask-setup submitCLI subcommand (cli.py). Generates PBS or SLURM job scripts from the command line. Key flags:--scheduler(pbs/slurm),--workload-type,--workers-per-node,--cores-per-worker,--mem-per-worker,--walltime,--queue,--project,--n-nodes,--shared-tmp-dir,--extra-directive(repeatable),--python,--output.All new v2.0 symbols exported from the top-level package (
__init__.py):MultiNodeConfig,SharedTempDir,detect_cluster_mode,setup_pbs_cluster,setup_slurm_cluster,generate_pbs_script,generate_slurm_script.
Changed
setup_dask_client()gainsmode="auto"andmulti_node_config=Noneparameters. Existing callers are unaffected (defaults preserve previous behaviour).DaskSetupConfig.VALID_WORKLOAD_TYPESnow includes"gpu".decide_topology()andvalidate_topology()updated to handle"gpu"workload type. The high-threads warning invalidate_topologyis suppressed for GPU topologies.
Notes
Multi-node PBS/SLURM support requires
pip install dask-jobqueue. The package imports cleanly without it; a helpfulImportErroris raised only whensetup_pbs_cluster/setup_slurm_clusterare called.GPU topology requires
pip install cupy-cudaXXX(matching your CUDA version) for GPU auto-detection; falls back gracefully when CuPy is absent.
[1.8.0] - 2026-03-23
Added
benchmark_config(configs, ds, operation)function (benchmark.py). A/B-tests multipleDaskSetupConfigobjects against the same xarray operation. Each config gets a fresh cluster, and results are collected asBenchmarkResultobjects with wall time, memory usage, spill volume, and tasks/second. Accepts 1–5 timed repeats and logs per-repeat progress. Returns a list ofBenchmarkResultentries in the order supplied.scaling_analysis(ds, operation, worker_counts, …)function (benchmark.py). Sweeps worker counts (default[1, 2, 4, 8]) using the same config and dataset, computes speedup and parallel efficiency at each count, and returns aScalingResultcontaining per-countBenchmarkResultobjects plus speedup/efficiency lists. Optionalplot=Truerenders a matplotlib figure with dual axes (wall time and speedup).chunk_impact(ds, client, operation, chunk_sizes, …)function (benchmark.py). Fixes a running cluster and sweeps a list of chunk-size dicts (or generates them automatically with_generate_auto_chunks). Returns aChunkImpactResultwith per-specBenchmarkResultobjects. Optionalplot=Truerenders a wall-time curve.BenchmarkResultdataclass (benchmark.py). Shared result carrier for all three analysis functions. Fields:name,wall_time_seconds,wall_time_std,peak_memory_gib,spill_gib,n_tasks,n_workers,tasks_per_second,errors,extra. Hassummary_line()for one-line console output andto_dict()for JSON-serialisable output.ScalingResultdataclass (benchmark.py). Returned byscaling_analysis(). Provideswall_timesproperty,best()method,summary()multi-line table, and optionalplot()method.ChunkImpactResultdataclass (benchmark.py). Returned bychunk_impact(). Providesoptimal()method,summary()table, and optionalplot()method.run_synthetic_benchmark(profile, operation, ds_size, repeats)function (benchmark.py). Runs a benchmark against a named profile using onlydask.array— no xarray dataset required. Supports four dataset sizes (tiny,small,medium,large). Returns aSyntheticBenchmarkResultwith a human-readablesummary().dask-setup benchmarkCLI subcommand (cli.py). Runsrun_synthetic_benchmarkfrom the command line. Flags:--profile(defaultdevelopment),--operation(defaultmean),--size(defaultsmall),--repeats(default1). Prints theSyntheticBenchmarkResult.summary()to stdout.BenchmarkResult,ScalingResult,ChunkImpactResult,benchmark_config,scaling_analysis,chunk_impact, andrun_synthetic_benchmarkexported from the top-level package (__init__.py). Import guarded so the package degrades gracefully whendask.distributedis unavailable.
[1.7.0] - 2026-03-23
Added
Profile inheritance (
based_on:key). YAML profiles now accept abased_on: <parent_name>field. Only the fields listed underconfig:in the child profile override the parent’s values; everything else is inherited. Works with builtin, site-wide, and user profiles. Circular chains and chains exceeding 16 levels are detected and rejected with a descriptive error.ConfigProfilegainsbased_on: str | Noneandprofile_version: str | Nonefields.Site-wide profiles.
ConfigManagernow loads profiles from/etc/dask_setup/profiles/(or the directory pointed to by$DASK_SETUP_PROFILE_DIR) between builtins and user profiles. Site admins can ship system-optimised defaults that users can override locally.ConfigManager.__init__accepts a newsite_profiles_dirparameter.Profile versioning. Every profile saved by
save_profile()now carries aversion: "1.7"field (the currentPROFILE_FORMAT_VERSIONconstant).load_profile_from_file()emits aUserWarningwhen loading a profile whose version string is newer than the installed package understands.PROFILE_FORMAT_VERSIONis exported from the top-level package.Profile import via URL / local file (
dask-setup import). New CLI subcommand:dask-setup import <URL_OR_PATH> [--name <name>] [--force]. Supports HTTP/HTTPS URLs and local file paths. Uses only stdliburllib.request— no extra dependencies.ConfigManagerexposes the correspondingimport_profile_from_url(url, name_override, force)method.JSON Schema for profiles (
dask-setup schema). A JSON Schema (draft-07) describing the profile YAML format is now shipped indask_setup/schema/profile_schema.json.ConfigManager.get_profile_schema()returns it as a Python dict.dask-setup schema [--output file]prints it to stdout or writes it to a file for use with editors.PROFILE_SCHEMAis exported from the top-level package.
Changed
ConfigManager.list_profiles()now merges profiles in three layers — builtins < site-wide < user — so user profiles always win on name conflicts.ConfigManager.get_profile()searches user → site-wide → builtin (same precedence order aslist_profiles).format_profile_details()in the CLI now showsBased on:andFormat version:lines when present.
[1.6.0] - 2026-03-23
Added
ZarrV3Optimizerclass (io_patterns.py). Handles the zarr-python ≥ 3.0 API, including sharding viazarr.codecs.ShardingCodecand the updated codec pipeline. Detects v3 stores viazarr.jsonmetadata or thezarr_format=3store attribute. When the outer chunk exceeds 64 MiB, a sharding config (outer/inner shapes, index codec) is returned inIORecommendation.extra["sharding"]. Preferred overZarrOptimizerwhen zarr-python ≥ 3.0 is detected.KerchunkOptimizerclass (io_patterns.py). Detects datasets opened via Kerchunk or VirtualiZarr reference stores (fsspecReferenceFileSystem,ManifestArray,.jsonreference files). Returns the existing chunk layout unchanged — rechunking would require a full data copy — and adds an informational warning about fixed byte-range boundaries.detect_storage_format()extended. Now returns"zarr_v3"for Zarr v3 stores and"kerchunk"for Kerchunk/VirtualiZarr reference datasets. Ordering ensures kerchunk is checked before zarr (kerchunk presents a zarr-like interface), andZarrV3Optimizeris preferred overZarrOptimizerwhen v3 is detected.recommend_io_chunks()dispatches to new optimizers. Routes"zarr_v3"toZarrV3Optimizerand"kerchunk"toKerchunkOptimizer. PopulatesIORecommendation.extra["sharding"]for Zarr v3 stores when sharding is appropriate. Adds a Kerchunk-specific warning when that format is detected.IORecommendation.extrafield. Newdict[str, Any]field onIORecommendationfor format-specific extras. Currently populated byZarrV3Optimizerwith sharding configuration when applicable.recommend_parquet_chunks(df, client, …)helper (parquet.py). Parquet / Arrow partition-size recommendations for Dask DataFrame workloads — analogous torecommend_io_chunks()for xarray datasets. Estimates bytes-per-row frommemory_usage()or dtype sizes, respects per-worker memory limits when aclientis provided, auto-selects compression (snappylocal,zstdcloud), and warns on wide tables. Returnsrows_per_partitionby default or a fullParquetRecommendationwhenverbose=True.ParquetRecommendationdataclass (parquet.py). Returned byrecommend_parquet_chunks()whenverbose=True. Fields:rows_per_partition,compression,storage_options,estimated_partition_mb,warnings,extra(includesrow_group_sizeandwrite_metadata_filehints). Has.summary().blosc2 codec variants in
VALID_COMPRESSION_ALGORITHMS. Added"blosc2","blosc2:lz4","blosc2:lz4hc","blosc2:blosclz","blosc2:zstd","blosc2:zlib", and"blosc2:snappy"— first-class codecs in Zarr v3 / blosc2 package.ZarrV3Optimizerautomatically usesblosc2:zstdorblosc2:lz4whenblosc2is importable.VALID_IO_FORMATSextended. Now includes"zarr_v3","kerchunk", and"parquet"alongside the existing"zarr"and"netcdf"entries.
Changed
ZarrV3Optimizer,KerchunkOptimizer,ParquetRecommendation, andrecommend_parquet_chunksare now exported from the top-level package.
[1.5.0] - 2026-03-23
Added
workload_type="auto"insetup_dask_client(). New sentinel value forworkload_type. When passed,setup_dask_client()callsinfer_workload_type(ds)to choose between"cpu","io", or"mixed"automatically. Falls back to"mixed"if no dataset is provided. Accepted byDaskSetupConfigso it can be stored in profiles.infer_workload_type(ds)helper (workload.py). Inspects an xarray Dataset or DataArray’s dimension names (time, lat, lon, lev …), variable dtypes (float-dominant → CPU; int/bool-dominant → I/O), and bytes-per-variable ratio. Returns"cpu","io", or"mixed". Defaults to"mixed"when evidence is ambiguous (score margin ≤ 2) ords=None. Exported from the top-level package.tune_memory_thresholds(client, strategy="auto", …)(tune.py). One-shot dynamic memory threshold adjustment. Reads current spill volume from the scheduler; tightens workermemory.target/memory.spillwhen spill is low (extra head-room), loosens them when spill is heavy (less disk write amplification). Strategy can be forced with"tighten","loosen", or disabled with"off". ReturnsMemoryTuneResultwith.summary()and per-field detail. Exported from the top-level package.MemoryTuneResultdataclass (tune.py). Returned bytune_memory_thresholds(). Fields:strategy,old_target,new_target,old_spill,new_spill,spill_gib_observed,rationale,workers_updated. Exported from the top-level package.DaskSetupConfig(adaptive_memory=True)opt-in. When set, callstune_memory_thresholds(client, strategy="tighten")immediately after the cluster is created so workers start with tighter thresholds from the first task. Also accepted as a keyword argument tosetup_dask_client().register_worker_callbacks(client, on_worker_death=…, on_worker_added=…)(callbacks.py). Installs aSchedulerPluginthat fires user callables when workers join or leave the cluster. Worker address is passed as the sole argument. Exceptions inside callbacks are caught and logged so a buggy callback cannot crash the scheduler. Exported from the top-level package.profile="auto"insetup_dask_client(). Inspects resources and environment then delegates toConfigManager.auto_select_profile(). Selection rules (first match): Jupyter →"interactive"; small machine (≤ 8 cores or ≤ 16 GiB) →"development"; PBS + JOBFS + ≥ 16 cores →"zarr_io_heavy"; large HPC (≥ 48 cores or ≥ 128 GiB) →"climate_analysis"; general HPC →"production"; fallback →"development".ConfigManager.auto_select_profile(resources). New public method implementing the profile auto-selection logic above.
[1.4.0] - 2026-03-23
Added
Post-run cluster reporting (
reporting.py). Newreportingmodule with two public exports:ClusterReport(a dataclass snapshot of cluster metrics) andcluster_report(client, start_time=None)(the collection function).ClusterReportfields:wall_time_seconds,memory_per_worker_gib,total_spill_gib, andtotal_tasks. Derived properties (wall_time_str,peak_memory_gib,total_memory_gib) and helper methods (summary_line(),to_dict()) are also available. All metric collection is defensive — fields that cannot be retrieved from the scheduler (due to Dask version differences) are left at their zero/empty defaults rather than raising. Both symbols are exported from the top-level package.Cluster summary on context-manager exit.
DaskClientContext.__enter__now records the wall-clock start time viatime.monotonic().DaskClientContext.__exit__callscluster_report()before closing the client and emits the result as a singlelogger.info()line ("Cluster run summary: wall=… | workers=… | mem=… GiB/max | …"). This gives users an automatic post-run summary without any extra code.Environment summary in
ClusterSetupError.ClusterSetupErrornow overrides_format_error_message()to append a🔍 Environment:section (produced byErrorContext.get_environment_summary()) to the formatted exception message. The section includes platform, Python version, memory, CPU count, and detected package versions — making it immediately visible in tracebacks and log output without separately callingget_diagnostic_info().
[1.3.0] - 2026-03-23
Added
Jupyter / IPython detection (
environment.py). Newenvironmentmodule with two public helpers:is_jupyter()(cached boolean) andget_environment_type()(returns"jupyter","ipython", or"script"). Detection is based on the IPython shell class name (ZMQInteractiveShellcovers JupyterLab, classic Notebook, VSCode notebooks, and Google Colab). Both functions are exported from the top-level package.Clickable dashboard link in Jupyter.
print_dashboard_info()indashboard.pynow checksis_jupyter()at call time. When inside a Jupyter kernel, it usesIPython.display.HTMLto render a styled, clickable anchor tag instead of printing the SSH tunnel hint. Falls back to the plain-text message ifIPython.displayis unavailable despite being in a Jupyter environment. The newdisplay_jupyter_dashboard(client)function is also available for direct use.Graceful resource detection degradation (
fallback_on_detection_failure).setup_dask_client()now acceptsfallback_on_detection_failure=True(also settable viaDaskSetupConfig). When enabled and all resource detection methods fail (SLURM, PBS, and psutil all raise), a conservative hardcoded fallback is used (2 cores, 8 GiB) rather than raisingResourceDetectionError. A warning is logged identifying the fallback. DefaultFalsepreserves existing behaviour. Thedetect_resources()function inresources.pygains the correspondingfallback=parameter.
[1.2.0] - 2026-03-23
Added
setup_dask_client(ds=)parameter. Pass an xarray Dataset or DataArray tosetup_dask_client()to get automatic chunk recommendations for that specific dataset and cluster configuration. Whends=is provided, the function returns a 4-tuple(client, cluster, tmp, chunks)where chunks is adict[str, int]ready to pass tods.chunk(chunks).suggest_chunksnow returns a value. Whends=is provided andsuggest_chunks=True, the detailed chunk report is printed and the chunk dict is returned in the 4-tuple. Withoutds=,suggest_chunks=Truecontinues to print generic cluster-based guidance.validate_chunks(ds, client). New standalone function that compares a dataset’s existing chunk sizes to the cluster’s per-worker memory limit and emitsUserWarningfor chunks that are too large (OOM risk) or too small (task overhead). Called automatically whends=is passed tosetup_dask_client().rechunk_dataset(ds, target_chunks, client, dask_tmp). New rechunking helper that wraps the optionalrechunkerlibrary. Routes both the intermediate temp store and the output Zarr store todask_tmp(typically$PBS_JOBFS) for fast local I/O. Handles temp-store cleanup and provides clear error messages. Requires:pip install rechunker zarr.ChunkRecommendationexported. TheChunkRecommendationdataclass fromxarray.pyis now part of the public API and included in__all__.DaskClientContextsupports 4-tuple unpacking. Whends=is in the context-manager kwargs,__enter__returns(client, cluster, tmp, chunks). Thechunksattribute on the context object is also set.
Changed
setup_dask_clientreturn type is now conditional. Mypy/pyright users benefit from@overloadstubs: passingds=None(default) infers a 3-tuple; passing a dataset infers a 4-tuple.
[1.1.0] - 2026-03-23
Added
Context manager support.
DaskClientContextclass allowswith DaskClientContext(...) as (client, cluster, tmp):so clusters are always closed cleanly, even on exception. Exported from the top-level package.Smart
reserve_mem_gbdefault. The default is now computed from the running system’s total RAM: 20 % of total RAM, clamped to [4 GiB, 50 GiB]. This replaces the previous hardcoded 50 GiB default which failed immediately on laptops and small VMs. Explicit values still override the smart default. TheDaskSetupConfigdataclass retains 50.0 as its own field default for users who instantiate it directly.py.typedmarker. The package now ships apy.typedfile (PEP 561) so that mypy, pyright, and other type checkers recognise it as fully typed.Python 3.13 in CI matrix. Tests now run on Python 3.11, 3.12, and 3.13.
Changed
Structured logging throughout.
resources,cluster,topology, andclientmodules now use the package’s ownDaskSetupLogger(viaget_logger()) instead of a mix of bareprint()calls and stdlib loggers. Setup progress — resource detection, topology selection, memory allocation, and the cluster summary — is now emitted vialogger.info()andlogger.debug()with structured context fields, making it easy to filter or redirect log output. Users can callconfigure_logging(level="DEBUG")to see the full trace, orconfigure_logging(level="ERROR")to silence everything.
Fixed
Incorrect Dask config key in CHANGELOG v1.0.0. The v1.0.0 entry incorrectly stated that
DaskSetupConfig.spill_threadsapplied the Dask keydistributed.p2p.threads. The correct key — which controls each worker’s I/O thread pool used for spill read/write — isdistributed.worker.io-threads. The code has been correct since the bug was introduced; only the CHANGELOG entry was wrong.
[1.0.0] - 2025-01-17
Added
Configurable spill compression for worker memory spill files via
DaskSetupConfig.spill_compression.Applies Dask key
distributed.worker.memory.spill-compression.Supported values include:
auto,lz4,zstd,snappy,gzip,blosc,zlib,bz2,lzma.Default:
auto.
Optional communication compression toggle via
DaskSetupConfig.comm_compression.Applies Dask key
distributed.comm.compression.Boolean flag; default:
False.
Configurable parallel spill I/O via
DaskSetupConfig.spill_threads.Applies Dask key
distributed.worker.io-threadsto control the size of each worker’s I/O thread pool used for spill read/write operations.Accepts positive integers (1-16) or
Nonefor default behavior.Default:
None.Note:
distributed.p2p.threads(peer-to-peer shuffle) was incorrectly cited here in the original release entry; the key in use has always beendistributed.worker.io-threads.
Propagation of compression and parallel I/O settings through
configure_dask_settings,create_cluster, and client setup, with summary output.New tests covering validation, serialization, Dask config application, and integration paths for compression and parallel I/O options.
Improved test isolation and robustness for PBS environment detection to avoid flaky results.
Enhanced
Improved memory parsing capabilities in resource detection:
Added support for space-separated memory formats (e.g., “16 GB”, “1.5 TiB”).
Enhanced validation with overflow protection (8 EiB maximum).
Better error messages and warnings for problematic memory values.
Comprehensive support for binary vs decimal units (KiB vs KB, etc.).
Improved SLURM memory detection with fallback handling.
Robust whitespace normalization and format validation.
Test suite enhancements including warning suppressions for deprecated distributed internal attributes.
Improved mock fixture architecture to synchronize psutil mock responses across modules.
Fixed
Fixed flaky PBS environment detection test by making worker count assertion more resilient (accepting range based on environment).
Fixed deprecation warnings from distributed library during tests by suppressing FutureWarnings related to
worker.nthreads.Removed unused imports and variables.
Fixed linting and formatting issues across multiple files.