seamless-dask 0.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.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: seamless-dask
3
+ Version: 0.1.0
4
+ Summary: Dask integration layer for Seamless distributed transformations
5
+ Author: Sjoerd de Vries
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/sjdv1982/seamless
8
+ Project-URL: Repository, https://github.com/sjdv1982/seamless
9
+ Project-URL: Issues, https://github.com/sjdv1982/seamless/issues
10
+ Keywords: seamless,dask,distributed,transformations
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: dask
22
+ Requires-Dist: distributed
23
+ Requires-Dist: dask_jobqueue
24
+ Requires-Dist: bokeh>=3.1.0
25
+ Requires-Dist: seamless-core
26
+ Requires-Dist: seamless-config
27
+ Requires-Dist: seamless-remote
28
+ Requires-Dist: seamless-transformer
29
+
30
+ # seamless-dask
31
+
32
+ `seamless-dask` is the Dask integration layer for the [Seamless](https://github.com/sjdv1982/seamless) ecosystem. It submits Seamless transformations to a Dask cluster, manages worker pools inside Dask workers, and provides a supervised cluster launcher for HPC environments. It is the distributed-execution alternative to the simpler single-machine `seamless-jobserver`.
33
+
34
+ **This is an internal infrastructure package.** User workflow code never imports `seamless-dask` directly — the user-facing API (`direct`, `delayed`) lives in `seamless-transformer`. When a Dask backend is configured, `seamless-config` and `seamless-remote` wire everything up behind the scenes.
35
+
36
+ ## Core components
37
+
38
+ ### SeamlessDaskClient
39
+
40
+ Wraps a `distributed.Client` with Seamless-specific logic: buffer caching, transformation submission, fat/thin checksum management, and cache pruning. Registered globally via `set_seamless_dask_client()` so that `seamless-transformer` can discover it.
41
+
42
+ ### TransformationDaskMixin
43
+
44
+ A mixin class that extends `seamless-transformer`'s `Transformation` with Dask execution. When a `SeamlessDaskClient` is registered and a transformation's `compute()` is called:
45
+
46
+ 1. Fast-path: check the Seamless database cache (via `seamless-remote`) before submitting anything.
47
+ 2. Request a permission token from the throttle manager.
48
+ 3. Submit the transformation to the Dask cluster.
49
+ 4. Store the result back in the cache.
50
+
51
+ If the cluster is at capacity, permission requests may be denied; when remote execution is optional, the caller retries and may eventually fall back to local execution.
52
+
53
+ ### Worker plugin
54
+
55
+ `SeamlessWorkerPlugin` (a Dask `WorkerPlugin`) runs inside each Dask worker. On setup it:
56
+
57
+ - Spawns a pool of Seamless worker processes (via `seamless-transformer`)
58
+ - Configures remote clients (via `seamless-config`) so workers can reach the hashserver and database
59
+ - Registers permission-counter resources with the Dask scheduler
60
+
61
+ ### Permission manager
62
+
63
+ An anti-deadlock throttle that prevents the Dask cluster from over-committing. Tracks a capacity counter based on `TRANSFORMATION_THROTTLE × workers`. When at capacity, batches new requests per 10-second epoch and grants based on system load. This prevents nested transformations from starving the cluster.
64
+
65
+ ### Dummy scheduler
66
+
67
+ An in-process Dask scheduler for development and testing. Creates a `Scheduler` + N `Worker` instances in a background thread, useful for local runs without a full cluster.
68
+
69
+ ---
70
+
71
+ ## Relation to the Seamless ecosystem
72
+
73
+ ```text
74
+ seamless-transformer (user-facing API: direct, delayed)
75
+ │
76
+ │ delegate job
77
+ ▼
78
+ seamless-remote
79
+ │ daskserver_remote
80
+ ▼
81
+ seamless-dask-wrapper ◄── CLI from this package
82
+ │
83
+ │ Dask scheduler
84
+ ▼
85
+ Dask workers
86
+ │ SeamlessWorkerPlugin
87
+ │ spawns seamless-transformer workers
88
+ │
89
+ ▼ nested transformations call seamless-remote again
90
+ ```
91
+
92
+ The `seamless-dask-wrapper` CLI is the entry point that `seamless-remote`'s `daskserver_remote` talks to. Inside each Dask worker, the full Seamless stack runs again — `seamless-transformer` dispatches work to its own worker processes, and calls `seamless-remote` for buffer resolution and cache operations. This recursion is what makes nested transformations work.
93
+
94
+ ---
95
+
96
+ ## CLI: seamless-dask-wrapper
97
+
98
+ The only CLI script. It launches and supervises a Dask cluster:
99
+
100
+ 1. Waits for a JSON status file written by the caller (typically `seamless-config` via `remote-http-launcher`).
101
+ 2. Reads cluster parameters from the status file.
102
+ 3. Evaluates a "cluster string" (`MODULE::SYMBOL`) to instantiate the cluster — supports `dask.distributed.LocalCluster` and `dask_jobqueue` backends (SLURMCluster, OARCluster, PBSCluster, etc.).
103
+ 4. Launches the cluster with adaptive scaling.
104
+ 5. Writes the scheduler and dashboard ports back to the status file.
105
+ 6. Monitors activity and shuts down after an inactivity timeout.
106
+
107
+ ### Status file
108
+
109
+ `seamless-dask-wrapper` receives all its configuration through a JSON **status file**, following the protocol defined by `remote-http-launcher`. The launcher creates the file with `"status": "starting"` and a `"parameters"` dict containing cluster configuration. The wrapper polls for this file on startup, reads the parameters, picks free ports for the scheduler and dashboard, launches the cluster, and then writes back to the same file with `"status": "running"` and the selected ports. If startup fails, it writes `"status": "failed"` instead. On clean shutdown it deletes the file.
110
+
111
+ This is a general-purpose handshake: the launcher doesn't need to know anything about Dask — it just writes parameters and waits for `"status"` to change. The wrapper doesn't need to know who launched it — it just reads the file. The `"parameters"` dict inside the status file carries the cluster configuration described below.
112
+
113
+ ### Key parameters (from status file)
114
+
115
+ | Category | Parameters |
116
+ | --- | --- |
117
+ | Job | `walltime`, `cores`, `memory`, `tmpdir`, `partition`, `project`, `job_extra_directives` |
118
+ | Scheduler | `unknown-task-duration`, `target-duration`, `internal-port-range`, `lifetime` |
119
+ | Worker | `transformation_throttle` (default 3), `dask-resources` |
120
+ | Cluster | `interactive`, `maximum_jobs`, `extra_dask_config`, `timeout` |
121
+
122
+ Worker threads are auto-set to `cores × transformation_throttle` to match Seamless worker pool sizing.
123
+
124
+ ### Parameter details
125
+
126
+ #### Job parameters
127
+
128
+ These parameters configure the HPC job that each Dask worker runs in. Most map to `dask_jobqueue` cluster constructor options.
129
+
130
+ - **`walltime`** — Passed directly to `dask_jobqueue` as `walltime`.
131
+ - **`cores`** — Multiplied by `transformation_throttle` to produce `job_cores`, which is passed to `dask_jobqueue` as `cores`. The original value controls how many Seamless worker processes are spawned per Dask worker.
132
+ - **`memory`** — Passed directly to `dask_jobqueue` as `memory`.
133
+ - **`tmpdir`** — Seamless-specific name. Mapped to two Dask config keys: `local-directory` and `temp-directory`. Defaults to `/tmp`.
134
+ - **`partition`** — Renamed to `queue` when passed to `dask_jobqueue` (matching its terminology for SLURM partitions / OAR queues).
135
+ - **`project`** — Passed directly to `dask_jobqueue` as `project` (the scheduler accounting/billing project).
136
+ - **`job_extra_directives`** — Passed to `dask_jobqueue` as `job-extra-directives` (hyphenated). Wrapped with `ensure_list()` so a single string is accepted.
137
+
138
+ #### Scheduler parameters
139
+
140
+ These tune the Dask scheduler and worker lifecycle.
141
+
142
+ - **`unknown-task-duration`** — Passed directly to Dask config as `distributed.scheduler.unknown-task-duration`. Default: `"1m"`.
143
+ - **`target-duration`** — Passed directly to Dask config as `distributed.scheduler.target-duration`. Also used to compute adaptive scaling intervals. Default: `"10m"`.
144
+ - **`internal-port-range`** — Seamless-specific. Normalized into a port range string and passed as `--worker-port` and `--nanny-port` worker args, plus Dask config keys `distributed.worker.port` and `distributed.nanny.port`.
145
+ - **`lifetime`** — Passed to Dask config as `distributed.worker.lifetime.duration`, but the value is computed: `walltime − lifetime-stagger − grace_period`. Set to `None` when `maximum_jobs == 1` (a single worker has no need for rolling restarts).
146
+
147
+ #### Worker parameters
148
+
149
+ These control Seamless-specific behavior inside each Dask worker.
150
+
151
+ - **`transformation_throttle`** — How many concurrent Seamless transformations each worker process handles. Exported as the environment variable `SEAMLESS_WORKER_TRANSFORMATION_THROTTLE` into worker job scripts. Also determines worker thread count (`cores × transformation_throttle`). Default: `3`.
152
+ - **`dask-resources`** — Custom Dask resource annotations for workers, passed to `distributed.worker.resources`. In exclusive mode, automatically includes `{"S": 1.0}` for resource-aware scheduling. The `"S"` resource value is also used as a scaling factor for adaptive target duration.
153
+
154
+ #### Cluster parameters
155
+
156
+ These govern the cluster lifecycle managed by `seamless-dask-wrapper`.
157
+
158
+ - **`interactive`** — When `true`, the cluster keeps at least one worker alive even when idle (minimum jobs = 1 instead of 0). Also prevents premature shutdown while tasks are still processing.
159
+ - **`maximum_jobs`** — Maximum number of Dask workers for adaptive scaling. Renamed to `maximum` when passed to Dask's adaptive scaling class. When set to `1`, worker lifetime rotation is disabled. Default: `1`.
160
+ - **`extra_dask_config`** — Escape hatch: a dict of arbitrary Dask config keys and values, merged directly into the Dask configuration. Useful for tuning settings not exposed as dedicated parameters.
161
+ - **`timeout`** — Inactivity timeout (in seconds) after which `seamless-dask-wrapper` shuts down the cluster. Not a Dask concept — this is managed by the wrapper's own monitoring loop.
162
+
163
+ ### SLURM examples
164
+
165
+ These examples show minimal status file parameters and the SLURM job script that `dask_jobqueue.SLURMCluster` generates from them. Both request 24 cores, 54 GB memory, and 1 hour of walltime on the `all` partition.
166
+
167
+ #### Pure Dask (`pure_dask: true`)
168
+
169
+ This mode uses Seamless infrastructure without using Seamless itself. You get `seamless-dask-wrapper` (controlled by `seamless-config`) as a managed Dask cluster launcher — with status-file handshake, adaptive scaling, port management, and inactivity timeout — but the workers are plain Dask workers. No `SeamlessWorkerPlugin`, no transformation throttle, no `seamless-core` or `seamless-transformer` involvement. Your code talks to the `distributed.Client` directly.
170
+
171
+ This is useful when you already have Dask workloads and want to take advantage of Seamless's cluster lifecycle management on HPC without adopting the Seamless computation model.
172
+
173
+ The wrapper passes `cores` through unchanged and defaults to 2 worker threads. No Seamless-specific environment variables are injected.
174
+
175
+ **Status file parameters:**
176
+
177
+ ```json
178
+ {
179
+ "parameters": {
180
+ "pure_dask": true,
181
+ "cores": 24,
182
+ "memory": "54000MB",
183
+ "walltime": "01:00:00",
184
+ "partition": "all",
185
+ "maximum_jobs": 4
186
+ }
187
+ }
188
+ ```
189
+
190
+ **Generated SLURM job script:**
191
+
192
+ ```bash
193
+ #!/usr/bin/env bash
194
+
195
+ #SBATCH --job-name=dask-worker
196
+ #SBATCH --ntasks=1
197
+ #SBATCH --cpus-per-task=24
198
+ #SBATCH --mem=54000M
199
+ #SBATCH --time=01:00:00
200
+ #SBATCH --partition=all
201
+
202
+ export DASK_DISTRIBUTED__WORKER__DAEMON=False
203
+ export DASK_DISTRIBUTED__SCHEDULER__UNKNOWN_TASK_DURATION=1m
204
+ export DASK_DISTRIBUTED__SCHEDULER__TARGET_DURATION=10m
205
+ export PYTHON_CPU_COUNT=$SLURM_JOB_CPUS_PER_NODE
206
+
207
+ dask worker \
208
+ --nthreads 2 \
209
+ --name worker-$SLURM_JOB_ID \
210
+ tcp://<scheduler_ip>:<scheduler_port>
211
+ ```
212
+
213
+ 24 SLURM cores, 2 Dask worker threads. Standard Dask behaviour — most cores sit idle unless your tasks release the GIL or you set `worker_threads` explicitly.
214
+
215
+ #### Seamless Dask (default)
216
+
217
+ Without `pure_dask`, the wrapper multiplies `cores × transformation_throttle` (default 3) to determine both the SLURM core request and the worker thread count. This matches the Seamless worker pool: each of the 24 worker processes handles up to 3 concurrent transformations, so 72 threads are needed.
218
+
219
+ **Status file parameters:**
220
+
221
+ ```json
222
+ {
223
+ "parameters": {
224
+ "cores": 24,
225
+ "memory": "54000MB",
226
+ "walltime": "01:00:00",
227
+ "partition": "all",
228
+ "maximum_jobs": 10,
229
+ "interactive": true,
230
+ "tmpdir": "/tmp"
231
+ }
232
+ }
233
+ ```
234
+
235
+ **Generated SLURM job script:**
236
+
237
+ ```bash
238
+ #!/usr/bin/env bash
239
+
240
+ #SBATCH --job-name=dask-worker
241
+ #SBATCH --ntasks=1
242
+ #SBATCH --cpus-per-task=72
243
+ #SBATCH --mem=54000M
244
+ #SBATCH --time=01:00:00
245
+ #SBATCH --partition=all
246
+
247
+ export SEAMLESS_DASK_QUEUE_EXCLUSIVE=0
248
+ export SEAMLESS_WORKER_TRANSFORMATION_THROTTLE=3
249
+ export DASK_DISTRIBUTED__WORKER__DAEMON=False
250
+ export DASK_DISTRIBUTED__SCHEDULER__UNKNOWN_TASK_DURATION=1m
251
+ export DASK_DISTRIBUTED__SCHEDULER__TARGET_DURATION=10m
252
+ export DASK_DISTRIBUTED__WORKER__LIFETIME__DURATION=55m
253
+ export DASK_DISTRIBUTED__WORKER__LIFETIME__STAGGER=4m
254
+ export PYTHON_CPU_COUNT=$SLURM_JOB_CPUS_PER_NODE
255
+
256
+ dask worker \
257
+ --nthreads 72 \
258
+ --local-directory /tmp \
259
+ --name worker-$SLURM_JOB_ID \
260
+ tcp://<scheduler_ip>:<scheduler_port>
261
+ ```
262
+
263
+ 72 SLURM cores (`24 × 3`), 72 worker threads, lifetime rotation enabled (55 minutes = 60 min walltime − 4 min stagger − 1 min grace). The `SeamlessWorkerPlugin` running inside this worker will spawn 24 Seamless worker processes, each handling up to 3 transformations concurrently.
264
+
265
+ ---
266
+
267
+ ## Installation
268
+
269
+ ```bash
270
+ pip install seamless-dask
271
+ ```
272
+
273
+ Requires Python >= 3.10. Dependencies: `dask`, `distributed`, `dask_jobqueue`, `bokeh>=3.1.0`, `seamless-core`, `seamless-config`, `seamless-remote`, `seamless-transformer`.
@@ -0,0 +1,244 @@
1
+ # seamless-dask
2
+
3
+ `seamless-dask` is the Dask integration layer for the [Seamless](https://github.com/sjdv1982/seamless) ecosystem. It submits Seamless transformations to a Dask cluster, manages worker pools inside Dask workers, and provides a supervised cluster launcher for HPC environments. It is the distributed-execution alternative to the simpler single-machine `seamless-jobserver`.
4
+
5
+ **This is an internal infrastructure package.** User workflow code never imports `seamless-dask` directly — the user-facing API (`direct`, `delayed`) lives in `seamless-transformer`. When a Dask backend is configured, `seamless-config` and `seamless-remote` wire everything up behind the scenes.
6
+
7
+ ## Core components
8
+
9
+ ### SeamlessDaskClient
10
+
11
+ Wraps a `distributed.Client` with Seamless-specific logic: buffer caching, transformation submission, fat/thin checksum management, and cache pruning. Registered globally via `set_seamless_dask_client()` so that `seamless-transformer` can discover it.
12
+
13
+ ### TransformationDaskMixin
14
+
15
+ A mixin class that extends `seamless-transformer`'s `Transformation` with Dask execution. When a `SeamlessDaskClient` is registered and a transformation's `compute()` is called:
16
+
17
+ 1. Fast-path: check the Seamless database cache (via `seamless-remote`) before submitting anything.
18
+ 2. Request a permission token from the throttle manager.
19
+ 3. Submit the transformation to the Dask cluster.
20
+ 4. Store the result back in the cache.
21
+
22
+ If the cluster is at capacity, permission requests may be denied; when remote execution is optional, the caller retries and may eventually fall back to local execution.
23
+
24
+ ### Worker plugin
25
+
26
+ `SeamlessWorkerPlugin` (a Dask `WorkerPlugin`) runs inside each Dask worker. On setup it:
27
+
28
+ - Spawns a pool of Seamless worker processes (via `seamless-transformer`)
29
+ - Configures remote clients (via `seamless-config`) so workers can reach the hashserver and database
30
+ - Registers permission-counter resources with the Dask scheduler
31
+
32
+ ### Permission manager
33
+
34
+ An anti-deadlock throttle that prevents the Dask cluster from over-committing. Tracks a capacity counter based on `TRANSFORMATION_THROTTLE × workers`. When at capacity, batches new requests per 10-second epoch and grants based on system load. This prevents nested transformations from starving the cluster.
35
+
36
+ ### Dummy scheduler
37
+
38
+ An in-process Dask scheduler for development and testing. Creates a `Scheduler` + N `Worker` instances in a background thread, useful for local runs without a full cluster.
39
+
40
+ ---
41
+
42
+ ## Relation to the Seamless ecosystem
43
+
44
+ ```text
45
+ seamless-transformer (user-facing API: direct, delayed)
46
+ │
47
+ │ delegate job
48
+ ▼
49
+ seamless-remote
50
+ │ daskserver_remote
51
+ ▼
52
+ seamless-dask-wrapper ◄── CLI from this package
53
+ │
54
+ │ Dask scheduler
55
+ ▼
56
+ Dask workers
57
+ │ SeamlessWorkerPlugin
58
+ │ spawns seamless-transformer workers
59
+ │
60
+ ▼ nested transformations call seamless-remote again
61
+ ```
62
+
63
+ The `seamless-dask-wrapper` CLI is the entry point that `seamless-remote`'s `daskserver_remote` talks to. Inside each Dask worker, the full Seamless stack runs again — `seamless-transformer` dispatches work to its own worker processes, and calls `seamless-remote` for buffer resolution and cache operations. This recursion is what makes nested transformations work.
64
+
65
+ ---
66
+
67
+ ## CLI: seamless-dask-wrapper
68
+
69
+ The only CLI script. It launches and supervises a Dask cluster:
70
+
71
+ 1. Waits for a JSON status file written by the caller (typically `seamless-config` via `remote-http-launcher`).
72
+ 2. Reads cluster parameters from the status file.
73
+ 3. Evaluates a "cluster string" (`MODULE::SYMBOL`) to instantiate the cluster — supports `dask.distributed.LocalCluster` and `dask_jobqueue` backends (SLURMCluster, OARCluster, PBSCluster, etc.).
74
+ 4. Launches the cluster with adaptive scaling.
75
+ 5. Writes the scheduler and dashboard ports back to the status file.
76
+ 6. Monitors activity and shuts down after an inactivity timeout.
77
+
78
+ ### Status file
79
+
80
+ `seamless-dask-wrapper` receives all its configuration through a JSON **status file**, following the protocol defined by `remote-http-launcher`. The launcher creates the file with `"status": "starting"` and a `"parameters"` dict containing cluster configuration. The wrapper polls for this file on startup, reads the parameters, picks free ports for the scheduler and dashboard, launches the cluster, and then writes back to the same file with `"status": "running"` and the selected ports. If startup fails, it writes `"status": "failed"` instead. On clean shutdown it deletes the file.
81
+
82
+ This is a general-purpose handshake: the launcher doesn't need to know anything about Dask — it just writes parameters and waits for `"status"` to change. The wrapper doesn't need to know who launched it — it just reads the file. The `"parameters"` dict inside the status file carries the cluster configuration described below.
83
+
84
+ ### Key parameters (from status file)
85
+
86
+ | Category | Parameters |
87
+ | --- | --- |
88
+ | Job | `walltime`, `cores`, `memory`, `tmpdir`, `partition`, `project`, `job_extra_directives` |
89
+ | Scheduler | `unknown-task-duration`, `target-duration`, `internal-port-range`, `lifetime` |
90
+ | Worker | `transformation_throttle` (default 3), `dask-resources` |
91
+ | Cluster | `interactive`, `maximum_jobs`, `extra_dask_config`, `timeout` |
92
+
93
+ Worker threads are auto-set to `cores × transformation_throttle` to match Seamless worker pool sizing.
94
+
95
+ ### Parameter details
96
+
97
+ #### Job parameters
98
+
99
+ These parameters configure the HPC job that each Dask worker runs in. Most map to `dask_jobqueue` cluster constructor options.
100
+
101
+ - **`walltime`** — Passed directly to `dask_jobqueue` as `walltime`.
102
+ - **`cores`** — Multiplied by `transformation_throttle` to produce `job_cores`, which is passed to `dask_jobqueue` as `cores`. The original value controls how many Seamless worker processes are spawned per Dask worker.
103
+ - **`memory`** — Passed directly to `dask_jobqueue` as `memory`.
104
+ - **`tmpdir`** — Seamless-specific name. Mapped to two Dask config keys: `local-directory` and `temp-directory`. Defaults to `/tmp`.
105
+ - **`partition`** — Renamed to `queue` when passed to `dask_jobqueue` (matching its terminology for SLURM partitions / OAR queues).
106
+ - **`project`** — Passed directly to `dask_jobqueue` as `project` (the scheduler accounting/billing project).
107
+ - **`job_extra_directives`** — Passed to `dask_jobqueue` as `job-extra-directives` (hyphenated). Wrapped with `ensure_list()` so a single string is accepted.
108
+
109
+ #### Scheduler parameters
110
+
111
+ These tune the Dask scheduler and worker lifecycle.
112
+
113
+ - **`unknown-task-duration`** — Passed directly to Dask config as `distributed.scheduler.unknown-task-duration`. Default: `"1m"`.
114
+ - **`target-duration`** — Passed directly to Dask config as `distributed.scheduler.target-duration`. Also used to compute adaptive scaling intervals. Default: `"10m"`.
115
+ - **`internal-port-range`** — Seamless-specific. Normalized into a port range string and passed as `--worker-port` and `--nanny-port` worker args, plus Dask config keys `distributed.worker.port` and `distributed.nanny.port`.
116
+ - **`lifetime`** — Passed to Dask config as `distributed.worker.lifetime.duration`, but the value is computed: `walltime − lifetime-stagger − grace_period`. Set to `None` when `maximum_jobs == 1` (a single worker has no need for rolling restarts).
117
+
118
+ #### Worker parameters
119
+
120
+ These control Seamless-specific behavior inside each Dask worker.
121
+
122
+ - **`transformation_throttle`** — How many concurrent Seamless transformations each worker process handles. Exported as the environment variable `SEAMLESS_WORKER_TRANSFORMATION_THROTTLE` into worker job scripts. Also determines worker thread count (`cores × transformation_throttle`). Default: `3`.
123
+ - **`dask-resources`** — Custom Dask resource annotations for workers, passed to `distributed.worker.resources`. In exclusive mode, automatically includes `{"S": 1.0}` for resource-aware scheduling. The `"S"` resource value is also used as a scaling factor for adaptive target duration.
124
+
125
+ #### Cluster parameters
126
+
127
+ These govern the cluster lifecycle managed by `seamless-dask-wrapper`.
128
+
129
+ - **`interactive`** — When `true`, the cluster keeps at least one worker alive even when idle (minimum jobs = 1 instead of 0). Also prevents premature shutdown while tasks are still processing.
130
+ - **`maximum_jobs`** — Maximum number of Dask workers for adaptive scaling. Renamed to `maximum` when passed to Dask's adaptive scaling class. When set to `1`, worker lifetime rotation is disabled. Default: `1`.
131
+ - **`extra_dask_config`** — Escape hatch: a dict of arbitrary Dask config keys and values, merged directly into the Dask configuration. Useful for tuning settings not exposed as dedicated parameters.
132
+ - **`timeout`** — Inactivity timeout (in seconds) after which `seamless-dask-wrapper` shuts down the cluster. Not a Dask concept — this is managed by the wrapper's own monitoring loop.
133
+
134
+ ### SLURM examples
135
+
136
+ These examples show minimal status file parameters and the SLURM job script that `dask_jobqueue.SLURMCluster` generates from them. Both request 24 cores, 54 GB memory, and 1 hour of walltime on the `all` partition.
137
+
138
+ #### Pure Dask (`pure_dask: true`)
139
+
140
+ This mode uses Seamless infrastructure without using Seamless itself. You get `seamless-dask-wrapper` (controlled by `seamless-config`) as a managed Dask cluster launcher — with status-file handshake, adaptive scaling, port management, and inactivity timeout — but the workers are plain Dask workers. No `SeamlessWorkerPlugin`, no transformation throttle, no `seamless-core` or `seamless-transformer` involvement. Your code talks to the `distributed.Client` directly.
141
+
142
+ This is useful when you already have Dask workloads and want to take advantage of Seamless's cluster lifecycle management on HPC without adopting the Seamless computation model.
143
+
144
+ The wrapper passes `cores` through unchanged and defaults to 2 worker threads. No Seamless-specific environment variables are injected.
145
+
146
+ **Status file parameters:**
147
+
148
+ ```json
149
+ {
150
+ "parameters": {
151
+ "pure_dask": true,
152
+ "cores": 24,
153
+ "memory": "54000MB",
154
+ "walltime": "01:00:00",
155
+ "partition": "all",
156
+ "maximum_jobs": 4
157
+ }
158
+ }
159
+ ```
160
+
161
+ **Generated SLURM job script:**
162
+
163
+ ```bash
164
+ #!/usr/bin/env bash
165
+
166
+ #SBATCH --job-name=dask-worker
167
+ #SBATCH --ntasks=1
168
+ #SBATCH --cpus-per-task=24
169
+ #SBATCH --mem=54000M
170
+ #SBATCH --time=01:00:00
171
+ #SBATCH --partition=all
172
+
173
+ export DASK_DISTRIBUTED__WORKER__DAEMON=False
174
+ export DASK_DISTRIBUTED__SCHEDULER__UNKNOWN_TASK_DURATION=1m
175
+ export DASK_DISTRIBUTED__SCHEDULER__TARGET_DURATION=10m
176
+ export PYTHON_CPU_COUNT=$SLURM_JOB_CPUS_PER_NODE
177
+
178
+ dask worker \
179
+ --nthreads 2 \
180
+ --name worker-$SLURM_JOB_ID \
181
+ tcp://<scheduler_ip>:<scheduler_port>
182
+ ```
183
+
184
+ 24 SLURM cores, 2 Dask worker threads. Standard Dask behaviour — most cores sit idle unless your tasks release the GIL or you set `worker_threads` explicitly.
185
+
186
+ #### Seamless Dask (default)
187
+
188
+ Without `pure_dask`, the wrapper multiplies `cores × transformation_throttle` (default 3) to determine both the SLURM core request and the worker thread count. This matches the Seamless worker pool: each of the 24 worker processes handles up to 3 concurrent transformations, so 72 threads are needed.
189
+
190
+ **Status file parameters:**
191
+
192
+ ```json
193
+ {
194
+ "parameters": {
195
+ "cores": 24,
196
+ "memory": "54000MB",
197
+ "walltime": "01:00:00",
198
+ "partition": "all",
199
+ "maximum_jobs": 10,
200
+ "interactive": true,
201
+ "tmpdir": "/tmp"
202
+ }
203
+ }
204
+ ```
205
+
206
+ **Generated SLURM job script:**
207
+
208
+ ```bash
209
+ #!/usr/bin/env bash
210
+
211
+ #SBATCH --job-name=dask-worker
212
+ #SBATCH --ntasks=1
213
+ #SBATCH --cpus-per-task=72
214
+ #SBATCH --mem=54000M
215
+ #SBATCH --time=01:00:00
216
+ #SBATCH --partition=all
217
+
218
+ export SEAMLESS_DASK_QUEUE_EXCLUSIVE=0
219
+ export SEAMLESS_WORKER_TRANSFORMATION_THROTTLE=3
220
+ export DASK_DISTRIBUTED__WORKER__DAEMON=False
221
+ export DASK_DISTRIBUTED__SCHEDULER__UNKNOWN_TASK_DURATION=1m
222
+ export DASK_DISTRIBUTED__SCHEDULER__TARGET_DURATION=10m
223
+ export DASK_DISTRIBUTED__WORKER__LIFETIME__DURATION=55m
224
+ export DASK_DISTRIBUTED__WORKER__LIFETIME__STAGGER=4m
225
+ export PYTHON_CPU_COUNT=$SLURM_JOB_CPUS_PER_NODE
226
+
227
+ dask worker \
228
+ --nthreads 72 \
229
+ --local-directory /tmp \
230
+ --name worker-$SLURM_JOB_ID \
231
+ tcp://<scheduler_ip>:<scheduler_port>
232
+ ```
233
+
234
+ 72 SLURM cores (`24 × 3`), 72 worker threads, lifetime rotation enabled (55 minutes = 60 min walltime − 4 min stagger − 1 min grace). The `SeamlessWorkerPlugin` running inside this worker will spawn 24 Seamless worker processes, each handling up to 3 transformations concurrently.
235
+
236
+ ---
237
+
238
+ ## Installation
239
+
240
+ ```bash
241
+ pip install seamless-dask
242
+ ```
243
+
244
+ Requires Python >= 3.10. Dependencies: `dask`, `distributed`, `dask_jobqueue`, `bokeh>=3.1.0`, `seamless-core`, `seamless-config`, `seamless-remote`, `seamless-transformer`.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "seamless-dask"
7
+ version = "0.1.0"
8
+ description = "Dask integration layer for Seamless distributed transformations"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{name = "Sjoerd de Vries"}]
13
+ dependencies = [
14
+ "dask",
15
+ "distributed",
16
+ "dask_jobqueue",
17
+ "bokeh>=3.1.0",
18
+ "seamless-core",
19
+ "seamless-config",
20
+ "seamless-remote",
21
+ "seamless-transformer",
22
+ ]
23
+ keywords = ["seamless", "dask", "distributed", "transformations"]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Intended Audience :: Science/Research",
27
+ "Intended Audience :: Developers",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Operating System :: OS Independent",
31
+ "Topic :: Scientific/Engineering",
32
+ "Topic :: Software Development :: Libraries",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/sjdv1982/seamless"
37
+ Repository = "https://github.com/sjdv1982/seamless"
38
+ Issues = "https://github.com/sjdv1982/seamless/issues"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["."]
42
+ include = ["seamless_dask*"]
43
+ exclude = ["plans*", "tests*"]
44
+
45
+ [project.scripts]
46
+ seamless-dask-wrapper = "seamless_dask.wrapper:main"
@@ -0,0 +1,72 @@
1
+ """Seamless utilities for working with Dask."""
2
+
3
+ from importlib import import_module
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ __all__ = [
7
+ "SeamlessDaskClient",
8
+ "get_seamless_dask_client",
9
+ "set_seamless_dask_client",
10
+ "TransformationDaskMixin",
11
+ "DummySchedulerHandle",
12
+ "create_dummy_client",
13
+ "create_dummy_scheduler",
14
+ "serve_dummy_scheduler",
15
+ "start_dummy_scheduler",
16
+ "get_dummy_scheduler_address",
17
+ ]
18
+
19
+ # Map exported symbols to (module, attribute) for lazy loading. This prevents
20
+ # heavyweight dependencies such as ``distributed`` from being imported when the
21
+ # package is merely used as a namespace (e.g. ``import seamless_dask.wrapper``).
22
+ _LAZY_EXPORTS = {
23
+ "SeamlessDaskClient": (".client", "SeamlessDaskClient"),
24
+ "get_seamless_dask_client": (
25
+ ".transformer_client",
26
+ "get_seamless_dask_client",
27
+ ),
28
+ "set_seamless_dask_client": (
29
+ ".transformer_client",
30
+ "set_seamless_dask_client",
31
+ ),
32
+ "TransformationDaskMixin": (".transformation_mixin", "TransformationDaskMixin"),
33
+ "DummySchedulerHandle": (".dummy_scheduler", "DummySchedulerHandle"),
34
+ "create_dummy_client": (".dummy_scheduler", "create_dummy_client"),
35
+ "create_dummy_scheduler": (".dummy_scheduler", "create_dummy_scheduler"),
36
+ "serve_dummy_scheduler": (".dummy_scheduler", "serve_dummy_scheduler"),
37
+ "start_dummy_scheduler": (".dummy_scheduler", "start_dummy_scheduler"),
38
+ "get_dummy_scheduler_address": (
39
+ ".dummy_scheduler",
40
+ "get_dummy_scheduler_address",
41
+ ),
42
+ }
43
+
44
+ if TYPE_CHECKING:
45
+ from .client import SeamlessDaskClient
46
+ from .dummy_scheduler import (
47
+ DummySchedulerHandle,
48
+ create_dummy_client,
49
+ create_dummy_scheduler,
50
+ get_dummy_scheduler_address,
51
+ serve_dummy_scheduler,
52
+ start_dummy_scheduler,
53
+ )
54
+ from .transformer_client import (
55
+ get_seamless_dask_client,
56
+ set_seamless_dask_client,
57
+ )
58
+ from .transformation_mixin import TransformationDaskMixin
59
+
60
+
61
+ def __getattr__(name: str) -> Any:
62
+ target = _LAZY_EXPORTS.get(name)
63
+ if target is None:
64
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
65
+ module = import_module(target[0], __name__)
66
+ value = getattr(module, target[1])
67
+ globals()[name] = value
68
+ return value
69
+
70
+
71
+ def __dir__() -> list[str]:
72
+ return sorted(set(list(globals().keys()) + __all__))