dask-setup 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dask_setup-2.0.0 → dask_setup-2.1.0}/PKG-INFO +1 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/pyproject.toml +1 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/__init__.py +3 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/benchmark.py +48 -39
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/client.py +70 -59
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/cluster.py +3 -5
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/config.py +0 -4
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/config_manager.py +13 -13
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/io_patterns.py +41 -40
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/logging.py +4 -2
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/multinode.py +248 -5
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/parquet.py +3 -4
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/rechunk.py +87 -14
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/reporting.py +1 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/resources.py +37 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/topology.py +2 -2
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/tune.py +1 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/workload.py +4 -4
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/xarray.py +100 -40
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup.egg-info/PKG-INFO +1 -1
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_benchmark.py +5 -5
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_multinode.py +318 -8
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_resources.py +83 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_xarray_chunks.py +141 -8
- {dask_setup-2.0.0 → dask_setup-2.1.0}/LICENSE +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/README.md +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/setup.cfg +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/callbacks.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/cli.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/dashboard.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/environment.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/error_handling.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/exceptions.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/legacy.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/py.typed +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/schema/__init__.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/schema/profile_schema.json +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/tempdir.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup/types.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup.egg-info/SOURCES.txt +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup.egg-info/dependency_links.txt +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup.egg-info/entry_points.txt +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup.egg-info/requires.txt +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/src/dask_setup.egg-info/top_level.txt +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_cli.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_client.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_cluster.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_compression.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_config.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_config_manager.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_dashboard.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_error_handling.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_exceptions.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_io_patterns.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_setup_dask_client.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_tempdir.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_topology.py +0 -0
- {dask_setup-2.0.0 → dask_setup-2.1.0}/tests/test_types.py +0 -0
|
@@ -22,6 +22,7 @@ from .multinode import (
|
|
|
22
22
|
detect_cluster_mode,
|
|
23
23
|
generate_pbs_script,
|
|
24
24
|
generate_slurm_script,
|
|
25
|
+
setup_interactive_cluster,
|
|
25
26
|
setup_pbs_cluster,
|
|
26
27
|
setup_slurm_cluster,
|
|
27
28
|
)
|
|
@@ -280,7 +281,7 @@ except ImportError:
|
|
|
280
281
|
)
|
|
281
282
|
|
|
282
283
|
|
|
283
|
-
__version__ = "2.
|
|
284
|
+
__version__ = "2.1.0"
|
|
284
285
|
|
|
285
286
|
__all__ = [
|
|
286
287
|
# Core API — always available
|
|
@@ -326,6 +327,7 @@ __all__ = [
|
|
|
326
327
|
"MultiNodeConfig",
|
|
327
328
|
"SharedTempDir",
|
|
328
329
|
"detect_cluster_mode",
|
|
330
|
+
"setup_interactive_cluster",
|
|
329
331
|
"setup_pbs_cluster",
|
|
330
332
|
"setup_slurm_cluster",
|
|
331
333
|
"generate_pbs_script",
|
|
@@ -39,6 +39,7 @@ Example::
|
|
|
39
39
|
|
|
40
40
|
from __future__ import annotations
|
|
41
41
|
|
|
42
|
+
import contextlib
|
|
42
43
|
import statistics
|
|
43
44
|
import time
|
|
44
45
|
from collections.abc import Callable, Sequence
|
|
@@ -324,15 +325,38 @@ class ChunkImpactResult:
|
|
|
324
325
|
----------
|
|
325
326
|
results : list[BenchmarkResult]
|
|
326
327
|
One entry per chunk specification, in the order they were tried.
|
|
327
|
-
|
|
328
|
-
The chunk specs that were swept.
|
|
328
|
+
chunk_specs : list[dict[str, int]]
|
|
329
|
+
The chunk specs that were swept. Alias for chunk_sizes.
|
|
329
330
|
recommended_chunks : dict[str, int]
|
|
330
331
|
The chunk spec with the best (lowest) wall time.
|
|
331
332
|
"""
|
|
332
333
|
|
|
333
334
|
results: list[BenchmarkResult]
|
|
334
|
-
|
|
335
|
-
recommended_chunks: dict[str, int]
|
|
335
|
+
chunk_specs: list[dict[str, int]]
|
|
336
|
+
recommended_chunks: dict[str, int] | None = None
|
|
337
|
+
|
|
338
|
+
def __post_init__(self) -> None:
|
|
339
|
+
"""Compute recommended_chunks if not provided."""
|
|
340
|
+
if self.recommended_chunks is None:
|
|
341
|
+
# Find the chunk spec with the lowest wall time
|
|
342
|
+
if self.results and self.chunk_specs:
|
|
343
|
+
valid = [
|
|
344
|
+
(r, cs)
|
|
345
|
+
for r, cs in zip(self.results, self.chunk_specs, strict=False)
|
|
346
|
+
if r.wall_time_seconds == r.wall_time_seconds # not nan
|
|
347
|
+
]
|
|
348
|
+
if valid:
|
|
349
|
+
_, best_chunks = min(valid, key=lambda x: x[0].wall_time_seconds)
|
|
350
|
+
self.recommended_chunks = best_chunks
|
|
351
|
+
else:
|
|
352
|
+
self.recommended_chunks = self.chunk_specs[0] if self.chunk_specs else {}
|
|
353
|
+
else:
|
|
354
|
+
self.recommended_chunks = {}
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def chunk_sizes(self) -> list[dict[str, int]]:
|
|
358
|
+
"""Alias for chunk_specs for backward compatibility."""
|
|
359
|
+
return self.chunk_specs
|
|
336
360
|
|
|
337
361
|
@property
|
|
338
362
|
def wall_times(self) -> list[float]:
|
|
@@ -341,6 +365,10 @@ class ChunkImpactResult:
|
|
|
341
365
|
def best(self) -> BenchmarkResult:
|
|
342
366
|
return min(self.results, key=lambda r: r.wall_time_seconds)
|
|
343
367
|
|
|
368
|
+
def optimal(self) -> BenchmarkResult:
|
|
369
|
+
"""Return the result with the best (lowest) wall time."""
|
|
370
|
+
return min(self.results, key=lambda r: r.wall_time_seconds)
|
|
371
|
+
|
|
344
372
|
def summary(self) -> str:
|
|
345
373
|
lines = [
|
|
346
374
|
f"{'Chunks':^36} {'Wall (s)':>10} {'Mem GiB':>8} {'Tasks/s':>8}",
|
|
@@ -352,7 +380,7 @@ class ChunkImpactResult:
|
|
|
352
380
|
f"{chunk_str:<36} {r.wall_time_seconds:>10.2f} "
|
|
353
381
|
f"{r.peak_memory_gib:>8.2f} {r.tasks_per_second:>8.1f}"
|
|
354
382
|
)
|
|
355
|
-
lines.append(f"\
|
|
383
|
+
lines.append(f"\nOptimal: {self.recommended_chunks}")
|
|
356
384
|
return "\n".join(lines)
|
|
357
385
|
|
|
358
386
|
def to_dataframe(self) -> Any:
|
|
@@ -476,10 +504,8 @@ def _measure_one(
|
|
|
476
504
|
errors.append(f"Task count failed: {e}")
|
|
477
505
|
|
|
478
506
|
# Worker count
|
|
479
|
-
|
|
507
|
+
with contextlib.suppress(Exception):
|
|
480
508
|
n_workers = len(client.scheduler_info().get("workers", {}))
|
|
481
|
-
except Exception:
|
|
482
|
-
pass
|
|
483
509
|
|
|
484
510
|
# Optional warmup
|
|
485
511
|
if warmup:
|
|
@@ -644,15 +670,11 @@ def benchmark_config(
|
|
|
644
670
|
)
|
|
645
671
|
finally:
|
|
646
672
|
if client is not None:
|
|
647
|
-
|
|
673
|
+
with contextlib.suppress(Exception):
|
|
648
674
|
client.close()
|
|
649
|
-
except Exception:
|
|
650
|
-
pass
|
|
651
675
|
if cluster is not None:
|
|
652
|
-
|
|
676
|
+
with contextlib.suppress(Exception):
|
|
653
677
|
cluster.close()
|
|
654
|
-
except Exception:
|
|
655
|
-
pass
|
|
656
678
|
|
|
657
679
|
if result is None:
|
|
658
680
|
result = BenchmarkResult(name=name, wall_time_seconds=float("nan"), errors=errors)
|
|
@@ -765,15 +787,11 @@ def scaling_analysis(
|
|
|
765
787
|
)
|
|
766
788
|
finally:
|
|
767
789
|
if client is not None:
|
|
768
|
-
|
|
790
|
+
with contextlib.suppress(Exception):
|
|
769
791
|
client.close()
|
|
770
|
-
except Exception:
|
|
771
|
-
pass
|
|
772
792
|
if cluster is not None:
|
|
773
|
-
|
|
793
|
+
with contextlib.suppress(Exception):
|
|
774
794
|
cluster.close()
|
|
775
|
-
except Exception:
|
|
776
|
-
pass
|
|
777
795
|
|
|
778
796
|
raw_results.append(result)
|
|
779
797
|
|
|
@@ -805,12 +823,10 @@ def scaling_analysis(
|
|
|
805
823
|
if plot:
|
|
806
824
|
fig = scaling.plot()
|
|
807
825
|
if fig is not None:
|
|
808
|
-
|
|
826
|
+
with contextlib.suppress(Exception):
|
|
809
827
|
import matplotlib.pyplot as plt
|
|
810
828
|
|
|
811
829
|
plt.show()
|
|
812
|
-
except Exception:
|
|
813
|
-
pass
|
|
814
830
|
|
|
815
831
|
return scaling
|
|
816
832
|
|
|
@@ -882,10 +898,9 @@ def chunk_impact(
|
|
|
882
898
|
|
|
883
899
|
# Generate chunk size sweep if not provided
|
|
884
900
|
if chunk_sizes is None:
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
chunk_sizes = [{}] # single run with no rechunking
|
|
901
|
+
chunk_sizes = (
|
|
902
|
+
_generate_auto_chunks(dims) if auto_chunks and dims else [{}]
|
|
903
|
+
) # single run with no rechunking
|
|
889
904
|
|
|
890
905
|
raw_results: list[BenchmarkResult] = []
|
|
891
906
|
|
|
@@ -927,7 +942,7 @@ def chunk_impact(
|
|
|
927
942
|
|
|
928
943
|
impact = ChunkImpactResult(
|
|
929
944
|
results=raw_results,
|
|
930
|
-
|
|
945
|
+
chunk_specs=chunk_sizes,
|
|
931
946
|
recommended_chunks=best_chunks,
|
|
932
947
|
)
|
|
933
948
|
|
|
@@ -935,12 +950,10 @@ def chunk_impact(
|
|
|
935
950
|
first_dim = next(iter(dims), None)
|
|
936
951
|
fig = impact.plot(dim=first_dim)
|
|
937
952
|
if fig is not None:
|
|
938
|
-
|
|
953
|
+
with contextlib.suppress(Exception):
|
|
939
954
|
import matplotlib.pyplot as plt
|
|
940
955
|
|
|
941
956
|
plt.show()
|
|
942
|
-
except Exception:
|
|
943
|
-
pass
|
|
944
957
|
|
|
945
958
|
return impact
|
|
946
959
|
|
|
@@ -1081,8 +1094,8 @@ def run_synthetic_benchmark(
|
|
|
1081
1094
|
if verbose:
|
|
1082
1095
|
print(f"Creating synthetic array {shape} chunked {chunks} …")
|
|
1083
1096
|
|
|
1084
|
-
np.random.
|
|
1085
|
-
arr = da.from_array(
|
|
1097
|
+
rng = np.random.default_rng(42)
|
|
1098
|
+
arr = da.from_array(rng.random(shape).astype(np.float32), chunks=chunks)
|
|
1086
1099
|
|
|
1087
1100
|
# Resolve dask operation
|
|
1088
1101
|
da_ops: dict[str, Callable[[Any], Any]] = {
|
|
@@ -1147,15 +1160,11 @@ def run_synthetic_benchmark(
|
|
|
1147
1160
|
errors.append(f"Cluster error: {e}")
|
|
1148
1161
|
finally:
|
|
1149
1162
|
if client is not None:
|
|
1150
|
-
|
|
1163
|
+
with contextlib.suppress(Exception):
|
|
1151
1164
|
client.close()
|
|
1152
|
-
except Exception:
|
|
1153
|
-
pass
|
|
1154
1165
|
if cluster_obj is not None:
|
|
1155
|
-
|
|
1166
|
+
with contextlib.suppress(Exception):
|
|
1156
1167
|
cluster_obj.close()
|
|
1157
|
-
except Exception:
|
|
1158
|
-
pass
|
|
1159
1168
|
|
|
1160
1169
|
valid_times = [t for t in times if t != float("inf")]
|
|
1161
1170
|
wall = statistics.mean(valid_times) if valid_times else float("nan")
|
|
@@ -15,7 +15,13 @@ from .config_manager import ConfigManager
|
|
|
15
15
|
from .dashboard import print_dashboard_info
|
|
16
16
|
from .exceptions import InsufficientResourcesError
|
|
17
17
|
from .logging import get_logger
|
|
18
|
-
from .multinode import
|
|
18
|
+
from .multinode import (
|
|
19
|
+
MultiNodeConfig,
|
|
20
|
+
detect_cluster_mode,
|
|
21
|
+
setup_interactive_cluster,
|
|
22
|
+
setup_pbs_cluster,
|
|
23
|
+
setup_slurm_cluster,
|
|
24
|
+
)
|
|
19
25
|
from .resources import detect_resources
|
|
20
26
|
from .tempdir import create_dask_temp_dir
|
|
21
27
|
from .topology import decide_topology, validate_topology
|
|
@@ -48,13 +54,14 @@ def _compute_smart_reserve_default() -> float:
|
|
|
48
54
|
"""
|
|
49
55
|
try:
|
|
50
56
|
total_ram_gb = psutil.virtual_memory().total / (1024**3)
|
|
51
|
-
|
|
57
|
+
smart_reserve = min(50.0, max(4.0, total_ram_gb * 0.20))
|
|
58
|
+
# Cap the smart reserve at 50.0 GiB as a safe HPC default
|
|
59
|
+
return min(50.0, smart_reserve)
|
|
52
60
|
except Exception:
|
|
53
61
|
return 50.0 # Safe HPC fallback if psutil is unexpectedly unavailable
|
|
54
62
|
|
|
55
63
|
|
|
56
64
|
def _resolve_configuration(
|
|
57
|
-
config: DaskSetupConfig | None = None,
|
|
58
65
|
profile: str | None = None,
|
|
59
66
|
workload_type: str = "io",
|
|
60
67
|
max_workers: int | None = None,
|
|
@@ -64,21 +71,16 @@ def _resolve_configuration(
|
|
|
64
71
|
adaptive: bool = False,
|
|
65
72
|
min_workers: int | None = None,
|
|
66
73
|
suggest_chunks: bool = False,
|
|
67
|
-
fallback_on_detection_failure: bool = False,
|
|
68
|
-
adaptive_memory: bool = False,
|
|
69
74
|
) -> DaskSetupConfig:
|
|
70
|
-
"""Resolve final configuration from a
|
|
75
|
+
"""Resolve final configuration from a profile and explicit parameters.
|
|
71
76
|
|
|
72
77
|
Priority order (highest to lowest):
|
|
73
78
|
|
|
74
79
|
1. Explicit keyword parameters passed to ``setup_dask_client()``
|
|
75
|
-
2.
|
|
80
|
+
2. Profile (``profile=``)
|
|
76
81
|
3. Defaults — ``reserve_mem_gb`` uses a smart default (20 % RAM, 4–50 GiB)
|
|
77
82
|
|
|
78
|
-
``config`` and ``profile`` are mutually exclusive.
|
|
79
|
-
|
|
80
83
|
Args:
|
|
81
|
-
config: A pre-built DaskSetupConfig object to use as the base configuration.
|
|
82
84
|
profile: Profile name to load from disk/builtins as the base configuration.
|
|
83
85
|
workload_type: Workload type override.
|
|
84
86
|
max_workers: Maximum workers cap.
|
|
@@ -96,28 +98,18 @@ def _resolve_configuration(
|
|
|
96
98
|
The heuristic that detects "explicitly set" parameters compares each value
|
|
97
99
|
against its default. Edge case: if you deliberately pass a value equal to
|
|
98
100
|
the default (e.g. ``dashboard=True``) it will be treated as "not set" and a
|
|
99
|
-
|
|
100
|
-
to avoid this ambiguity entirely.
|
|
101
|
+
profile value will take precedence.
|
|
101
102
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
# Build defaults using the environment-aware smart reserve
|
|
109
|
-
smart_reserve = _compute_smart_reserve_default()
|
|
110
|
-
defaults = DaskSetupConfig(reserve_mem_gb=smart_reserve)
|
|
111
|
-
logger.debug("Smart reserve default computed", reserve_mem_gb=smart_reserve)
|
|
103
|
+
# Build defaults - use provided reserve_mem_gb or fallback to smart default
|
|
104
|
+
defaults = DaskSetupConfig(
|
|
105
|
+
reserve_mem_gb=reserve_mem_gb if reserve_mem_gb is not None else 50.0
|
|
106
|
+
)
|
|
107
|
+
logger.debug("Configuration defaults set", reserve_mem_gb=defaults.reserve_mem_gb)
|
|
112
108
|
|
|
113
|
-
# Resolve the base configuration:
|
|
109
|
+
# Resolve the base configuration: load profile if specified
|
|
114
110
|
base_config = None
|
|
115
111
|
|
|
116
|
-
if
|
|
117
|
-
# Caller supplied a ready-made DaskSetupConfig — use it as the profile-level base
|
|
118
|
-
base_config = config
|
|
119
|
-
logger.debug("Using caller-provided DaskSetupConfig as base")
|
|
120
|
-
elif profile is not None:
|
|
112
|
+
if profile is not None:
|
|
121
113
|
manager = ConfigManager()
|
|
122
114
|
profile_obj = manager.get_profile(profile)
|
|
123
115
|
if profile_obj is None:
|
|
@@ -130,9 +122,7 @@ def _resolve_configuration(
|
|
|
130
122
|
#
|
|
131
123
|
# Note: This heuristic compares each value against its default to decide whether it was
|
|
132
124
|
# explicitly set. Edge case: if you deliberately pass a value that *equals* the default
|
|
133
|
-
# (e.g.
|
|
134
|
-
# and a base_config value will take precedence. To avoid this, use a DaskSetupConfig
|
|
135
|
-
# object via the 'config=' parameter instead.
|
|
125
|
+
# (e.g. dashboard=True) it will be treated as "not set" and a profile value will take precedence.
|
|
136
126
|
explicit_params: dict[str, Any] = {}
|
|
137
127
|
|
|
138
128
|
if workload_type != "io":
|
|
@@ -140,7 +130,7 @@ def _resolve_configuration(
|
|
|
140
130
|
if max_workers is not None:
|
|
141
131
|
explicit_params["max_workers"] = max_workers
|
|
142
132
|
if reserve_mem_gb is not None:
|
|
143
|
-
#
|
|
133
|
+
# Explicit float means "user chose this"
|
|
144
134
|
explicit_params["reserve_mem_gb"] = reserve_mem_gb
|
|
145
135
|
if max_mem_gb is not None:
|
|
146
136
|
explicit_params["max_mem_gb"] = max_mem_gb
|
|
@@ -152,10 +142,6 @@ def _resolve_configuration(
|
|
|
152
142
|
explicit_params["min_workers"] = min_workers
|
|
153
143
|
if suggest_chunks is not False:
|
|
154
144
|
explicit_params["suggest_chunks"] = suggest_chunks
|
|
155
|
-
if fallback_on_detection_failure:
|
|
156
|
-
explicit_params["fallback_on_detection_failure"] = fallback_on_detection_failure
|
|
157
|
-
if adaptive_memory:
|
|
158
|
-
explicit_params["adaptive_memory"] = adaptive_memory
|
|
159
145
|
|
|
160
146
|
explicit_config = DaskSetupConfig(**explicit_params) if explicit_params else None
|
|
161
147
|
|
|
@@ -308,7 +294,7 @@ def setup_dask_client(
|
|
|
308
294
|
def setup_dask_client(
|
|
309
295
|
workload_type: str = "io",
|
|
310
296
|
max_workers: int | None = None,
|
|
311
|
-
reserve_mem_gb: float
|
|
297
|
+
reserve_mem_gb: float = 50.0,
|
|
312
298
|
max_mem_gb: float | None = None,
|
|
313
299
|
dashboard: bool = True,
|
|
314
300
|
adaptive: bool = False,
|
|
@@ -380,17 +366,27 @@ def setup_dask_client(
|
|
|
380
366
|
and tightens the worker ``memory.target`` / ``memory.spill`` thresholds
|
|
381
367
|
slightly, giving workers more head-room from the start. Default
|
|
382
368
|
``False``.
|
|
383
|
-
mode : {"auto", "local", "pbs", "slurm"}
|
|
369
|
+
mode : {"auto", "local", "pbs", "slurm", "interactive"}
|
|
384
370
|
Backend selection.
|
|
385
371
|
|
|
386
372
|
- ``"local"`` — always use a single-node ``LocalCluster`` (default
|
|
387
373
|
behaviour prior to v2.0).
|
|
388
|
-
- ``"pbs"`` — launch via ``dask-jobqueue.PBSCluster
|
|
389
|
-
``pip install dask-jobqueue``.
|
|
390
|
-
- ``"slurm"`` — launch via ``dask-jobqueue.SLURMCluster
|
|
374
|
+
- ``"pbs"`` — launch via ``dask-jobqueue.PBSCluster`` (submits new
|
|
375
|
+
batch jobs). Requires ``pip install dask-jobqueue``.
|
|
376
|
+
- ``"slurm"`` — launch via ``dask-jobqueue.SLURMCluster`` (submits
|
|
377
|
+
new batch jobs).
|
|
378
|
+
- ``"interactive"`` — use resources already allocated in the current
|
|
379
|
+
interactive PBS (``qsub -I``) or SLURM (``salloc``) session.
|
|
380
|
+
Single-node allocations create a ``LocalCluster``; multi-node
|
|
381
|
+
allocations create an ``SSHCluster`` across all nodes in
|
|
382
|
+
``PBS_NODEFILE`` / ``SLURM_NODELIST``.
|
|
391
383
|
- ``"auto"`` (default) — inspect the environment and choose
|
|
392
|
-
``"
|
|
393
|
-
``
|
|
384
|
+
``"interactive"`` when inside a PBS interactive job
|
|
385
|
+
(``PBS_ENVIRONMENT=PBS_INTERACTIVE``) or a SLURM interactive
|
|
386
|
+
allocation (``SLURM_BATCH_FLAG`` not set to ``"1"``);
|
|
387
|
+
``"pbs"`` when ``PBS_JOBID`` is set in a batch job;
|
|
388
|
+
``"slurm"`` when ``SLURM_JOB_ID`` is set in a batch job;
|
|
389
|
+
``"local"`` otherwise.
|
|
394
390
|
multi_node_config : MultiNodeConfig or None
|
|
395
391
|
Configuration for the multi-node backend (``mode="pbs"`` or
|
|
396
392
|
``"slurm"``). Ignored when ``mode="local"``. When ``None`` and a
|
|
@@ -462,9 +458,14 @@ def setup_dask_client(
|
|
|
462
458
|
resolved_mode = detect_cluster_mode()
|
|
463
459
|
logger.debug("Mode auto-resolved", mode=resolved_mode)
|
|
464
460
|
|
|
465
|
-
if resolved_mode
|
|
466
|
-
|
|
461
|
+
if resolved_mode == "interactive":
|
|
462
|
+
logger.info("Interactive cluster mode — using already-allocated nodes")
|
|
463
|
+
client, cluster, tmp_path = setup_interactive_cluster(
|
|
464
|
+
workload_type=workload_type,
|
|
465
|
+
)
|
|
466
|
+
return client, cluster, tmp_path # type: ignore[return-value]
|
|
467
467
|
|
|
468
|
+
if resolved_mode in {"pbs", "slurm"}:
|
|
468
469
|
mn_cfg = multi_node_config
|
|
469
470
|
if mn_cfg is None:
|
|
470
471
|
# Build a minimal MultiNodeConfig from whatever was passed
|
|
@@ -496,20 +497,25 @@ def setup_dask_client(
|
|
|
496
497
|
logger.info("Auto-selected profile", profile=profile)
|
|
497
498
|
|
|
498
499
|
# Load and merge configuration
|
|
500
|
+
# Use explicit default for reserve_mem_gb if not provided
|
|
501
|
+
resolved_reserve_mem = reserve_mem_gb if reserve_mem_gb is not None else 50.0
|
|
502
|
+
|
|
499
503
|
config = _resolve_configuration(
|
|
500
|
-
config=config,
|
|
501
504
|
profile=profile,
|
|
502
505
|
workload_type=workload_type,
|
|
503
506
|
max_workers=max_workers,
|
|
504
|
-
reserve_mem_gb=
|
|
507
|
+
reserve_mem_gb=resolved_reserve_mem,
|
|
505
508
|
max_mem_gb=max_mem_gb,
|
|
506
509
|
dashboard=dashboard,
|
|
507
510
|
adaptive=adaptive,
|
|
508
511
|
min_workers=min_workers,
|
|
509
512
|
suggest_chunks=suggest_chunks,
|
|
510
|
-
fallback_on_detection_failure=fallback_on_detection_failure,
|
|
511
|
-
adaptive_memory=adaptive_memory,
|
|
512
513
|
)
|
|
514
|
+
|
|
515
|
+
# Apply additional config-level settings that don't go through _resolve_configuration
|
|
516
|
+
config.fallback_on_detection_failure = fallback_on_detection_failure
|
|
517
|
+
config.adaptive_memory = adaptive_memory
|
|
518
|
+
|
|
513
519
|
logger.debug(
|
|
514
520
|
"Configuration resolved",
|
|
515
521
|
workload_type=config.workload_type,
|
|
@@ -599,19 +605,11 @@ def setup_dask_client(
|
|
|
599
605
|
if config.dashboard and config.dashboard_port:
|
|
600
606
|
dashboard_address = f":{config.dashboard_port}"
|
|
601
607
|
|
|
602
|
-
# Map the boolean silence_logs config to a logging level.
|
|
603
|
-
# True → suppress everything except errors (logging.ERROR)
|
|
604
|
-
# False → show warnings and above (logging.WARNING), which is a reasonable HPC default
|
|
605
|
-
import logging as _stdlib_logging
|
|
606
|
-
|
|
607
|
-
silence_logs_level = _stdlib_logging.ERROR if config.silence_logs else _stdlib_logging.WARNING
|
|
608
|
-
|
|
609
608
|
cluster = create_cluster(
|
|
610
609
|
topology=topology,
|
|
611
610
|
memory_spec=memory_spec,
|
|
612
611
|
temp_dir=temp_dir,
|
|
613
612
|
dashboard_address=dashboard_address,
|
|
614
|
-
silence_logs=silence_logs_level,
|
|
615
613
|
adaptive=config.adaptive,
|
|
616
614
|
min_workers=config.min_workers,
|
|
617
615
|
memory_target=config.memory_target,
|
|
@@ -640,10 +638,23 @@ def setup_dask_client(
|
|
|
640
638
|
if config.dashboard:
|
|
641
639
|
print_dashboard_info(client, silent=config.silence_logs)
|
|
642
640
|
|
|
643
|
-
# Log setup summary
|
|
641
|
+
# Log and print setup summary
|
|
644
642
|
spill_threads_str = (
|
|
645
643
|
f" | spill_threads={config.spill_threads}" if config.spill_threads is not None else ""
|
|
646
644
|
)
|
|
645
|
+
summary_lines = [
|
|
646
|
+
"[setup_dask_client] Configuration summary",
|
|
647
|
+
f"temp/spill dir: {temp_dir}",
|
|
648
|
+
f"Workers: {topology.n_workers} | threads/worker: {topology.threads_per_worker} | processes: {topology.processes}",
|
|
649
|
+
f"Memory: total ~{memory_spec.total_mem_gib:.1f} GiB | usable ~{memory_spec.usable_mem_gb:.1f} GiB | per-worker ~{memory_spec.mem_per_worker_bytes / (1024**3):.1f} GiB",
|
|
650
|
+
f"Compression: spill={config.spill_compression} | comm={config.comm_compression}{spill_threads_str}",
|
|
651
|
+
]
|
|
652
|
+
|
|
653
|
+
# Print summary to console
|
|
654
|
+
for line in summary_lines:
|
|
655
|
+
print(line)
|
|
656
|
+
|
|
657
|
+
# Also log via structured logger
|
|
647
658
|
logger.info(f"Temp/spill dir: {temp_dir}")
|
|
648
659
|
logger.info(
|
|
649
660
|
f"Workers: {topology.n_workers}"
|
|
@@ -677,7 +688,7 @@ def setup_dask_client(
|
|
|
677
688
|
workload_type=config.workload_type,
|
|
678
689
|
verbose=config.suggest_chunks,
|
|
679
690
|
)
|
|
680
|
-
# recommend_chunks returns ChunkRecommendation
|
|
691
|
+
# recommend_chunks always returns ChunkRecommendation
|
|
681
692
|
chunk_recommendations = raw.chunks if hasattr(raw, "chunks") else raw
|
|
682
693
|
logger.info("Chunk recommendations computed", chunks=str(chunk_recommendations))
|
|
683
694
|
|
|
@@ -103,12 +103,10 @@ def configure_dask_settings(
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
# Add spill threads configuration if specified.
|
|
106
|
-
# "distributed.
|
|
107
|
-
#
|
|
108
|
-
# Note: "distributed.p2p.threads" is a different setting (peer-to-peer shuffle threads)
|
|
109
|
-
# and is intentionally NOT used here.
|
|
106
|
+
# "distributed.p2p.threads" controls the number of threads used for
|
|
107
|
+
# peer-to-peer operations and spill I/O operations.
|
|
110
108
|
if spill_threads is not None:
|
|
111
|
-
config_dict["distributed.
|
|
109
|
+
config_dict["distributed.p2p.threads"] = spill_threads
|
|
112
110
|
|
|
113
111
|
logger.debug(
|
|
114
112
|
"Dask global settings applied",
|
|
@@ -252,10 +252,6 @@ class DaskSetupConfig:
|
|
|
252
252
|
"io_access_pattern": self.io_access_pattern,
|
|
253
253
|
"io_storage_location": self.io_storage_location,
|
|
254
254
|
"io_compression_level": self.io_compression_level,
|
|
255
|
-
# Resilience options
|
|
256
|
-
"fallback_on_detection_failure": self.fallback_on_detection_failure,
|
|
257
|
-
# Adaptive behaviour options
|
|
258
|
-
"adaptive_memory": self.adaptive_memory,
|
|
259
255
|
# Metadata
|
|
260
256
|
"name": self.name,
|
|
261
257
|
"description": self.description,
|
|
@@ -202,7 +202,7 @@ Use `dask-setup profile create <name>` or manually create YAML files in the prof
|
|
|
202
202
|
def get_profile(self, name: str) -> ConfigProfile | None:
|
|
203
203
|
"""Get a specific profile by name.
|
|
204
204
|
|
|
205
|
-
Search order:
|
|
205
|
+
Search order: builtin profiles → site-wide profiles → user profiles.
|
|
206
206
|
|
|
207
207
|
Args:
|
|
208
208
|
name: Profile name
|
|
@@ -210,13 +210,9 @@ Use `dask-setup profile create <name>` or manually create YAML files in the prof
|
|
|
210
210
|
Returns:
|
|
211
211
|
ConfigProfile if found, None otherwise
|
|
212
212
|
"""
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
try:
|
|
217
|
-
return self.load_profile_from_file(profile_file)
|
|
218
|
-
except Exception as e:
|
|
219
|
-
raise InvalidConfigurationError(f"Failed to load profile '{name}': {e}") from e
|
|
213
|
+
# Built-in profiles take highest precedence
|
|
214
|
+
if name in self.builtin_profiles:
|
|
215
|
+
return self.builtin_profiles[name]
|
|
220
216
|
|
|
221
217
|
# Site-wide profiles
|
|
222
218
|
site_file = self.site_profiles_dir / f"{name}.yaml"
|
|
@@ -226,9 +222,13 @@ Use `dask-setup profile create <name>` or manually create YAML files in the prof
|
|
|
226
222
|
except Exception as e:
|
|
227
223
|
raise InvalidConfigurationError(f"Failed to load site profile '{name}': {e}") from e
|
|
228
224
|
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
225
|
+
# User profiles take lowest precedence
|
|
226
|
+
profile_file = self.profiles_dir / f"{name}.yaml"
|
|
227
|
+
if profile_file.exists():
|
|
228
|
+
try:
|
|
229
|
+
return self.load_profile_from_file(profile_file)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
raise InvalidConfigurationError(f"Failed to load profile '{name}': {e}") from e
|
|
232
232
|
|
|
233
233
|
return None
|
|
234
234
|
|
|
@@ -455,11 +455,11 @@ Use `dask-setup profile create <name>` or manually create YAML files in the prof
|
|
|
455
455
|
import urllib.request
|
|
456
456
|
|
|
457
457
|
try:
|
|
458
|
-
req = urllib.request.Request(
|
|
458
|
+
req = urllib.request.Request( # noqa: S310
|
|
459
459
|
url,
|
|
460
460
|
headers={"User-Agent": f"dask_setup/{PROFILE_FORMAT_VERSION}"},
|
|
461
461
|
)
|
|
462
|
-
with urllib.request.urlopen(req, timeout=30) as response:
|
|
462
|
+
with urllib.request.urlopen(req, timeout=30) as response: # noqa: S310
|
|
463
463
|
content = response.read().decode("utf-8")
|
|
464
464
|
except urllib.error.URLError as e:
|
|
465
465
|
raise InvalidConfigurationError(f"Could not fetch profile from {url!r}: {e}") from e
|