bindsight 0.3.5__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.
- bindsight/README.md +44 -0
- bindsight/__init__.py +15 -0
- bindsight/benchmark/__init__.py +71 -0
- bindsight/benchmark/core.py +497 -0
- bindsight/benchmark/defaults.py +28 -0
- bindsight/benchmark/designer_bench.py +899 -0
- bindsight/benchmark/outcomes.py +508 -0
- bindsight/benchmark/panel.py +621 -0
- bindsight/benchmark/rediscovery.py +1153 -0
- bindsight/benchmark/statistics.py +622 -0
- bindsight/benchmark/study.py +1153 -0
- bindsight/benchmark/study_figures.py +164 -0
- bindsight/benchmark/study_report.py +607 -0
- bindsight/cli.py +2429 -0
- bindsight/config.py +451 -0
- bindsight/cost.py +258 -0
- bindsight/deg/__init__.py +7 -0
- bindsight/deg/pydeseq2_runner.py +285 -0
- bindsight/design/__init__.py +23 -0
- bindsight/design/_common.py +501 -0
- bindsight/design/bindcraft.py +78 -0
- bindsight/design/boltzgen.py +79 -0
- bindsight/design/developability.py +140 -0
- bindsight/design/embeddings.py +165 -0
- bindsight/design/prescreen.py +169 -0
- bindsight/design/protocol.py +111 -0
- bindsight/design/rfdiff_mpnn.py +93 -0
- bindsight/epitopes/__init__.py +7 -0
- bindsight/epitopes/surface_bind.py +134 -0
- bindsight/export/__init__.py +7 -0
- bindsight/export/ro_crate.py +595 -0
- bindsight/io/__init__.py +19 -0
- bindsight/io/cbioportal.py +118 -0
- bindsight/io/gdc.py +343 -0
- bindsight/io/paths.py +124 -0
- bindsight/pipelines/__init__.py +9 -0
- bindsight/pipelines/caveats.py +60 -0
- bindsight/pipelines/discover.py +1443 -0
- bindsight/pipelines/full_run.py +410 -0
- bindsight/plugins.py +335 -0
- bindsight/provenance/__init__.py +34 -0
- bindsight/provenance/append.py +199 -0
- bindsight/provenance/fragments.py +216 -0
- bindsight/provenance/manifest.py +506 -0
- bindsight/rank/__init__.py +7 -0
- bindsight/rank/scoring.py +355 -0
- bindsight/report/__init__.py +39 -0
- bindsight/report/format.py +100 -0
- bindsight/report/html.py +645 -0
- bindsight/report/showcase.py +653 -0
- bindsight/report/templates/report.css +67 -0
- bindsight/report/templates/report.html.j2 +420 -0
- bindsight/report/theme.py +231 -0
- bindsight/report/web/__init__.py +35 -0
- bindsight/report/web/app.py +624 -0
- bindsight/report/web/static/binder_viewer.js +245 -0
- bindsight/report/web/static/bindsight.css +455 -0
- bindsight/report/web/static/charts.js +425 -0
- bindsight/report/web/static/vendor/3Dmol-min.js +2 -0
- bindsight/report/web/static/vendor/3Dmol.LICENSE +90 -0
- bindsight/report/web/static/your_data_check.js +370 -0
- bindsight/report/web/templates/base.html.j2 +48 -0
- bindsight/report/web/templates/evidence.html.j2 +341 -0
- bindsight/report/web/templates/overview.html.j2 +140 -0
- bindsight/report/web/templates/runs.html.j2 +125 -0
- bindsight/report/web/templates/try.html.j2 +155 -0
- bindsight/report/web/templates/your_data.html.j2 +46 -0
- bindsight/runners/__init__.py +7 -0
- bindsight/runners/colab.py +158 -0
- bindsight/runners/job_exec.py +1110 -0
- bindsight/runners/kaggle.py +425 -0
- bindsight/runners/kaggle_kernel.py +489 -0
- bindsight/runners/local_docker.py +154 -0
- bindsight/runners/mock.py +167 -0
- bindsight/runners/modal_runner.py +199 -0
- bindsight/runners/notebook.py +116 -0
- bindsight/runners/notebook_content.py +217 -0
- bindsight/runners/protocol.py +81 -0
- bindsight/runners/source_wheel.py +104 -0
- bindsight/runners/tools.py +997 -0
- bindsight/structures/__init__.py +7 -0
- bindsight/structures/alphafolddb.py +93 -0
- bindsight/structures/plddt.py +92 -0
- bindsight/structures/topology.py +227 -0
- bindsight/surfaceome/__init__.py +42 -0
- bindsight/surfaceome/data/surfaceome_extended.tsv +4814 -0
- bindsight/surfaceome/data/surfy_offline.txt +17 -0
- bindsight/surfaceome/data/surfy_v1.ensembl.tsv +3380 -0
- bindsight/surfaceome/data/surfy_v1.uniprot.txt +2901 -0
- bindsight/surfaceome/surfy.py +441 -0
- bindsight/targets/__init__.py +7 -0
- bindsight/targets/ensembl_uniprot.py +56 -0
- bindsight/targets/gtex.py +250 -0
- bindsight/targets/open_targets.py +239 -0
- bindsight/validate/__init__.py +7 -0
- bindsight/validate/af2_ig.py +49 -0
- bindsight/validate/boltz2.py +256 -0
- bindsight/validate/chai1r.py +42 -0
- bindsight/validate/protocol.py +188 -0
- bindsight-0.3.5.data/data/bindsight_demo/config.yaml +54 -0
- bindsight-0.3.5.dist-info/METADATA +495 -0
- bindsight-0.3.5.dist-info/RECORD +106 -0
- bindsight-0.3.5.dist-info/WHEEL +4 -0
- bindsight-0.3.5.dist-info/entry_points.txt +20 -0
- bindsight-0.3.5.dist-info/licenses/COPYRIGHT +24 -0
- bindsight-0.3.5.dist-info/licenses/LICENSE +681 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""Builder for the self-contained Kaggle kernel that runs the design+validation job.
|
|
4
|
+
|
|
5
|
+
Kaggle's *default* free GPU is a **Tesla P100 (sm_60)**, which current PyTorch
|
|
6
|
+
cannot drive at all: it ships no Pascal kernels, so CUDA reports available and
|
|
7
|
+
the first real launch dies hours into a run. This kernel therefore pins
|
|
8
|
+
``machine_shape`` to a **Tesla T4 (sm_75)** and checks compute capability before
|
|
9
|
+
doing any work.
|
|
10
|
+
|
|
11
|
+
Even on a T4, the preinstalled stack
|
|
12
|
+
(py3.12 / torch 2.10+cu128) supports neither that image's CUDA nor RFdiffusion's
|
|
13
|
+
legacy requirements. RFdiffusion (the only designer that fits 16 GB) needs an old
|
|
14
|
+
py3.9 / torch-1.12 environment, and that environment is mutually exclusive with
|
|
15
|
+
the modern Boltz-2 validator (torch ≥2.2 / py ≥3.10). So the kernel builds **two
|
|
16
|
+
micromamba environments** on the T4 and runs the existing executor
|
|
17
|
+
(:mod:`bindsight.runners.job_exec`) across them:
|
|
18
|
+
|
|
19
|
+
- ``se3`` (py3.9, torch 1.12.1+cu113 — supports sm_60): RFdiffusion + ProteinMPNN.
|
|
20
|
+
- ``boltz`` (py3.11, torch 2.2.2+cu118 — still ships sm_60): Boltz-2 + bindsight.
|
|
21
|
+
|
|
22
|
+
``job_exec`` runs under ``boltz`` and invokes the design tools with the ``se3``
|
|
23
|
+
interpreter via ``BINDSIGHT_DESIGN_PYTHON`` (a wrapper that does ``micromamba run
|
|
24
|
+
-p .../se3``), so there is still exactly one executor — only the per-tool
|
|
25
|
+
interpreter differs. The design spec + target structure are embedded as base64
|
|
26
|
+
(no Kaggle dataset needed); the results tarball is written to
|
|
27
|
+
``/kaggle/working/<handle_id>.tar.gz`` for :meth:`KaggleRunner.fetch`.
|
|
28
|
+
|
|
29
|
+
The pinned upstream revisions come from :mod:`bindsight.runners.tools` so the
|
|
30
|
+
kernel can never drift from the Colab/Modal/local paths.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from bindsight.runners import tools
|
|
38
|
+
|
|
39
|
+
# Channels-free pip recipe for RFdiffusion's legacy env. RFdiffusion's own
|
|
40
|
+
# env/SE3nv.yml pins pytorch=1.9/cudatoolkit=11.1 via conda channels whose ancient
|
|
41
|
+
# builds are unreliable to solve today; the cu113 pip wheels below are the
|
|
42
|
+
# community-proven equivalent and (unlike Kaggle's default cu128 torch) include the
|
|
43
|
+
# P100's sm_60 kernels. SE3-Transformer's own requirements (e3nn==0.3.3, dllogger,
|
|
44
|
+
# …) come from RFdiffusion/env/SE3Transformer/requirements.txt at install time.
|
|
45
|
+
# cu113 is the newest CUDA wheel line for torch 1.12.1; the dgl cu113 wheels at the
|
|
46
|
+
# find-links below are therefore built against this exact torch. Package name is
|
|
47
|
+
# plain ``dgl`` with a ``+cu113`` local version (the old ``dgl-cu113`` name is gone).
|
|
48
|
+
_SE3_TORCH = "torch==1.12.1+cu113"
|
|
49
|
+
_SE3_TORCH_INDEX = "https://download.pytorch.org/whl/cu113"
|
|
50
|
+
_SE3_DGL = "dgl==1.0.2+cu113"
|
|
51
|
+
_SE3_DGL_FIND = "https://data.dgl.ai/wheels/cu113/repo.html"
|
|
52
|
+
_SE3_EXTRA_PIP = [
|
|
53
|
+
"hydra-core==1.3.2",
|
|
54
|
+
"pyrsistent",
|
|
55
|
+
"omegaconf",
|
|
56
|
+
"icecream",
|
|
57
|
+
"scipy",
|
|
58
|
+
"numpy<1.24",
|
|
59
|
+
"opt_einsum",
|
|
60
|
+
"opt_einsum_fx",
|
|
61
|
+
"e3nn==0.3.3",
|
|
62
|
+
]
|
|
63
|
+
# Boltz-2 env: a torch that still ships sm_60 (Kaggle's default cu128 does not).
|
|
64
|
+
_BOLTZ_TORCH = "torch==2.2.2"
|
|
65
|
+
_BOLTZ_TORCH_INDEX = "https://download.pytorch.org/whl/cu118"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_BINDSIGHT_REPO = "https://github.com/mikhaeelatefrizk/bindsight.git"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_kernel_script(
|
|
72
|
+
*,
|
|
73
|
+
handle_id: str,
|
|
74
|
+
payload: dict[str, str],
|
|
75
|
+
bindsight_ref: str | None = None,
|
|
76
|
+
n_trajectories_note: int | None = None,
|
|
77
|
+
bindsight_wheel_b64: str | None = None,
|
|
78
|
+
bindsight_wheel_name: str = "bindsight-0.0.0-py3-none-any.whl",
|
|
79
|
+
mode: str = "design_and_validate",
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Return the full Python source of the Kaggle kernel.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
handle_id: stable id; the results tarball is ``<handle_id>.tar.gz``.
|
|
85
|
+
payload: ``{filename: base64}`` for the spec dir (``spec.json`` + the
|
|
86
|
+
target structure file the spec references).
|
|
87
|
+
bindsight_ref: optional git ref (branch/tag/sha) to install bindsight from;
|
|
88
|
+
defaults to the repository's default branch. Used to run a feature
|
|
89
|
+
branch before it is merged.
|
|
90
|
+
n_trajectories_note: optional, embedded only as a log breadcrumb.
|
|
91
|
+
bindsight_wheel_b64: optional base64 of a wheel built from the working
|
|
92
|
+
tree. When given the kernel installs it instead of pip-installing
|
|
93
|
+
from git, which is the only way an unpushed change reaches the GPU.
|
|
94
|
+
Without it a run silently exercises whatever the ref resolves to.
|
|
95
|
+
bindsight_wheel_name: the wheel's filename, which must be a valid PEP 427
|
|
96
|
+
name. pip parses the distribution, version and compatibility tags out
|
|
97
|
+
of it and rejects anything else outright: writing the payload to
|
|
98
|
+
``bindsight-embedded.whl`` failed a run with "Invalid wheel filename
|
|
99
|
+
(wrong number of parts)" after the environment build had finished.
|
|
100
|
+
mode: ``design_and_validate`` or ``validate_only``. The latter skips the
|
|
101
|
+
RFdiffusion clone, its checkpoint download and the entire se3
|
|
102
|
+
environment, none of which a re-scoring job touches.
|
|
103
|
+
"""
|
|
104
|
+
git_url = f"git+{_BINDSIGHT_REPO}" + (f"@{bindsight_ref}" if bindsight_ref else "")
|
|
105
|
+
# A wheel built from the working tree beats any git ref: it is the code that
|
|
106
|
+
# launched the run, rather than whatever the named branch points at by the
|
|
107
|
+
# time pip resolves it. Without one, an unpushed fix is invisible to the GPU.
|
|
108
|
+
install_source = "embedded wheel" if bindsight_wheel_b64 else git_url
|
|
109
|
+
# Emit Python *literals* with repr(), not json.dumps() — JSON's null/true/false
|
|
110
|
+
# are not valid Python identifiers (an early bug embedded ``= null``).
|
|
111
|
+
consts = {
|
|
112
|
+
"HANDLE_ID": handle_id,
|
|
113
|
+
"PAYLOAD": payload,
|
|
114
|
+
"N_TRAJ_NOTE": n_trajectories_note,
|
|
115
|
+
"RFDIFF_REPO": tools.RFDIFF_REPO,
|
|
116
|
+
"RFDIFF_COMMIT": tools.RFDIFF_COMMIT,
|
|
117
|
+
"RFDIFF_WEIGHTS": tools.RFDIFF_WEIGHTS,
|
|
118
|
+
"RFDIFF_WEIGHT_SHA256": tools.RFDIFF_WEIGHT_SHA256,
|
|
119
|
+
"MPNN_REPO": tools.PROTEINMPNN_REPO,
|
|
120
|
+
"MPNN_COMMIT": tools.PROTEINMPNN_COMMIT,
|
|
121
|
+
"BOLTZ_PIP": tools.BOLTZ_PIP,
|
|
122
|
+
"SE3_TORCH": _SE3_TORCH,
|
|
123
|
+
"SE3_TORCH_INDEX": _SE3_TORCH_INDEX,
|
|
124
|
+
"SE3_DGL": _SE3_DGL,
|
|
125
|
+
"SE3_DGL_FIND": _SE3_DGL_FIND,
|
|
126
|
+
"SE3_EXTRA_PIP": _SE3_EXTRA_PIP,
|
|
127
|
+
"BOLTZ_TORCH": _BOLTZ_TORCH,
|
|
128
|
+
"BOLTZ_TORCH_INDEX": _BOLTZ_TORCH_INDEX,
|
|
129
|
+
"BINDSIGHT_GIT": git_url,
|
|
130
|
+
"BINDSIGHT_WHEEL_B64": bindsight_wheel_b64 or "",
|
|
131
|
+
"BINDSIGHT_WHEEL_NAME": bindsight_wheel_name,
|
|
132
|
+
"INSTALL_SOURCE": install_source,
|
|
133
|
+
"MIN_CC": MIN_COMPUTE_CAPABILITY,
|
|
134
|
+
"MODE": mode,
|
|
135
|
+
}
|
|
136
|
+
header_lines = ["# Auto-generated by bindsight KaggleRunner — split-env design+validation."]
|
|
137
|
+
header_lines += [f"{name} = {value!r}" for name, value in consts.items()]
|
|
138
|
+
return "\n".join(header_lines) + "\n\n" + _BODY
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
#: Kaggle accelerator to request. ``enable_gpu`` alone gets Kaggle's default,
|
|
142
|
+
#: which is a P100 — and Kaggle's own CLI documentation now warns that the P100
|
|
143
|
+
#: is unusable for GPU compute with the default image, because current PyTorch
|
|
144
|
+
#: builds ship no Pascal (sm_60) kernels. The failure mode is nasty:
|
|
145
|
+
#: ``torch.cuda.is_available()`` returns True and the first real kernel launch
|
|
146
|
+
#: dies with ``cudaErrorNoKernelImageForDevice``. The T4 (sm_75) is also what
|
|
147
|
+
#: current JAX supports, which is what makes BindCraft and AF2 initial-guess
|
|
148
|
+
#: reachable at all.
|
|
149
|
+
KAGGLE_ACCELERATOR = "NvidiaTeslaT4"
|
|
150
|
+
|
|
151
|
+
#: Compute capability the kernel requires. Turing is 7.5. Anything lower is a
|
|
152
|
+
#: P100 or older and cannot run the pinned stack; the kernel fails fast rather
|
|
153
|
+
#: than burning quota to discover it mid-run.
|
|
154
|
+
MIN_COMPUTE_CAPABILITY = (7, 5)
|
|
155
|
+
|
|
156
|
+
#: The same accelerator under the name :mod:`bindsight.cost` knows it by.
|
|
157
|
+
#:
|
|
158
|
+
#: These two constants have to agree. When the accelerator was pinned to a T4,
|
|
159
|
+
#: ``KaggleRunner`` kept defaulting to ``"P100"``, so every Kaggle runtime and
|
|
160
|
+
#: cost estimate was computed against a card the run would never be given —
|
|
161
|
+
#: a 3.0x slowdown factor applied where 4.0x was correct, quietly under-quoting
|
|
162
|
+
#: every estimate by a quarter. Deriving the default from here removes the
|
|
163
|
+
#: opportunity to make that mistake twice.
|
|
164
|
+
KAGGLE_COST_GPU = "T4"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def build_kernel_metadata(
|
|
168
|
+
*, username: str, slug: str, accelerator: str = KAGGLE_ACCELERATOR
|
|
169
|
+
) -> dict[str, Any]:
|
|
170
|
+
"""kernel-metadata.json for a GPU + internet script kernel (no dataset sources).
|
|
171
|
+
|
|
172
|
+
``machine_shape`` pins which accelerator Kaggle attaches. Without it Kaggle
|
|
173
|
+
picks its default, so the hardware a published result was produced on would
|
|
174
|
+
not be under version control. See :data:`KAGGLE_ACCELERATOR`.
|
|
175
|
+
"""
|
|
176
|
+
return {
|
|
177
|
+
"id": f"{username}/{slug}",
|
|
178
|
+
"title": slug,
|
|
179
|
+
"code_file": "kernel.py",
|
|
180
|
+
"language": "python",
|
|
181
|
+
"kernel_type": "script",
|
|
182
|
+
"is_private": True,
|
|
183
|
+
"enable_gpu": True,
|
|
184
|
+
"machine_shape": accelerator,
|
|
185
|
+
"enable_internet": True,
|
|
186
|
+
"dataset_sources": [],
|
|
187
|
+
"competition_sources": [],
|
|
188
|
+
"kernel_sources": [],
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# The static kernel body. References the variables defined in the generated header
|
|
193
|
+
# above; kept brace-safe (no .format) so the embedded shell/Python is verbatim.
|
|
194
|
+
_BODY = r'''
|
|
195
|
+
import base64, gzip, hashlib, json, os, pathlib, subprocess, sys, threading, time
|
|
196
|
+
|
|
197
|
+
MR = "/opt/mamba" # MAMBA_ROOT_PREFIX (off the /kaggle/working output volume)
|
|
198
|
+
MM = "/opt/micromamba/bin/micromamba"
|
|
199
|
+
TOOLS = "/opt/tools"
|
|
200
|
+
SE3 = f"{MR}/envs/se3"
|
|
201
|
+
BOLTZ = f"{MR}/envs/boltz"
|
|
202
|
+
# dgl's C lib dlopen()s CUDA 11's libcusparse.so.11 etc. via the system loader, so
|
|
203
|
+
# those libs must be on LD_LIBRARY_PATH. We install conda cudatoolkit=11.3 into the
|
|
204
|
+
# se3 env (puts them in $SE3/lib) and also keep torch's bundled libs (torch/lib).
|
|
205
|
+
SE3_LD = f"{SE3}/lib:{SE3}/lib/python3.9/site-packages/torch/lib"
|
|
206
|
+
os.environ["MAMBA_ROOT_PREFIX"] = MR
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def sh(cmd, env=None):
|
|
210
|
+
"""Run a shell command, streaming output; raise on non-zero exit."""
|
|
211
|
+
print(f"\n+ {cmd}", flush=True)
|
|
212
|
+
t0 = time.time()
|
|
213
|
+
r = subprocess.run(cmd, shell=True, env={**os.environ, **(env or {})})
|
|
214
|
+
print(f" ({time.time()-t0:.0f}s, exit={r.returncode})", flush=True)
|
|
215
|
+
if r.returncode != 0:
|
|
216
|
+
raise SystemExit(f"step failed: {cmd}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def disk_report(label):
|
|
220
|
+
"""Print free space on every volume that matters.
|
|
221
|
+
|
|
222
|
+
Kaggle documents 20 GB of persisted /kaggle/working and does not publish the
|
|
223
|
+
size of the scratch area, while this build needs roughly 60 GB for two torch
|
|
224
|
+
stacks plus model weights. Rather than assume it fits, every stage records
|
|
225
|
+
what was actually available, so the constraint is a published number in the
|
|
226
|
+
run log instead of a guess in a README.
|
|
227
|
+
"""
|
|
228
|
+
import shutil
|
|
229
|
+
|
|
230
|
+
parts = []
|
|
231
|
+
for path in ("/", "/kaggle/working", "/kaggle/temp", "/opt", "/tmp"):
|
|
232
|
+
try:
|
|
233
|
+
u = shutil.disk_usage(path)
|
|
234
|
+
except OSError:
|
|
235
|
+
continue
|
|
236
|
+
parts.append(f"{path} {u.free / 2**30:.1f}/{u.total / 2**30:.1f} GiB free")
|
|
237
|
+
print(f"[disk @ {label}] " + " | ".join(parts), flush=True)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
_VRAM_PEAK = [0]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _vram_now():
|
|
244
|
+
"""Device-wide VRAM in use, in MiB, or None if it cannot be read."""
|
|
245
|
+
out = subprocess.run(
|
|
246
|
+
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
|
|
247
|
+
capture_output=True, text=True,
|
|
248
|
+
)
|
|
249
|
+
if out.returncode != 0 or not out.stdout.strip():
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
return int(out.stdout.strip().splitlines()[0])
|
|
253
|
+
except ValueError:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _vram_sampler():
|
|
258
|
+
"""Record the high-water mark of VRAM for the whole kernel.
|
|
259
|
+
|
|
260
|
+
Sampling only between stages measured nothing: the work runs in subprocesses
|
|
261
|
+
under two separate micromamba environments, and by the time a stage boundary
|
|
262
|
+
is reached every one of them has exited and released its memory. A whole
|
|
263
|
+
3h49m design run reported 0 MiB at every stage while plainly using the GPU,
|
|
264
|
+
which made the "every memory claim is a measurement" promise false.
|
|
265
|
+
|
|
266
|
+
Device-wide sampling from a background thread is what actually observes it,
|
|
267
|
+
and it does so regardless of which environment or subprocess holds the
|
|
268
|
+
allocation — which is the reason not to use torch's own counter here.
|
|
269
|
+
"""
|
|
270
|
+
while True:
|
|
271
|
+
v = _vram_now()
|
|
272
|
+
if v is not None and v > _VRAM_PEAK[0]:
|
|
273
|
+
_VRAM_PEAK[0] = v
|
|
274
|
+
time.sleep(5)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
threading.Thread(target=_vram_sampler, daemon=True).start()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def gpu_report(label):
|
|
281
|
+
"""Print current and peak VRAM, so every memory claim is a measurement."""
|
|
282
|
+
out = subprocess.run(
|
|
283
|
+
["nvidia-smi", "--query-gpu=name,memory.used,memory.total",
|
|
284
|
+
"--format=csv,noheader,nounits"],
|
|
285
|
+
capture_output=True, text=True,
|
|
286
|
+
)
|
|
287
|
+
if out.returncode == 0 and out.stdout.strip():
|
|
288
|
+
print(
|
|
289
|
+
f"[gpu @ {label}] {out.stdout.strip()} | peak so far {_VRAM_PEAK[0]} MiB",
|
|
290
|
+
flush=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def step(msg):
|
|
295
|
+
print(f"\n========== {msg} ==========", flush=True)
|
|
296
|
+
disk_report(msg)
|
|
297
|
+
gpu_report(msg)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
step("GPU check")
|
|
301
|
+
sh("nvidia-smi --query-gpu=name,memory.total,compute_cap,driver_version "
|
|
302
|
+
"--format=csv,noheader || true")
|
|
303
|
+
|
|
304
|
+
# Fail fast on the wrong accelerator. On a P100 the default Kaggle image reports
|
|
305
|
+
# torch.cuda.is_available() == True and then dies on the first kernel launch with
|
|
306
|
+
# cudaErrorNoKernelImageForDevice, hours into the run. Checking compute capability
|
|
307
|
+
# up front costs a second and turns that into an immediate, legible failure.
|
|
308
|
+
_cc = subprocess.run(
|
|
309
|
+
["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader"],
|
|
310
|
+
capture_output=True, text=True,
|
|
311
|
+
)
|
|
312
|
+
_cc_text = (_cc.stdout or "").strip().splitlines()[0].strip() if _cc.stdout.strip() else ""
|
|
313
|
+
if _cc_text:
|
|
314
|
+
_major, _, _minor = _cc_text.partition(".")
|
|
315
|
+
_have = (int(_major), int(_minor or 0))
|
|
316
|
+
print(f"compute capability {_have[0]}.{_have[1]} (need >= {MIN_CC[0]}.{MIN_CC[1]})", flush=True)
|
|
317
|
+
if _have < tuple(MIN_CC):
|
|
318
|
+
raise SystemExit(
|
|
319
|
+
f"this kernel needs compute capability >= {MIN_CC[0]}.{MIN_CC[1]} "
|
|
320
|
+
f"but got {_have[0]}.{_have[1]}. Kaggle attached the wrong accelerator: set "
|
|
321
|
+
"machine_shape to NvidiaTeslaT4 in kernel-metadata.json. The default P100 "
|
|
322
|
+
"(6.0) reports CUDA as available and then fails on the first kernel launch."
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
print("WARNING: could not read compute capability; continuing unguarded", flush=True)
|
|
326
|
+
|
|
327
|
+
step("install micromamba")
|
|
328
|
+
pathlib.Path("/opt/micromamba/bin").mkdir(parents=True, exist_ok=True)
|
|
329
|
+
sh("curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest "
|
|
330
|
+
"| tar -xj -C /opt/micromamba bin/micromamba")
|
|
331
|
+
sh(f"{MM} --version")
|
|
332
|
+
|
|
333
|
+
# Everything RFdiffusion needs — the clone, ~480 MB of checkpoints, and the
|
|
334
|
+
# whole se3 environment — is design-only. A validate-only job re-scores
|
|
335
|
+
# binders that already exist, so building it there spent roughly six minutes
|
|
336
|
+
# and several gigabytes of a weekly quota on tools the job never invokes.
|
|
337
|
+
if MODE != "validate_only":
|
|
338
|
+
step("clone RFdiffusion + ProteinMPNN, fetch weights")
|
|
339
|
+
pathlib.Path(TOOLS).mkdir(parents=True, exist_ok=True)
|
|
340
|
+
if not pathlib.Path(f"{TOOLS}/RFdiffusion").exists():
|
|
341
|
+
sh(f"git clone -q {RFDIFF_REPO} {TOOLS}/RFdiffusion")
|
|
342
|
+
sh(f"git -C {TOOLS}/RFdiffusion checkout -q {RFDIFF_COMMIT}")
|
|
343
|
+
pathlib.Path(f"{TOOLS}/RFdiffusion/models").mkdir(parents=True, exist_ok=True)
|
|
344
|
+
for name, url in RFDIFF_WEIGHTS.items():
|
|
345
|
+
dst = f"{TOOLS}/RFdiffusion/models/{name}"
|
|
346
|
+
if not pathlib.Path(dst).exists():
|
|
347
|
+
sh(f"wget -q '{url}' -O '{dst}'")
|
|
348
|
+
# Hash every checkpoint and print it. Until now these ~480 MB of model
|
|
349
|
+
# parameters arrived over plain HTTP, were loaded and executed, and nothing
|
|
350
|
+
# checked that what landed was what was asked for. A truncated transfer
|
|
351
|
+
# produces a checkpoint that may still load and quietly design differently.
|
|
352
|
+
# Printing the digest unconditionally is also how a pin gets established:
|
|
353
|
+
# the first run publishes it into this log.
|
|
354
|
+
_h = hashlib.sha256()
|
|
355
|
+
with open(dst, "rb") as _fh:
|
|
356
|
+
for _chunk in iter(lambda: _fh.read(1 << 20), b""):
|
|
357
|
+
_h.update(_chunk)
|
|
358
|
+
_digest = _h.hexdigest()
|
|
359
|
+
print(f"checkpoint {name}: {pathlib.Path(dst).stat().st_size} bytes sha256={_digest}", flush=True)
|
|
360
|
+
_want = RFDIFF_WEIGHT_SHA256.get(name)
|
|
361
|
+
if _want and _digest != _want:
|
|
362
|
+
raise SystemExit(
|
|
363
|
+
f"{name}: sha256 {_digest} does not match the pinned {_want}. "
|
|
364
|
+
"Refusing to design against a checkpoint that is not the one this "
|
|
365
|
+
"result would claim."
|
|
366
|
+
)
|
|
367
|
+
if not pathlib.Path(f"{TOOLS}/ProteinMPNN").exists():
|
|
368
|
+
sh(f"git clone -q {MPNN_REPO} {TOOLS}/ProteinMPNN")
|
|
369
|
+
sh(f"git -C {TOOLS}/ProteinMPNN checkout -q {MPNN_COMMIT}")
|
|
370
|
+
|
|
371
|
+
step("build se3 env (RFdiffusion: py3.9 / torch1.12+cu113 — covers sm_75)")
|
|
372
|
+
sh(f"{MM} create -y -p {SE3} -c conda-forge python=3.9 pip")
|
|
373
|
+
# CUDA 11.3 runtime libs (libcusparse.so.11 …) that dgl's C library dlopen()s.
|
|
374
|
+
sh(f"{MM} install -y -p {SE3} -c conda-forge cudatoolkit=11.3")
|
|
375
|
+
sh(f"{MM} run -p {SE3} pip install -q --no-input {SE3_TORCH} --extra-index-url {SE3_TORCH_INDEX}")
|
|
376
|
+
sh(f"{MM} run -p {SE3} pip install -q --no-input {SE3_DGL} -f {SE3_DGL_FIND}")
|
|
377
|
+
sh(f"{MM} run -p {SE3} pip install -q --no-input " + " ".join(f"'{p}'" for p in SE3_EXTRA_PIP))
|
|
378
|
+
# SE3-Transformer's own requirements (e3nn pin, dllogger, …), then the packages.
|
|
379
|
+
sh(f"{MM} run -p {SE3} pip install -q --no-input -r {TOOLS}/RFdiffusion/env/SE3Transformer/requirements.txt")
|
|
380
|
+
sh(f"{MM} run -p {SE3} pip install -q --no-input --no-deps {TOOLS}/RFdiffusion/env/SE3Transformer")
|
|
381
|
+
sh(f"{MM} run -p {SE3} pip install -q --no-input --no-deps -e {TOOLS}/RFdiffusion")
|
|
382
|
+
|
|
383
|
+
step("write se3 interpreter wrapper (sets LD_LIBRARY_PATH for dgl's CUDA libs)")
|
|
384
|
+
# Invoke the se3 python directly (not via `micromamba run`) so the explicit
|
|
385
|
+
# LD_LIBRARY_PATH reliably reaches the loader when dgl dlopen()s its CUDA deps.
|
|
386
|
+
wrapper = "/opt/se3_python.sh"
|
|
387
|
+
pathlib.Path(wrapper).write_text(
|
|
388
|
+
"#!/bin/bash\n"
|
|
389
|
+
f"export LD_LIBRARY_PATH={SE3_LD}:$LD_LIBRARY_PATH\n"
|
|
390
|
+
"export DGLBACKEND=pytorch\n"
|
|
391
|
+
f"exec {SE3}/bin/python \"$@\"\n"
|
|
392
|
+
)
|
|
393
|
+
os.chmod(wrapper, 0o755)
|
|
394
|
+
|
|
395
|
+
# Sanity: RFdiffusion importable + CUDA usable on the pinned T4 — via the wrapper, so the
|
|
396
|
+
# import-time dgl CUDA-lib load is exercised exactly as the real run will do it.
|
|
397
|
+
sh(f"{wrapper} -c \""
|
|
398
|
+
"import torch, dgl, e3nn; "
|
|
399
|
+
"print('se3 torch', torch.__version__, 'dgl', dgl.__version__, 'cuda', torch.cuda.is_available(), "
|
|
400
|
+
"torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'NO-CUDA'); "
|
|
401
|
+
"x=torch.zeros(4,device='cuda'); print('se3 cuda op ok', (x+1).sum().item())\"")
|
|
402
|
+
|
|
403
|
+
else:
|
|
404
|
+
step("skip the se3 environment (validate_only: no designer runs)")
|
|
405
|
+
# There is no se3 interpreter to point at, so the name stays bound to None
|
|
406
|
+
# rather than going undefined: everything the design branch defines is read
|
|
407
|
+
# again further down, and a name that exists in only one branch turns a
|
|
408
|
+
# skipped step into a NameError at the end of a paid job.
|
|
409
|
+
wrapper = None
|
|
410
|
+
print("MODE=validate_only: RFdiffusion is not cloned, its checkpoints are "
|
|
411
|
+
"not fetched, and the se3 environment is not built.", flush=True)
|
|
412
|
+
|
|
413
|
+
step("build boltz env (Boltz-2 + bindsight: py3.11 / torch2.2+cu118 — covers sm_75)")
|
|
414
|
+
sh(f"{MM} create -y -p {BOLTZ} -c conda-forge python=3.11 pip")
|
|
415
|
+
sh(f"{MM} run -p {BOLTZ} pip install -q --no-input {BOLTZ_TORCH} --index-url {BOLTZ_TORCH_INDEX}")
|
|
416
|
+
sh(f"{MM} run -p {BOLTZ} pip install -q --no-input '{BOLTZ_PIP}' biopython")
|
|
417
|
+
# What the validator actually resolved to, recorded in the run log. The install
|
|
418
|
+
# above is quiet, so no run before this one recorded the version of the tool
|
|
419
|
+
# that produced its confidence numbers — the pin said what was asked for, and
|
|
420
|
+
# nothing said what arrived. A pin is a request; this is the receipt.
|
|
421
|
+
sh(f"{MM} run -p {BOLTZ} python -c \""
|
|
422
|
+
"from importlib.metadata import version; "
|
|
423
|
+
"print('boltz version', version('boltz'))\"")
|
|
424
|
+
print("bindsight install source:", INSTALL_SOURCE, flush=True)
|
|
425
|
+
if BINDSIGHT_WHEEL_B64:
|
|
426
|
+
# Install the exact tree that launched this run. Pinning a branch name would
|
|
427
|
+
# still leave the GPU installing whatever that branch points at now, and an
|
|
428
|
+
# unpushed commit would be invisible entirely.
|
|
429
|
+
# The filename matters: pip reads the distribution, version and
|
|
430
|
+
# compatibility tags out of it, and rejects a name it cannot parse.
|
|
431
|
+
_whl = f"/tmp/{BINDSIGHT_WHEEL_NAME}"
|
|
432
|
+
pathlib.Path(_whl).write_bytes(base64.b64decode(BINDSIGHT_WHEEL_B64))
|
|
433
|
+
print(" wheel bytes:", pathlib.Path(_whl).stat().st_size, flush=True)
|
|
434
|
+
sh(f"{MM} run -p {BOLTZ} pip install -q --no-input '{_whl}'")
|
|
435
|
+
else:
|
|
436
|
+
sh(f"{MM} run -p {BOLTZ} pip install -q --no-input {BINDSIGHT_GIT}")
|
|
437
|
+
sh(f"{MM} run -p {BOLTZ} python -c \""
|
|
438
|
+
"import bindsight; print('bindsight', bindsight.__version__, 'from', bindsight.__file__)\"")
|
|
439
|
+
sh(f"{MM} run -p {BOLTZ} python -c \""
|
|
440
|
+
"import torch; print('boltz torch', torch.__version__, 'cuda', torch.cuda.is_available(), "
|
|
441
|
+
"torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'NO-CUDA'); "
|
|
442
|
+
"x=torch.zeros(4,device='cuda'); print('boltz cuda op ok', (x+1).sum().item())\"")
|
|
443
|
+
|
|
444
|
+
step("patch Boltz-2 precision for non-bf16 GPUs (T4 is sm_75; bf16 needs sm_80+)")
|
|
445
|
+
# Boltz-2 hardcodes precision="bf16-mixed" (main.py). bfloat16 needs compute
|
|
446
|
+
# capability 8.0+, and no free-tier GPU has it — the T4 this kernel pins is 7.5 —
|
|
447
|
+
# so force full fp32, which is what the boltz1 path already uses and is
|
|
448
|
+
# numerically safe. This patch stays necessary for as long as the free tier is
|
|
449
|
+
# pre-Ampere.
|
|
450
|
+
import glob as _glob # noqa: E402
|
|
451
|
+
|
|
452
|
+
_bm = _glob.glob(f"{BOLTZ}/lib/python*/site-packages/boltz/main.py")[0]
|
|
453
|
+
_txt = pathlib.Path(_bm).read_text()
|
|
454
|
+
_patched = _txt.replace('precision=32 if model == "boltz1" else "bf16-mixed"', "precision=32")
|
|
455
|
+
assert _patched != _txt, "could not find Boltz-2 precision line to patch"
|
|
456
|
+
pathlib.Path(_bm).write_text(_patched)
|
|
457
|
+
print("patched", _bm, "-> precision=32")
|
|
458
|
+
|
|
459
|
+
step("materialise spec + target structure (embedded base64)")
|
|
460
|
+
spec_dir = pathlib.Path("/tmp/spec"); spec_dir.mkdir(parents=True, exist_ok=True)
|
|
461
|
+
for name, b64 in PAYLOAD.items():
|
|
462
|
+
# The payload is gzipped: structures are text and shrink to about a fifth,
|
|
463
|
+
# which is what keeps the script under Kaggle's size limit once a
|
|
464
|
+
# working-tree wheel is embedded alongside them.
|
|
465
|
+
dest = spec_dir / name
|
|
466
|
+
if not dest.resolve().is_relative_to(spec_dir.resolve()):
|
|
467
|
+
raise SystemExit(f"payload entry escapes the spec directory: {name}")
|
|
468
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
469
|
+
dest.write_bytes(gzip.decompress(base64.b64decode(b64)))
|
|
470
|
+
print(" wrote", name, dest.stat().st_size, "bytes")
|
|
471
|
+
print(" spec:", (spec_dir / "spec.json").read_text()[:400])
|
|
472
|
+
|
|
473
|
+
step("run job_exec (RFdiffusion -> ProteinMPNN under se3; Boltz-2 + orchestration under boltz)")
|
|
474
|
+
out_tmp = f"/tmp/{HANDLE_ID}.tar.gz"
|
|
475
|
+
job_env = {"BINDSIGHT_TOOLS_ROOT": TOOLS}
|
|
476
|
+
if wrapper is not None:
|
|
477
|
+
# Left unset for validate_only, where bindsight falls back to the boltz
|
|
478
|
+
# environment's own interpreter. Exporting a path that was never built
|
|
479
|
+
# would dress a step this mode deliberately skips as an exec failure.
|
|
480
|
+
job_env["BINDSIGHT_DESIGN_PYTHON"] = wrapper
|
|
481
|
+
sh(f"{MM} run -p {BOLTZ} python -m bindsight.runners.job_exec "
|
|
482
|
+
f"{spec_dir}/spec.json {out_tmp}", env=job_env)
|
|
483
|
+
|
|
484
|
+
step("stage tarball to /kaggle/working (keep the output volume lean)")
|
|
485
|
+
pathlib.Path("/kaggle/working").mkdir(parents=True, exist_ok=True)
|
|
486
|
+
sh(f"cp {out_tmp} /kaggle/working/{HANDLE_ID}.tar.gz")
|
|
487
|
+
sz = pathlib.Path(f"/kaggle/working/{HANDLE_ID}.tar.gz").stat().st_size
|
|
488
|
+
print(f"\nDONE -> /kaggle/working/{HANDLE_ID}.tar.gz ({sz/1e6:.2f} MB)")
|
|
489
|
+
'''
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Mikhaeel Atef Rizk Wahba
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""Local-GPU runner.
|
|
4
|
+
|
|
5
|
+
For users with a local NVIDIA GPU. Two modes, selected by the ``native`` flag
|
|
6
|
+
(or the ``BINDSIGHT_LOCAL_NATIVE`` env var):
|
|
7
|
+
|
|
8
|
+
- **native** — runs :mod:`bindsight.runners.job_exec` in the current Python env
|
|
9
|
+
(a CUDA box with bindsight + the design tools installed). Simplest for an
|
|
10
|
+
already-provisioned workstation; also the mode CPU tests exercise (with
|
|
11
|
+
``job_exec`` monkeypatched).
|
|
12
|
+
- **docker** — ``docker run --gpus all <image> python -m bindsight.runners.job_exec``
|
|
13
|
+
against the pinned bindsight image, mounting the spec + results dirs. No local
|
|
14
|
+
Python deps beyond Docker.
|
|
15
|
+
|
|
16
|
+
Both call the same executor, so the design+validation pipeline is identical to
|
|
17
|
+
the Modal/Kaggle paths.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import uuid
|
|
27
|
+
from datetime import UTC, datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from bindsight.cost import estimate
|
|
31
|
+
from bindsight.runners.protocol import CostEstimate, JobHandle, JobStatus
|
|
32
|
+
|
|
33
|
+
LOG = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Track launched processes by handle id (JobHandle is frozen, so we can't stash
|
|
36
|
+
# the Popen on it). Maps handle id -> (Popen, tarball Path).
|
|
37
|
+
_PROCS: dict[str, tuple[subprocess.Popen[bytes], Path]] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LocalDockerRunner:
|
|
41
|
+
"""Run design+validation on a local GPU, natively or via Docker."""
|
|
42
|
+
|
|
43
|
+
name = "local_docker"
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
designer: str = "rfdiff_mpnn",
|
|
49
|
+
n_units_per_target: int = 50,
|
|
50
|
+
gpu_type: str = "A100-40GB",
|
|
51
|
+
image: str = "ghcr.io/mikhaeelatefrizk/bindsight:dev",
|
|
52
|
+
native: bool | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self.designer = designer
|
|
55
|
+
self.n_units_per_target = n_units_per_target
|
|
56
|
+
self.gpu_type = gpu_type
|
|
57
|
+
self.image = image
|
|
58
|
+
self.native = (
|
|
59
|
+
native
|
|
60
|
+
if native is not None
|
|
61
|
+
else os.environ.get("BINDSIGHT_LOCAL_NATIVE", "").lower() in {"1", "true", "yes"}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def estimate_cost(self, spec_size: int) -> CostEstimate:
|
|
65
|
+
"""Estimate cost (always $0 — your hardware)."""
|
|
66
|
+
return estimate(
|
|
67
|
+
backend=self.name,
|
|
68
|
+
stage="design",
|
|
69
|
+
plugin=self.designer,
|
|
70
|
+
n_units=spec_size,
|
|
71
|
+
gpu_type=self.gpu_type,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def submit(self, spec_path: Path, *, results_dir: Path) -> JobHandle:
|
|
75
|
+
"""Launch the executor (native subprocess or docker run); return a handle."""
|
|
76
|
+
results_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
handle_id = str(uuid.uuid4())
|
|
78
|
+
tarball = results_dir / f"{handle_id}.tar.gz"
|
|
79
|
+
|
|
80
|
+
if self.native:
|
|
81
|
+
cmd = [
|
|
82
|
+
sys.executable,
|
|
83
|
+
"-m",
|
|
84
|
+
"bindsight.runners.job_exec",
|
|
85
|
+
str(spec_path),
|
|
86
|
+
str(tarball),
|
|
87
|
+
]
|
|
88
|
+
else:
|
|
89
|
+
spec_dir = spec_path.parent.resolve()
|
|
90
|
+
cmd = [
|
|
91
|
+
"docker",
|
|
92
|
+
"run",
|
|
93
|
+
"--rm",
|
|
94
|
+
"--gpus",
|
|
95
|
+
"all",
|
|
96
|
+
"-v",
|
|
97
|
+
f"{spec_dir}:/spec",
|
|
98
|
+
"-v",
|
|
99
|
+
f"{results_dir.resolve()}:/results",
|
|
100
|
+
self.image,
|
|
101
|
+
"python",
|
|
102
|
+
"-m",
|
|
103
|
+
"bindsight.runners.job_exec",
|
|
104
|
+
f"/spec/{spec_path.name}",
|
|
105
|
+
f"/results/{handle_id}.tar.gz",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
LOG.info(
|
|
109
|
+
"local_docker submit (%s): %s", "native" if self.native else "docker", " ".join(cmd)
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
proc = subprocess.Popen(cmd)
|
|
113
|
+
except FileNotFoundError as e:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"docker not found — install Docker, or use native mode "
|
|
116
|
+
"(BINDSIGHT_LOCAL_NATIVE=1) on a machine with bindsight + the design tools."
|
|
117
|
+
if not self.native
|
|
118
|
+
else f"failed to launch executor: {e}"
|
|
119
|
+
) from e
|
|
120
|
+
_PROCS[handle_id] = (proc, tarball)
|
|
121
|
+
return JobHandle(
|
|
122
|
+
backend=self.name,
|
|
123
|
+
id=handle_id,
|
|
124
|
+
submitted_at=datetime.now(UTC).isoformat(timespec="seconds"),
|
|
125
|
+
results_dir=str(results_dir),
|
|
126
|
+
tarball=str(tarball),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def poll(self, handle: JobHandle) -> JobStatus:
|
|
130
|
+
"""Report queued/running/succeeded/failed from the tracked process."""
|
|
131
|
+
entry = _PROCS.get(handle.id)
|
|
132
|
+
tarball = Path(entry[1]) if entry else Path(getattr(handle, "tarball", ""))
|
|
133
|
+
if entry is None:
|
|
134
|
+
state = "succeeded" if tarball.exists() else "failed"
|
|
135
|
+
return JobStatus(handle=handle, state=state, log_tail=None)
|
|
136
|
+
proc, _ = entry
|
|
137
|
+
rc = proc.poll()
|
|
138
|
+
if rc is None:
|
|
139
|
+
return JobStatus(handle=handle, state="running", progress=None)
|
|
140
|
+
state = "succeeded" if rc == 0 and tarball.exists() else "failed"
|
|
141
|
+
return JobStatus(handle=handle, state=state, log_tail=f"exit code {rc}")
|
|
142
|
+
|
|
143
|
+
def fetch(self, handle: JobHandle) -> Path:
|
|
144
|
+
"""Block until the job finishes; return the results tarball path."""
|
|
145
|
+
entry = _PROCS.get(handle.id)
|
|
146
|
+
tarball = Path(entry[1]) if entry else Path(getattr(handle, "tarball", ""))
|
|
147
|
+
if entry is not None:
|
|
148
|
+
proc, _ = entry
|
|
149
|
+
rc = proc.wait()
|
|
150
|
+
if rc != 0:
|
|
151
|
+
raise RuntimeError(f"local_docker job {handle.id} failed (exit {rc})")
|
|
152
|
+
if not tarball.exists():
|
|
153
|
+
raise RuntimeError(f"local_docker job {handle.id} produced no results at {tarball}")
|
|
154
|
+
return tarball
|