nb2slurm 0.0.1.dev1__tar.gz → 0.0.1.dev2__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.
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/PKG-INFO +62 -2
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/README.md +61 -1
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/0_config.ipynb +191 -187
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/1_build.ipynb +110 -109
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/2_submit.ipynb +99 -95
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/README.md +2 -2
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/monte_carlo2slurm.ipynb +30 -4
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/hpc-for-beginners.md +7 -2
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/setup_notebooks.ipynb +3 -18
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/walkthrough.ipynb +11 -11
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/__init__.py +1 -1
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/config.py +1 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/ssh.py +118 -24
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/.claude/settings.local.json +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/.gitignore +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/CITATION.cff +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/LICENSE +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/Seamless.pdf +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/3_sync.ipynb +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/analyse_subsets.ipynb +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/jobs.json +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/notebooks/0_settings.ipynb +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/notebooks/1_simulate.ipynb +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/notebooks/2_plot.ipynb +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/scripts/montecarlo.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/pyproject.toml +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/done.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/environment.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/render.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/runtime.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/settings.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/structure.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/cancel_jobs.sh.j2 +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/job.slurm.j2 +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/run_workflow.py.j2 +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/submit_batch.sh.j2 +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/submit_jobs.sh.j2 +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/workflow.py +0 -0
- {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/tests/test_workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nb2slurm
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev2
|
|
4
4
|
Summary: Seamlessly scale a single-subject notebook workflow to many subjects on a SLURM HPC, driven entirely from notebooks.
|
|
5
5
|
Project-URL: Homepage, https://github.com/eWaterCycle/nb2slurm
|
|
6
6
|
Author: Mark Melotto
|
|
@@ -300,6 +300,41 @@ wf.status(ssh=cfg) # parsed squeue
|
|
|
300
300
|
wf.cancel(ssh=cfg) # scancel what we submitted
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
+
## Connecting over SSH
|
|
304
|
+
|
|
305
|
+
`SSHConfig` is how nb2slurm reaches the cluster — paramiko for commands, `rsync`
|
|
306
|
+
for file sync. You need an SSH key registered on the cluster. nb2slurm can make
|
|
307
|
+
the key, but **you** register it (most HPCs disable password login, so there's no
|
|
308
|
+
way for a tool to install it for you):
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
# 1. make a keypair locally (ed25519 recommended; key_type="rsa" also supported)
|
|
312
|
+
nb2slurm.generate_key(key_type="ed25519") # -> ~/.ssh/id_ed25519 (+.pub), prints the public key
|
|
313
|
+
# copy the printed PUBLIC key into your HPC's key-upload page / portal (SURF,
|
|
314
|
+
# Snellius, ...), or into ~/.ssh/authorized_keys on a login node. Reprint any
|
|
315
|
+
# time with nb2slurm.public_key("~/.ssh/id_ed25519").
|
|
316
|
+
|
|
317
|
+
# 2. describe the connection
|
|
318
|
+
cfg = nb2slurm.SSHConfig(
|
|
319
|
+
host="snellius.surf.nl", user="me",
|
|
320
|
+
remote_dir="/home/me/myproject",
|
|
321
|
+
key_filename="~/.ssh/id_ed25519",
|
|
322
|
+
# passphrase="…", # only if the key is encrypted AND you're not using ssh-agent
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# 3. check it before anything else
|
|
326
|
+
cfg.test_connection() # runs hostname/whoami, prints a clear OK/FAIL (fails fast, never hangs)
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Passphrase-protected keys.** The "password" most clusters prompt for is your
|
|
330
|
+
key's *passphrase* (decrypted locally), not a server login. The clean way is to
|
|
331
|
+
load the key into **ssh-agent** once — `ssh-add ~/.ssh/id_ed25519` — so both
|
|
332
|
+
nb2slurm *and* `rsync` (`push`/`pull`) authenticate through it with no prompts.
|
|
333
|
+
Passing `passphrase=` to `SSHConfig` also works, but it only unlocks the paramiko
|
|
334
|
+
calls (`test_connection`/`submit`/`status`/`check`); `push`/`pull` go through the
|
|
335
|
+
`rsync` CLI and still need the agent. Secrets (`password`, `passphrase`) are never
|
|
336
|
+
written to disk by `save_config`.
|
|
337
|
+
|
|
303
338
|
## Creating the conda environment + kernel
|
|
304
339
|
|
|
305
340
|
The SLURM job does `conda activate <env>` and papermill needs a registered
|
|
@@ -326,6 +361,23 @@ Passing `environment=env` to `Workflow` keeps the names in sync (it errors if
|
|
|
326
361
|
`kernel`/`conda_env` disagree) and makes `build()` also write `environment.yml`.
|
|
327
362
|
`create_environment()` uses `mamba` when available, falls back to `conda`, and
|
|
328
363
|
registers the kernel via `ipykernel`. Omit `ssh=` to build the env locally instead.
|
|
364
|
+
It runs **non-interactively** (it never stalls on a conda `[Y/n]` prompt over SSH)
|
|
365
|
+
and **streams** conda/mamba output live, so a multi-minute solve doesn't look like
|
|
366
|
+
a hang.
|
|
367
|
+
|
|
368
|
+
### Rebuilding or removing an environment
|
|
369
|
+
|
|
370
|
+
`create_environment` is idempotent: re-running it *updates the env in place* (no
|
|
371
|
+
`Overwrite?` prompt). To recover from a half-built env, or force a clean slate:
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
wf.create_environment(ssh=cfg, overwrite=True) # delete, then rebuild from scratch
|
|
375
|
+
wf.remove_environment(ssh=cfg) # delete the env AND its Jupyter kernel
|
|
376
|
+
wf.environment.exists(ssh=cfg) # -> True/False, changes nothing
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
(`Environment` has the same `.create(overwrite=...)`, `.remove()`, and `.exists()`
|
|
380
|
+
if you're driving one directly.)
|
|
329
381
|
|
|
330
382
|
### Using a cluster's existing environment (no env creation)
|
|
331
383
|
|
|
@@ -478,6 +530,11 @@ wf.submit([("NL","123","ssp126")], ssh=cfg) # override: run an explicit subset
|
|
|
478
530
|
wf.submit(ssh=cfg, jobs_json="rerun.json") # override: use a different file
|
|
479
531
|
```
|
|
480
532
|
|
|
533
|
+
`concurrency` caps how many jobs run at once, chained with SLURM `afterany`
|
|
534
|
+
dependencies so you don't flood the queue. Set **`concurrency=0`** to submit
|
|
535
|
+
everything at once with **no dependencies** — ideal for a handful of quick,
|
|
536
|
+
independent jobs. You can also override it per call: `wf.submit(ssh=cfg, concurrency=0)`.
|
|
537
|
+
|
|
481
538
|
Because each job's output dir is built from the JSON, your first notebook never
|
|
482
539
|
builds folders — it just receives `outdir` and writes `settings.json`. The
|
|
483
540
|
underlying parser is exposed as `nb2slurm.Structure` if you want it directly
|
|
@@ -501,7 +558,10 @@ The split is the safety mechanism:
|
|
|
501
558
|
back can't overwrite a notebook you changed locally while jobs were running.
|
|
502
559
|
|
|
503
560
|
So the normal loop after editing a notebook is: `push` the change, submit again
|
|
504
|
-
(finished work is skipped via `done.csv`), then `pull` results when ready.
|
|
561
|
+
(finished work is skipped via `done.csv`), then `pull` results when ready. `pull`
|
|
562
|
+
is safe to run even before any results exist — it creates the remote `output/`
|
|
563
|
+
and `done/` dirs if they're missing rather than erroring, and is fine to re-run as
|
|
564
|
+
more jobs finish.
|
|
505
565
|
|
|
506
566
|
## Control notebooks
|
|
507
567
|
|
|
@@ -70,6 +70,41 @@ wf.status(ssh=cfg) # parsed squeue
|
|
|
70
70
|
wf.cancel(ssh=cfg) # scancel what we submitted
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
## Connecting over SSH
|
|
74
|
+
|
|
75
|
+
`SSHConfig` is how nb2slurm reaches the cluster — paramiko for commands, `rsync`
|
|
76
|
+
for file sync. You need an SSH key registered on the cluster. nb2slurm can make
|
|
77
|
+
the key, but **you** register it (most HPCs disable password login, so there's no
|
|
78
|
+
way for a tool to install it for you):
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# 1. make a keypair locally (ed25519 recommended; key_type="rsa" also supported)
|
|
82
|
+
nb2slurm.generate_key(key_type="ed25519") # -> ~/.ssh/id_ed25519 (+.pub), prints the public key
|
|
83
|
+
# copy the printed PUBLIC key into your HPC's key-upload page / portal (SURF,
|
|
84
|
+
# Snellius, ...), or into ~/.ssh/authorized_keys on a login node. Reprint any
|
|
85
|
+
# time with nb2slurm.public_key("~/.ssh/id_ed25519").
|
|
86
|
+
|
|
87
|
+
# 2. describe the connection
|
|
88
|
+
cfg = nb2slurm.SSHConfig(
|
|
89
|
+
host="snellius.surf.nl", user="me",
|
|
90
|
+
remote_dir="/home/me/myproject",
|
|
91
|
+
key_filename="~/.ssh/id_ed25519",
|
|
92
|
+
# passphrase="…", # only if the key is encrypted AND you're not using ssh-agent
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# 3. check it before anything else
|
|
96
|
+
cfg.test_connection() # runs hostname/whoami, prints a clear OK/FAIL (fails fast, never hangs)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Passphrase-protected keys.** The "password" most clusters prompt for is your
|
|
100
|
+
key's *passphrase* (decrypted locally), not a server login. The clean way is to
|
|
101
|
+
load the key into **ssh-agent** once — `ssh-add ~/.ssh/id_ed25519` — so both
|
|
102
|
+
nb2slurm *and* `rsync` (`push`/`pull`) authenticate through it with no prompts.
|
|
103
|
+
Passing `passphrase=` to `SSHConfig` also works, but it only unlocks the paramiko
|
|
104
|
+
calls (`test_connection`/`submit`/`status`/`check`); `push`/`pull` go through the
|
|
105
|
+
`rsync` CLI and still need the agent. Secrets (`password`, `passphrase`) are never
|
|
106
|
+
written to disk by `save_config`.
|
|
107
|
+
|
|
73
108
|
## Creating the conda environment + kernel
|
|
74
109
|
|
|
75
110
|
The SLURM job does `conda activate <env>` and papermill needs a registered
|
|
@@ -96,6 +131,23 @@ Passing `environment=env` to `Workflow` keeps the names in sync (it errors if
|
|
|
96
131
|
`kernel`/`conda_env` disagree) and makes `build()` also write `environment.yml`.
|
|
97
132
|
`create_environment()` uses `mamba` when available, falls back to `conda`, and
|
|
98
133
|
registers the kernel via `ipykernel`. Omit `ssh=` to build the env locally instead.
|
|
134
|
+
It runs **non-interactively** (it never stalls on a conda `[Y/n]` prompt over SSH)
|
|
135
|
+
and **streams** conda/mamba output live, so a multi-minute solve doesn't look like
|
|
136
|
+
a hang.
|
|
137
|
+
|
|
138
|
+
### Rebuilding or removing an environment
|
|
139
|
+
|
|
140
|
+
`create_environment` is idempotent: re-running it *updates the env in place* (no
|
|
141
|
+
`Overwrite?` prompt). To recover from a half-built env, or force a clean slate:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
wf.create_environment(ssh=cfg, overwrite=True) # delete, then rebuild from scratch
|
|
145
|
+
wf.remove_environment(ssh=cfg) # delete the env AND its Jupyter kernel
|
|
146
|
+
wf.environment.exists(ssh=cfg) # -> True/False, changes nothing
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
(`Environment` has the same `.create(overwrite=...)`, `.remove()`, and `.exists()`
|
|
150
|
+
if you're driving one directly.)
|
|
99
151
|
|
|
100
152
|
### Using a cluster's existing environment (no env creation)
|
|
101
153
|
|
|
@@ -248,6 +300,11 @@ wf.submit([("NL","123","ssp126")], ssh=cfg) # override: run an explicit subset
|
|
|
248
300
|
wf.submit(ssh=cfg, jobs_json="rerun.json") # override: use a different file
|
|
249
301
|
```
|
|
250
302
|
|
|
303
|
+
`concurrency` caps how many jobs run at once, chained with SLURM `afterany`
|
|
304
|
+
dependencies so you don't flood the queue. Set **`concurrency=0`** to submit
|
|
305
|
+
everything at once with **no dependencies** — ideal for a handful of quick,
|
|
306
|
+
independent jobs. You can also override it per call: `wf.submit(ssh=cfg, concurrency=0)`.
|
|
307
|
+
|
|
251
308
|
Because each job's output dir is built from the JSON, your first notebook never
|
|
252
309
|
builds folders — it just receives `outdir` and writes `settings.json`. The
|
|
253
310
|
underlying parser is exposed as `nb2slurm.Structure` if you want it directly
|
|
@@ -271,7 +328,10 @@ The split is the safety mechanism:
|
|
|
271
328
|
back can't overwrite a notebook you changed locally while jobs were running.
|
|
272
329
|
|
|
273
330
|
So the normal loop after editing a notebook is: `push` the change, submit again
|
|
274
|
-
(finished work is skipped via `done.csv`), then `pull` results when ready.
|
|
331
|
+
(finished work is skipped via `done.csv`), then `pull` results when ready. `pull`
|
|
332
|
+
is safe to run even before any results exist — it creates the remote `output/`
|
|
333
|
+
and `done/` dirs if they're missing rather than erroring, and is fine to re-run as
|
|
334
|
+
more jobs finish.
|
|
275
335
|
|
|
276
336
|
## Control notebooks
|
|
277
337
|
|
|
@@ -1,187 +1,191 @@
|
|
|
1
|
-
{
|
|
2
|
-
"cells": [
|
|
3
|
-
{
|
|
4
|
-
"cell_type": "markdown",
|
|
5
|
-
"metadata": {},
|
|
6
|
-
"source": [
|
|
7
|
-
"# 0. Configure your run\n",
|
|
8
|
-
"\n",
|
|
9
|
-
"This is the only notebook you normally edit. Fill in the cells below, then run it\n",
|
|
10
|
-
"top to bottom. It saves two files in the project root:\n",
|
|
11
|
-
"\n",
|
|
12
|
-
"* **`control_config.json`** — all your settings, read by `1_build`, `2_submit`\n",
|
|
13
|
-
" and `3_sync` (so they always agree).\n",
|
|
14
|
-
"* **`jobs.json`** — the nested list of jobs to run.\n",
|
|
15
|
-
"\n",
|
|
16
|
-
"Re-run this notebook whenever you change a setting."
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"cell_type": "markdown",
|
|
21
|
-
"metadata": {},
|
|
22
|
-
"source": [
|
|
23
|
-
"## Project\n",
|
|
24
|
-
"\n",
|
|
25
|
-
"Name, the notebooks to run in order, and what varies per job."
|
|
26
|
-
]
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"cell_type": "code",
|
|
30
|
-
"execution_count": null,
|
|
31
|
-
"metadata": {},
|
|
32
|
-
"outputs": [],
|
|
33
|
-
"source": [
|
|
34
|
-
"project_name = \"myproject\"\n",
|
|
35
|
-
"notebooks = [\n",
|
|
36
|
-
" \"notebooks/0_settings.ipynb\", # first notebook: writes settings.json\n",
|
|
37
|
-
" \"notebooks/1_analysis.ipynb\",\n",
|
|
38
|
-
" \"notebooks/2_more.ipynb\",\n",
|
|
39
|
-
"]\n",
|
|
40
|
-
"varying = [\"country\", \"region\", \"scenario\"] # the levels in jobs.json (below)"
|
|
41
|
-
]
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"cell_type": "markdown",
|
|
45
|
-
"metadata": {},
|
|
46
|
-
"source": "## The jobs to run\n\nA nested dict: each root-to-leaf path is one SLURM job, and the levels line up\nwith `varying` above. nb2slurm builds the matching output folders and submits one\njob per leaf.\n\n**Format:** a *dict* nests one more level, a *list* at the bottom means several\njobs sharing that parent, and `None`/`[]` ends the path. It's plain JSON, so for\nsmall runs type it by hand (below) and for big runs generate it in Python (a\ncomprehension, a CSV, an API query, ...) — see the README for the rules. The only\nthing that matters is that the finished dict lands in `jobs.json`."
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"cell_type": "code",
|
|
50
|
-
"execution_count": null,
|
|
51
|
-
"metadata": {},
|
|
52
|
-
"outputs": [],
|
|
53
|
-
"source": [
|
|
54
|
-
"jobs = {\n",
|
|
55
|
-
" \"NL\": {\"123\": [\"ssp126\", \"ssp245\"]},\n",
|
|
56
|
-
" \"DE\": {\"789\": [\"ssp585\"]},\n",
|
|
57
|
-
"}\n",
|
|
58
|
-
"# -> jobs: (NL,123,ssp126) (NL,123,ssp245) (DE,789,ssp585)"
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"cell_type": "markdown",
|
|
63
|
-
"metadata": {},
|
|
64
|
-
"source": [
|
|
65
|
-
"## HPC connection\n",
|
|
66
|
-
"\n",
|
|
67
|
-
"Your cluster login details. The project dir is derived so it can't drift."
|
|
68
|
-
]
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
"cell_type": "code",
|
|
72
|
-
"execution_count": null,
|
|
73
|
-
"metadata": {},
|
|
74
|
-
"outputs": [],
|
|
75
|
-
"source": [
|
|
76
|
-
"username_on_slurm = \"me\"\n",
|
|
77
|
-
"slurm_host = \"spider.surf.nl\"\n",
|
|
78
|
-
"project_dir_on_slurm = f\"/home/{username_on_slurm}/{project_name}\"\n",
|
|
79
|
-
"ssh_key_file = \"~/.ssh/id_ed25519\""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
" name=
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
")\n",
|
|
168
|
-
"\n",
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# 0. Configure your run\n",
|
|
8
|
+
"\n",
|
|
9
|
+
"This is the only notebook you normally edit. Fill in the cells below, then run it\n",
|
|
10
|
+
"top to bottom. It saves two files in the project root:\n",
|
|
11
|
+
"\n",
|
|
12
|
+
"* **`control_config.json`** — all your settings, read by `1_build`, `2_submit`\n",
|
|
13
|
+
" and `3_sync` (so they always agree).\n",
|
|
14
|
+
"* **`jobs.json`** — the nested list of jobs to run.\n",
|
|
15
|
+
"\n",
|
|
16
|
+
"Re-run this notebook whenever you change a setting."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"cell_type": "markdown",
|
|
21
|
+
"metadata": {},
|
|
22
|
+
"source": [
|
|
23
|
+
"## Project\n",
|
|
24
|
+
"\n",
|
|
25
|
+
"Name, the notebooks to run in order, and what varies per job."
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"cell_type": "code",
|
|
30
|
+
"execution_count": null,
|
|
31
|
+
"metadata": {},
|
|
32
|
+
"outputs": [],
|
|
33
|
+
"source": [
|
|
34
|
+
"project_name = \"myproject\"\n",
|
|
35
|
+
"notebooks = [\n",
|
|
36
|
+
" \"notebooks/0_settings.ipynb\", # first notebook: writes settings.json\n",
|
|
37
|
+
" \"notebooks/1_analysis.ipynb\",\n",
|
|
38
|
+
" \"notebooks/2_more.ipynb\",\n",
|
|
39
|
+
"]\n",
|
|
40
|
+
"varying = [\"country\", \"region\", \"scenario\"] # the levels in jobs.json (below)"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"cell_type": "markdown",
|
|
45
|
+
"metadata": {},
|
|
46
|
+
"source": "## The jobs to run\n\nA nested dict: each root-to-leaf path is one SLURM job, and the levels line up\nwith `varying` above. nb2slurm builds the matching output folders and submits one\njob per leaf.\n\n**Format:** a *dict* nests one more level, a *list* at the bottom means several\njobs sharing that parent, and `None`/`[]` ends the path. It's plain JSON, so for\nsmall runs type it by hand (below) and for big runs generate it in Python (a\ncomprehension, a CSV, an API query, ...) — see the README for the rules. The only\nthing that matters is that the finished dict lands in `jobs.json`."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"cell_type": "code",
|
|
50
|
+
"execution_count": null,
|
|
51
|
+
"metadata": {},
|
|
52
|
+
"outputs": [],
|
|
53
|
+
"source": [
|
|
54
|
+
"jobs = {\n",
|
|
55
|
+
" \"NL\": {\"123\": [\"ssp126\", \"ssp245\"]},\n",
|
|
56
|
+
" \"DE\": {\"789\": [\"ssp585\"]},\n",
|
|
57
|
+
"}\n",
|
|
58
|
+
"# -> jobs: (NL,123,ssp126) (NL,123,ssp245) (DE,789,ssp585)"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"cell_type": "markdown",
|
|
63
|
+
"metadata": {},
|
|
64
|
+
"source": [
|
|
65
|
+
"## HPC connection\n",
|
|
66
|
+
"\n",
|
|
67
|
+
"Your cluster login details. The project dir is derived so it can't drift."
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"cell_type": "code",
|
|
72
|
+
"execution_count": null,
|
|
73
|
+
"metadata": {},
|
|
74
|
+
"outputs": [],
|
|
75
|
+
"source": [
|
|
76
|
+
"username_on_slurm = \"me\"\n",
|
|
77
|
+
"slurm_host = \"spider.surf.nl\"\n",
|
|
78
|
+
"project_dir_on_slurm = f\"/home/{username_on_slurm}/{project_name}\"\n",
|
|
79
|
+
"ssh_key_file = \"~/.ssh/id_ed25519\"\n",
|
|
80
|
+
"\n",
|
|
81
|
+
"# No key yet? nb2slurm.generate_key(key_type=\"ed25519\") makes ~/.ssh/id_ed25519\n",
|
|
82
|
+
"# and prints the public key to register with your HPC. ssh-add it (or pass\n",
|
|
83
|
+
"# passphrase= to SSHConfig) if the key has a passphrase."
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"cell_type": "markdown",
|
|
88
|
+
"metadata": {},
|
|
89
|
+
"source": [
|
|
90
|
+
"## Resources per job\n",
|
|
91
|
+
"\n",
|
|
92
|
+
"What each SLURM job asks for, and where outputs go."
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"cell_type": "code",
|
|
97
|
+
"execution_count": null,
|
|
98
|
+
"metadata": {},
|
|
99
|
+
"outputs": [],
|
|
100
|
+
"source": "time_for_job_to_run = \"04:00:00\" # HH:MM:SS wall-clock limit\ncpus_per_job = 2\nnodes_per_job = 1\njobs_at_once = 3 # max running in parallel\noutput_directory = \"output\" # could be \"/scratch/me/output\""
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"cell_type": "markdown",
|
|
104
|
+
"metadata": {},
|
|
105
|
+
"source": "## Conda environment (optional)\n\nSkip if your cluster already provides Python (set `conda_env`/`setup` instead).\nOtherwise nb2slurm builds this environment + kernel for you in `1_build`.\n\n*Need a different environment for one or two notebooks (e.g. a calibration step)?*\nAdd `kernels={\"notebooks/step_8.ipynb\": \"myenv2\"}` and\n`extra_environments=[nb2slurm.Environment(name=\"myenv2\", kernel=\"myenv2\", ...)]`\nto the `Workflow` in the **Save** cell below — see the README for details. The\nsingle-environment setup here covers the common case."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"cell_type": "code",
|
|
109
|
+
"execution_count": null,
|
|
110
|
+
"metadata": {},
|
|
111
|
+
"outputs": [],
|
|
112
|
+
"source": [
|
|
113
|
+
"conda_env_name = \"myenv\"\n",
|
|
114
|
+
"kernel_name = \"myenv\"\n",
|
|
115
|
+
"conda_packages = [\"xarray\", \"numpy\"]\n",
|
|
116
|
+
"pip_packages = [\"nb2slurm\", \"ewatercycle\"]"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"cell_type": "markdown",
|
|
121
|
+
"metadata": {},
|
|
122
|
+
"source": [
|
|
123
|
+
"## Data mounts (optional)\n",
|
|
124
|
+
"\n",
|
|
125
|
+
"rclone mounts the job should set up before running."
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"cell_type": "code",
|
|
130
|
+
"execution_count": null,
|
|
131
|
+
"metadata": {},
|
|
132
|
+
"outputs": [],
|
|
133
|
+
"source": [
|
|
134
|
+
"data_mounts = [\n",
|
|
135
|
+
" {\"remote\": \"dcache:/climate-data/caravan\", \"mountpoint\": \"/scratch/caravan\"},\n",
|
|
136
|
+
"]"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"cell_type": "markdown",
|
|
141
|
+
"metadata": {},
|
|
142
|
+
"source": [
|
|
143
|
+
"## Save\n",
|
|
144
|
+
"\n",
|
|
145
|
+
"Builds the objects from your settings and writes `control_config.json` and\n",
|
|
146
|
+
"`jobs.json`. The other notebooks load these — you don't edit them by hand."
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"cell_type": "code",
|
|
151
|
+
"execution_count": null,
|
|
152
|
+
"metadata": {},
|
|
153
|
+
"outputs": [],
|
|
154
|
+
"source": [
|
|
155
|
+
"import json, pathlib, nb2slurm\n",
|
|
156
|
+
"\n",
|
|
157
|
+
"env = nb2slurm.Environment(\n",
|
|
158
|
+
" name=conda_env_name, kernel=kernel_name,\n",
|
|
159
|
+
" conda_packages=conda_packages, pip_packages=pip_packages,\n",
|
|
160
|
+
")\n",
|
|
161
|
+
"wf = nb2slurm.Workflow(\n",
|
|
162
|
+
" name=project_name, notebooks=notebooks, kernel=kernel_name,\n",
|
|
163
|
+
" varying=varying, jobs_json=\"jobs.json\",\n",
|
|
164
|
+
" resources=dict(nodes=nodes_per_job, cpus=cpus_per_job, time=time_for_job_to_run),\n",
|
|
165
|
+
" mounts=data_mounts, concurrency=jobs_at_once,\n",
|
|
166
|
+
" output_dir=output_directory, environment=env,\n",
|
|
167
|
+
")\n",
|
|
168
|
+
"cfg = nb2slurm.SSHConfig(\n",
|
|
169
|
+
" host=slurm_host, user=username_on_slurm,\n",
|
|
170
|
+
" remote_dir=project_dir_on_slurm, key_filename=ssh_key_file,\n",
|
|
171
|
+
")\n",
|
|
172
|
+
"\n",
|
|
173
|
+
"pathlib.Path(\"jobs.json\").write_text(json.dumps(jobs, indent=2))\n",
|
|
174
|
+
"nb2slurm.save_config(\"control_config.json\", workflow=wf, ssh=cfg)\n",
|
|
175
|
+
"print(\"saved control_config.json and jobs.json\")"
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
"metadata": {
|
|
180
|
+
"kernelspec": {
|
|
181
|
+
"display_name": "Python 3",
|
|
182
|
+
"language": "python",
|
|
183
|
+
"name": "python3"
|
|
184
|
+
},
|
|
185
|
+
"language_info": {
|
|
186
|
+
"name": "python"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"nbformat": 4,
|
|
190
|
+
"nbformat_minor": 5
|
|
191
|
+
}
|