dask-setup 1.0.0__py3-none-any.whl

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/client.py ADDED
@@ -0,0 +1,280 @@
1
+ """Main client setup orchestration for dask_setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dask.distributed import Client, LocalCluster
6
+
7
+ from .cluster import calculate_memory_spec, create_cluster
8
+ from .config import DaskSetupConfig
9
+ from .config_manager import ConfigManager
10
+ from .dashboard import print_dashboard_info
11
+ from .exceptions import InsufficientResourcesError
12
+ from .resources import detect_resources
13
+ from .tempdir import create_dask_temp_dir
14
+ from .topology import decide_topology, validate_topology
15
+
16
+
17
+ def _resolve_configuration(
18
+ profile: str | None = None,
19
+ workload_type: str = "io",
20
+ max_workers: int | None = None,
21
+ reserve_mem_gb: float = 50.0,
22
+ max_mem_gb: float | None = None,
23
+ dashboard: bool = True,
24
+ adaptive: bool = False,
25
+ min_workers: int | None = None,
26
+ suggest_chunks: bool = False,
27
+ ) -> DaskSetupConfig:
28
+ """Resolve final configuration from profile and explicit parameters.
29
+
30
+ Priority order (highest to lowest):
31
+ 1. Explicit parameters passed to setup_dask_client()
32
+ 2. Profile configuration (if specified)
33
+ 3. Default values
34
+
35
+ Args:
36
+ profile: Profile name to load
37
+ **kwargs: Explicit parameters from setup_dask_client()
38
+
39
+ Returns:
40
+ Resolved DaskSetupConfig
41
+ """
42
+ # Start with defaults
43
+ defaults = DaskSetupConfig()
44
+
45
+ # Load profile if specified
46
+ profile_config = None
47
+ if profile:
48
+ manager = ConfigManager()
49
+ profile_obj = manager.get_profile(profile)
50
+ if profile_obj is None:
51
+ available = list(manager.list_profiles().keys())
52
+ raise ValueError(f"Profile '{profile}' not found. Available profiles: {available}")
53
+ profile_config = profile_obj.config
54
+
55
+ # Create explicit config from parameters (only non-default values)
56
+ explicit_params = {}
57
+
58
+ # Only include parameters that were explicitly passed (not defaults)
59
+ # We'll use a simple heuristic - if it matches the default, assume it wasn't set
60
+ if workload_type != "io":
61
+ explicit_params["workload_type"] = workload_type
62
+ if max_workers is not None:
63
+ explicit_params["max_workers"] = max_workers
64
+ if reserve_mem_gb != 50.0:
65
+ explicit_params["reserve_mem_gb"] = reserve_mem_gb
66
+ if max_mem_gb is not None:
67
+ explicit_params["max_mem_gb"] = max_mem_gb
68
+ if dashboard is not True:
69
+ explicit_params["dashboard"] = dashboard
70
+ if adaptive is not False:
71
+ explicit_params["adaptive"] = adaptive
72
+ if min_workers is not None:
73
+ explicit_params["min_workers"] = min_workers
74
+ if suggest_chunks is not False:
75
+ explicit_params["suggest_chunks"] = suggest_chunks
76
+
77
+ explicit_config = DaskSetupConfig(**explicit_params) if explicit_params else None
78
+
79
+ # Merge configurations: defaults < profile < explicit
80
+ final_config = defaults
81
+ if profile_config:
82
+ final_config = final_config.merge_with(profile_config)
83
+ if explicit_config:
84
+ final_config = final_config.merge_with(explicit_config)
85
+
86
+ return final_config
87
+
88
+
89
+ def setup_dask_client(
90
+ workload_type: str = "io",
91
+ max_workers: int | None = None,
92
+ reserve_mem_gb: float = 50.0,
93
+ max_mem_gb: float | None = None,
94
+ dashboard: bool = True,
95
+ adaptive: bool = False,
96
+ min_workers: int | None = None,
97
+ profile: str | None = None,
98
+ suggest_chunks: bool = False,
99
+ ) -> tuple[Client, LocalCluster, str]:
100
+ """Create a single-node Dask LocalCluster tuned for HPC login/compute nodes.
101
+
102
+ Routes temp/spill to $PBS_JOBFS when present.
103
+
104
+ Parameters
105
+ ----------
106
+ workload_type : {"cpu","io","mixed"}
107
+ Shape worker topology for CPU-bound, I/O-bound, or mixed workloads.
108
+ max_workers : int or None
109
+ Cap on worker processes. Defaults to all logical cores available.
110
+ reserve_mem_gb : float
111
+ Memory to reserve for OS / cache / filesystem (GiB).
112
+ max_mem_gb : float or None
113
+ Cap total memory used by Dask. Default is node total.
114
+ dashboard : bool
115
+ If True, start a dashboard on a random free port and print an SSH tunnel hint.
116
+ adaptive : bool
117
+ Enable single-node adaptive scaling (elastic number of workers).
118
+ min_workers : int or None
119
+ Minimum workers when adaptive=True.
120
+ profile : str or None
121
+ Name of configuration profile to use. Profile settings are overridden by
122
+ explicit parameters.
123
+ suggest_chunks : bool
124
+ If True, print xarray chunking recommendations after cluster setup.
125
+ Requires xarray and numpy to be installed.
126
+
127
+ Returns
128
+ -------
129
+ client : dask.distributed.Client
130
+ cluster : dask.distributed.LocalCluster
131
+ dask_local_dir : str
132
+ Absolute path to the temp/spill directory (under $PBS_JOBFS if available).
133
+
134
+ Raises
135
+ ------
136
+ InvalidConfigurationError
137
+ If workload_type is invalid or parameters are inconsistent.
138
+ InsufficientResourcesError
139
+ If system resources are insufficient for the requested configuration.
140
+ ResourceDetectionError
141
+ If resource detection fails completely.
142
+ """
143
+ # Load and merge configuration
144
+ config = _resolve_configuration(
145
+ profile=profile,
146
+ workload_type=workload_type,
147
+ max_workers=max_workers,
148
+ reserve_mem_gb=reserve_mem_gb,
149
+ max_mem_gb=max_mem_gb,
150
+ dashboard=dashboard,
151
+ adaptive=adaptive,
152
+ min_workers=min_workers,
153
+ suggest_chunks=suggest_chunks,
154
+ )
155
+
156
+ # Detect system resources
157
+ resources = detect_resources()
158
+
159
+ # Create temporary directory for spill files (use config for base dir if specified)
160
+ temp_dir = create_dask_temp_dir(base_dir=config.temp_base_dir)
161
+
162
+ # Decide worker topology based on workload type
163
+ topology = decide_topology(
164
+ workload_type=config.workload_type,
165
+ total_cores=resources.total_cores,
166
+ max_workers=config.max_workers,
167
+ )
168
+
169
+ # Validate topology makes sense
170
+ validate_topology(topology, resources.total_cores)
171
+
172
+ # Calculate memory allocation
173
+ try:
174
+ memory_spec = calculate_memory_spec(
175
+ total_mem_bytes=resources.total_mem_bytes,
176
+ n_workers=topology.n_workers,
177
+ reserve_mem_gb=config.reserve_mem_gb,
178
+ max_mem_gb=config.max_mem_gb,
179
+ )
180
+ except ValueError as e:
181
+ # Extract memory values for better error reporting
182
+ total_gib = resources.total_mem_bytes / (1024**3)
183
+ available_gb = total_gib - config.reserve_mem_gb
184
+ required_gb = topology.n_workers * 1.0 # Rough estimate: 1 GB per worker minimum
185
+
186
+ # Generate suggested actions based on the configuration
187
+ suggestions = []
188
+ if config.reserve_mem_gb > available_gb / 2: # Reserve more than half of available
189
+ suggestions.append(
190
+ f"Reduce reserve_mem_gb from {config.reserve_mem_gb:.1f} GB to {available_gb * 0.3:.1f} GB"
191
+ )
192
+ if topology.n_workers > 1:
193
+ suggestions.append(
194
+ f"Limit max_workers to 1 or 2 workers instead of {topology.n_workers}"
195
+ )
196
+ if not suggestions: # Fallback suggestions
197
+ suggestions = [
198
+ "Close other applications to free up memory",
199
+ "Request a larger memory allocation for your job",
200
+ ]
201
+
202
+ raise InsufficientResourcesError(
203
+ required_mem=required_gb, available_mem=available_gb, suggested_actions=suggestions
204
+ ) from e
205
+
206
+ # Create the cluster
207
+ dashboard_address = ":0" if config.dashboard else None
208
+ if config.dashboard and config.dashboard_port:
209
+ dashboard_address = f":{config.dashboard_port}"
210
+
211
+ cluster = create_cluster(
212
+ topology=topology,
213
+ memory_spec=memory_spec,
214
+ temp_dir=temp_dir,
215
+ dashboard_address=dashboard_address,
216
+ adaptive=config.adaptive,
217
+ min_workers=config.min_workers,
218
+ memory_target=config.memory_target,
219
+ memory_spill=config.memory_spill,
220
+ memory_pause=config.memory_pause,
221
+ memory_terminate=config.memory_terminate,
222
+ spill_compression=config.spill_compression,
223
+ comm_compression=config.comm_compression,
224
+ spill_threads=config.spill_threads,
225
+ )
226
+
227
+ # Connect client
228
+ client = Client(cluster)
229
+
230
+ # Print dashboard info if enabled
231
+ if config.dashboard:
232
+ print_dashboard_info(client, silent=config.silence_logs)
233
+ if not config.silence_logs:
234
+ print() # Add blank line
235
+
236
+ # Print summary information
237
+ spill_threads_str = (
238
+ f" | spill_threads={config.spill_threads}" if config.spill_threads is not None else ""
239
+ )
240
+ print(
241
+ f"[setup_dask_client] temp/spill dir: {temp_dir}\\n"
242
+ f"Workers: {topology.n_workers} | threads/worker: {topology.threads_per_worker} | processes: {topology.processes}\\n"
243
+ f"Mem: 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\\n"
244
+ f"Compression: spill={config.spill_compression} | comm={config.comm_compression}{spill_threads_str}"
245
+ )
246
+
247
+ # Print xarray chunking suggestions if enabled
248
+ if config.suggest_chunks:
249
+ try:
250
+ # Try to import xarray module to check availability
251
+
252
+ print("\n" + "=" * 60)
253
+ print(" Xarray Chunking Recommendations")
254
+ print("=" * 60)
255
+ print(
256
+ "To get optimal chunking suggestions for your xarray datasets:\n"
257
+ "\n"
258
+ " from dask_setup import recommend_chunks\n"
259
+ " chunks = recommend_chunks(your_dataset, client, verbose=True)\n"
260
+ " ds_optimized = your_dataset.chunk(chunks)\n"
261
+ "\n"
262
+ "Or use the standalone function:\n"
263
+ "\n"
264
+ " chunks = recommend_chunks(ds, workload_type='cpu') # or 'io', 'mixed'\n"
265
+ "\n"
266
+ f"Based on your current cluster configuration:\n"
267
+ f"• Workload type: {config.workload_type}\n"
268
+ f"• Target chunk size: 256-512 MiB per chunk\n"
269
+ f"• Safety factor: 60% of worker memory ({memory_spec.mem_per_worker_bytes / (1024**3) * 0.6:.1f} GiB max per chunk)\n"
270
+ f"• {topology.n_workers} workers available for parallelization\n"
271
+ )
272
+ print("=" * 60)
273
+
274
+ except ImportError:
275
+ print(
276
+ "\n Xarray integration requires xarray and numpy to be installed.\n"
277
+ "Install with: pip install xarray numpy"
278
+ )
279
+
280
+ return client, cluster, str(temp_dir)
dask_setup/cluster.py ADDED
@@ -0,0 +1,174 @@
1
+ """Dask cluster creation and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import dask
9
+ from dask.distributed import LocalCluster
10
+
11
+ from .types import MemorySpec, TopologySpec
12
+
13
+
14
+ def calculate_memory_spec(
15
+ total_mem_bytes: int,
16
+ n_workers: int,
17
+ reserve_mem_gb: float = 50.0,
18
+ max_mem_gb: float | None = None,
19
+ ) -> MemorySpec:
20
+ """Calculate memory allocation for Dask workers.
21
+
22
+ Args:
23
+ total_mem_bytes: Total system memory in bytes
24
+ n_workers: Number of workers that will be created
25
+ reserve_mem_gb: Memory to reserve for system in GiB
26
+ max_mem_gb: Optional cap on total memory usage in GiB
27
+
28
+ Returns:
29
+ MemorySpec with calculated memory allocation
30
+
31
+ Raises:
32
+ ValueError: If insufficient memory is available
33
+ """
34
+ total_mem_gib = total_mem_bytes / (1024**3)
35
+
36
+ # Apply max_mem_gb cap if specified
37
+ effective_total_gb = min(max_mem_gb or total_mem_gib, total_mem_gib)
38
+
39
+ # Calculate usable memory after reservation
40
+ usable_mem_gb = max(0.0, effective_total_gb - reserve_mem_gb)
41
+
42
+ if usable_mem_gb <= 0:
43
+ raise ValueError(
44
+ f"Not enough memory after reserving {reserve_mem_gb:.1f} GiB from "
45
+ f"{total_mem_gib:.1f} GiB total. Lower reserve_mem_gb or increase available memory."
46
+ )
47
+
48
+ # Calculate per-worker memory (minimum 1 GiB per worker)
49
+ mem_per_worker_gb = max(1.0, usable_mem_gb / n_workers)
50
+ mem_per_worker_bytes = int(mem_per_worker_gb * (1024**3))
51
+
52
+ return MemorySpec(
53
+ total_mem_gib=total_mem_gib,
54
+ usable_mem_gb=usable_mem_gb,
55
+ mem_per_worker_bytes=mem_per_worker_bytes,
56
+ reserved_mem_gb=reserve_mem_gb,
57
+ )
58
+
59
+
60
+ def configure_dask_settings(
61
+ temp_dir: Path,
62
+ memory_target: float = 0.75,
63
+ memory_spill: float = 0.85,
64
+ memory_pause: float = 0.92,
65
+ memory_terminate: float = 0.98,
66
+ spill_compression: str = "auto",
67
+ comm_compression: bool = False,
68
+ spill_threads: int | None = None,
69
+ ) -> None:
70
+ """Configure Dask global settings for optimal HPC performance.
71
+
72
+ Args:
73
+ temp_dir: Temporary directory for spill files
74
+ memory_target: Memory target threshold for spilling (0.0-1.0)
75
+ memory_spill: Memory spill threshold for aggressive spilling (0.0-1.0)
76
+ memory_pause: Memory pause threshold for pausing new tasks (0.0-1.0)
77
+ memory_terminate: Memory terminate threshold for killing workers (0.0-1.0)
78
+ spill_compression: Compression algorithm for spill files ('auto', 'lz4', 'zstd', etc.)
79
+ comm_compression: Whether to enable network communication compression
80
+ spill_threads: Number of threads for parallel spill I/O operations (None for default)
81
+ """
82
+ temp_dir_str = str(temp_dir)
83
+
84
+ config_dict = {
85
+ # Temporary file locations
86
+ "temporary-directory": temp_dir_str,
87
+ "distributed.worker.local-directory": temp_dir_str,
88
+ # Memory management thresholds (configurable)
89
+ "distributed.worker.memory.target": memory_target,
90
+ "distributed.worker.memory.spill": memory_spill,
91
+ "distributed.worker.memory.pause": memory_pause,
92
+ "distributed.worker.memory.terminate": memory_terminate,
93
+ # Compression settings
94
+ "distributed.worker.memory.spill-compression": spill_compression,
95
+ "distributed.comm.compression": comm_compression,
96
+ # Process spawning (more reliable on HPC systems)
97
+ "distributed.worker.multiprocessing-method": "spawn",
98
+ # Array optimization
99
+ "array.slicing.split_large_chunks": True,
100
+ }
101
+
102
+ # Add spill threads configuration if specified
103
+ if spill_threads is not None:
104
+ config_dict["distributed.p2p.threads"] = spill_threads
105
+
106
+ dask.config.set(config_dict)
107
+
108
+
109
+ def create_cluster(
110
+ topology: TopologySpec,
111
+ memory_spec: MemorySpec,
112
+ temp_dir: Path,
113
+ dashboard_address: str | None = ":0",
114
+ silence_logs: int = logging.ERROR,
115
+ adaptive: bool = False,
116
+ min_workers: int | None = None,
117
+ memory_target: float = 0.75,
118
+ memory_spill: float = 0.85,
119
+ memory_pause: float = 0.92,
120
+ memory_terminate: float = 0.98,
121
+ spill_compression: str = "auto",
122
+ comm_compression: bool = False,
123
+ spill_threads: int | None = None,
124
+ ) -> LocalCluster:
125
+ """Create and configure a Dask LocalCluster.
126
+
127
+ Args:
128
+ topology: Worker topology specification
129
+ memory_spec: Memory allocation specification
130
+ temp_dir: Temporary directory for worker files
131
+ dashboard_address: Dashboard bind address (None to disable)
132
+ silence_logs: Log level to suppress worker output
133
+ adaptive: Whether to enable adaptive scaling
134
+ min_workers: Minimum workers for adaptive scaling
135
+ memory_target: Memory target threshold for spilling (0.0-1.0)
136
+ memory_spill: Memory spill threshold for aggressive spilling (0.0-1.0)
137
+ memory_pause: Memory pause threshold for pausing new tasks (0.0-1.0)
138
+ memory_terminate: Memory terminate threshold for killing workers (0.0-1.0)
139
+ spill_compression: Compression algorithm for spill files ('auto', 'lz4', 'zstd', etc.)
140
+ comm_compression: Whether to enable network communication compression
141
+ spill_threads: Number of threads for parallel spill I/O operations (None for default)
142
+
143
+ Returns:
144
+ Configured LocalCluster instance
145
+ """
146
+ # Configure global Dask settings with compression
147
+ configure_dask_settings(
148
+ temp_dir=temp_dir,
149
+ memory_target=memory_target,
150
+ memory_spill=memory_spill,
151
+ memory_pause=memory_pause,
152
+ memory_terminate=memory_terminate,
153
+ spill_compression=spill_compression,
154
+ comm_compression=comm_compression,
155
+ spill_threads=spill_threads,
156
+ )
157
+
158
+ # Create the cluster
159
+ cluster = LocalCluster(
160
+ n_workers=topology.n_workers,
161
+ threads_per_worker=topology.threads_per_worker,
162
+ processes=topology.processes,
163
+ memory_limit=memory_spec.mem_per_worker_bytes,
164
+ dashboard_address=dashboard_address,
165
+ local_directory=str(temp_dir),
166
+ silence_logs=silence_logs,
167
+ )
168
+
169
+ # Enable adaptive scaling if requested
170
+ if adaptive:
171
+ min_w = min_workers if min_workers is not None else max(1, topology.n_workers // 2)
172
+ cluster.adapt(minimum=min_w, maximum=topology.n_workers, wait_count=2)
173
+
174
+ return cluster