cortexgrid 0.2.85__tar.gz → 0.2.86__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.
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/PKG-INFO +6 -4
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/_bundle.py +157 -23
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/jobs.py +13 -3
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/model_serving.py +27 -7
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/docs/cortexgrid/README.md +4 -3
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/pyproject.toml +2 -1
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/.gitignore +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/LICENSE +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/__init__.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/_ray_job_driver.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/_serve_entry.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/checkpoint.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/experiment.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/infra.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/mlflow_util.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/model_storage.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/py.typed +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/ray_util.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/s3_util.py +0 -0
- {cortexgrid-0.2.85 → cortexgrid-0.2.86}/cortexgrid/secrets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cortexgrid
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.86
|
|
4
4
|
Summary: Connect your ML code to the RoboLab compute cluster — Ray, MLflow, and S3
|
|
5
5
|
Project-URL: Homepage, https://github.com/robodatalab/cortexgrid
|
|
6
6
|
Project-URL: Repository, https://github.com/robodatalab/cortexgrid
|
|
@@ -13,6 +13,7 @@ Requires-Dist: cloudpickle>=3.0
|
|
|
13
13
|
Requires-Dist: fabric>=3.2.3
|
|
14
14
|
Requires-Dist: haikunator>=2.1.0
|
|
15
15
|
Requires-Dist: mlflow<4,>=3.11
|
|
16
|
+
Requires-Dist: packaging>=24
|
|
16
17
|
Requires-Dist: pip>=23.0
|
|
17
18
|
Requires-Dist: pydantic-settings>=2.13.1
|
|
18
19
|
Requires-Dist: pydantic>=2.13.3
|
|
@@ -115,9 +116,10 @@ print(f"Submitted: {job_id}")
|
|
|
115
116
|
A separate service — the **jobs control plane** — polls MLflow for pending job requests, matches them against the set of Ray submissions the cluster already has, and submits anything missing. It is also responsible for retrying failed jobs and honouring user-requested stops.
|
|
116
117
|
|
|
117
118
|
Each submission captures the code and dependencies the entry function needs automatically ([_bundle.py](https://github.com/robodatalab/cortexgrid/blob/main/cortexgrid/_bundle.py)):
|
|
118
|
-
- `bundle(entry)` traces the import graph from the function's source file, resolving each import the way the interpreter does (via `sys.path`)
|
|
119
|
-
-
|
|
120
|
-
-
|
|
119
|
+
- `bundle(entry)` traces the import graph from the function's source file, resolving each import the way the interpreter does (via `sys.path`). The standard library is excluded (it ships with the interpreter)
|
|
120
|
+
- Your own modules -- anything outside site-packages / dist-packages -- ship **as source**: they are staged at their import paths and tarred into the Ray `working_dir`
|
|
121
|
+
- Third-party packages are recorded as the installed distribution that owns the imported file, pinned to its installed version (`tqdm==4.67.3`), and Ray **pip-installs** them on the worker into a per-node cached virtualenv layered on the image (`runtime_env["pip"]`). An import into site-packages that no installed distribution owns fails `cortexgrid.remote` with `UnownedDependencyError`
|
|
122
|
+
- Distributions the worker image already has are not installed again: `worker_provides()` is the dependency closure of the packages baked into the ray image (torch and its CUDA stack, ray, mlflow, ...), and is subtracted from the pip list. See [k8s/docker/ray/Dockerfile](https://github.com/robodatalab/cortexgrid/blob/main/k8s/docker/ray/Dockerfile) and `_WORKER_BAKED` in `_bundle.py`, which must list the same packages
|
|
121
123
|
- Injects MLflow/S3 credentials so task code running on the DGX can reach all services
|
|
122
124
|
|
|
123
125
|
##### Retries
|
|
@@ -3,27 +3,38 @@
|
|
|
3
3
|
`bundle(seed)` describes what is needed to run the module at `seed`: the local
|
|
4
4
|
files it reaches -- following its import graph and each package's __init__
|
|
5
5
|
chain, resolving imports the way the interpreter does -- and the third-party
|
|
6
|
-
|
|
7
|
-
package location (site-packages / dist-packages)
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
distributions those files import. A file is local unless it lives in an installed
|
|
7
|
+
package location (site-packages / dist-packages). An import that resolves into
|
|
8
|
+
one is recorded as the distribution that installed the file, at its installed
|
|
9
|
+
version, and is not followed: installing that distribution brings its own
|
|
10
|
+
dependencies. The standard library is excluded (it ships with the interpreter).
|
|
11
|
+
Bundles of several seeds combine with `BundleDesc.merge`.
|
|
10
12
|
|
|
11
13
|
`stage(files, dest)` lays a bundle out under `dest` at each file's import path,
|
|
12
14
|
so `dest` on sys.path (e.g. a Ray working_dir) makes every module importable.
|
|
15
|
+
|
|
16
|
+
`BundleDesc.pip_requirements(worker_provides())` pins the third-party
|
|
17
|
+
distributions the Ray worker image does not already have, for a Ray `pip`
|
|
18
|
+
runtime_env to install on the worker.
|
|
13
19
|
"""
|
|
14
20
|
|
|
15
21
|
from __future__ import annotations
|
|
16
22
|
|
|
17
23
|
import ast
|
|
18
|
-
from collections.abc import Iterator
|
|
24
|
+
from collections.abc import Iterable, Iterator
|
|
19
25
|
from dataclasses import dataclass
|
|
20
26
|
import functools
|
|
21
27
|
import importlib.machinery
|
|
28
|
+
import importlib.metadata
|
|
22
29
|
import importlib.util
|
|
30
|
+
import os
|
|
23
31
|
from pathlib import Path
|
|
24
32
|
import shutil
|
|
25
33
|
import sys
|
|
26
34
|
|
|
35
|
+
from packaging.requirements import Requirement
|
|
36
|
+
from packaging.utils import canonicalize_name
|
|
37
|
+
|
|
27
38
|
|
|
28
39
|
ThirdPartyDependencyName = str
|
|
29
40
|
ThirdPartyDependencyVersion = str
|
|
@@ -39,17 +50,43 @@ class BundleDesc:
|
|
|
39
50
|
tp_deps={**self.tp_deps, **other.tp_deps},
|
|
40
51
|
)
|
|
41
52
|
|
|
53
|
+
def pip_requirements(
|
|
54
|
+
self, provided: frozenset[ThirdPartyDependencyName]
|
|
55
|
+
) -> list[str]:
|
|
56
|
+
"""The third-party distributions, pinned (`name==version`) and sorted,
|
|
57
|
+
minus the `provided` ones."""
|
|
58
|
+
return sorted(
|
|
59
|
+
f"{name}=={version}"
|
|
60
|
+
for name, version in self.tp_deps.items()
|
|
61
|
+
if name not in provided
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UnownedDependencyError(LookupError):
|
|
66
|
+
"""An import resolved into an installed package location, but no installed
|
|
67
|
+
distribution owns the file, so it can be neither shipped nor installed."""
|
|
68
|
+
|
|
42
69
|
|
|
43
70
|
def bundle(seed: Path) -> BundleDesc:
|
|
44
71
|
"""What is needed to run the module at `seed`: its local files, each at its
|
|
45
|
-
real path
|
|
46
|
-
|
|
72
|
+
real path, and the third-party distributions they import, keyed by
|
|
73
|
+
canonical name, each at its installed version.
|
|
74
|
+
|
|
75
|
+
Raises UnownedDependencyError for an import that resolves into an installed
|
|
76
|
+
package location no distribution owns."""
|
|
47
77
|
seed = seed.resolve()
|
|
48
78
|
files: set[Path] = set()
|
|
79
|
+
tp_deps: dict[ThirdPartyDependencyName, ThirdPartyDependencyVersion] = {}
|
|
80
|
+
visited: set[Path] = set()
|
|
49
81
|
queue: list[Path] = [seed]
|
|
50
82
|
while queue:
|
|
51
83
|
file = queue.pop()
|
|
52
|
-
if file in
|
|
84
|
+
if file in visited:
|
|
85
|
+
continue
|
|
86
|
+
visited.add(file)
|
|
87
|
+
if not _is_local(file):
|
|
88
|
+
dist = _owning_distribution(file)
|
|
89
|
+
tp_deps[canonicalize_name(dist.metadata["Name"])] = dist.version
|
|
53
90
|
continue
|
|
54
91
|
files.add(file)
|
|
55
92
|
queue.extend(_init_chain(file)) # importing a module runs its __init__ chain
|
|
@@ -58,7 +95,7 @@ def bundle(seed: Path) -> BundleDesc:
|
|
|
58
95
|
dep = _module_file(name)
|
|
59
96
|
if dep is not None:
|
|
60
97
|
queue.append(dep)
|
|
61
|
-
return BundleDesc(local_files=files, tp_deps=
|
|
98
|
+
return BundleDesc(local_files=files, tp_deps=tp_deps)
|
|
62
99
|
|
|
63
100
|
|
|
64
101
|
def stage(files: set[Path], dest: Path) -> None:
|
|
@@ -70,23 +107,120 @@ def stage(files: set[Path], dest: Path) -> None:
|
|
|
70
107
|
shutil.copy2(file, target)
|
|
71
108
|
|
|
72
109
|
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
|
|
110
|
+
# What the Ray worker image pip-installs, as the Dockerfile spells it. They and
|
|
111
|
+
# their dependency trees are on the worker already, so they are never installed
|
|
112
|
+
# there again -- a second copy in the job's virtualenv would shadow the image's.
|
|
113
|
+
#
|
|
114
|
+
# KEEP IN SYNC with k8s/docker/ray/Dockerfile, by hand: every package it
|
|
115
|
+
# pip-installs must be listed here. A package missing here gets installed a
|
|
116
|
+
# second time on the worker; one listed here but no longer in the image is
|
|
117
|
+
# never installed at all.
|
|
118
|
+
_WORKER_BAKED = (
|
|
119
|
+
"ray[default,serve]",
|
|
120
|
+
"smart_open[s3]",
|
|
121
|
+
"mlflow",
|
|
122
|
+
"python-dotenv",
|
|
123
|
+
"psutil",
|
|
124
|
+
"nvidia-cublas-cu12",
|
|
125
|
+
"nvidia-cudnn-cu12",
|
|
126
|
+
"nvidia-cuda-nvrtc-cu12",
|
|
127
|
+
"nvidia-cuda-runtime-cu12",
|
|
128
|
+
"nvidia-cuda-cupti-cu12",
|
|
129
|
+
"nvidia-cufft-cu12",
|
|
130
|
+
"nvidia-curand-cu12",
|
|
131
|
+
"nvidia-cusolver-cu12",
|
|
132
|
+
"nvidia-cusparse-cu12",
|
|
133
|
+
"nvidia-cusparselt-cu12",
|
|
134
|
+
"nvidia-nccl-cu12",
|
|
135
|
+
"nvidia-nvshmem-cu12",
|
|
136
|
+
"nvidia-nvtx-cu12",
|
|
137
|
+
"nvidia-nvjitlink-cu12",
|
|
138
|
+
"nvidia-cufile-cu12",
|
|
139
|
+
"cuda-bindings",
|
|
140
|
+
"triton",
|
|
141
|
+
"torch",
|
|
142
|
+
"filelock",
|
|
143
|
+
"typing-extensions",
|
|
144
|
+
"sympy",
|
|
145
|
+
"networkx",
|
|
146
|
+
"jinja2",
|
|
147
|
+
"fsspec",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@functools.lru_cache(maxsize=1)
|
|
152
|
+
def worker_provides() -> frozenset[ThirdPartyDependencyName]:
|
|
153
|
+
"""Every distribution the Ray worker image already provides:
|
|
154
|
+
`distribution_closure(_WORKER_BAKED)`."""
|
|
155
|
+
return distribution_closure(_WORKER_BAKED)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def distribution_closure(
|
|
159
|
+
requirements: Iterable[str],
|
|
160
|
+
) -> frozenset[ThirdPartyDependencyName]:
|
|
161
|
+
"""Canonical names of the distributions `requirements` name plus everything
|
|
162
|
+
they depend on, transitively, as this environment's installed metadata
|
|
163
|
+
declares it -- extras followed where requested, environment markers
|
|
164
|
+
evaluated here. A requirement not installed here contributes only its own
|
|
165
|
+
name: its dependencies are unknown."""
|
|
166
|
+
names: set[ThirdPartyDependencyName] = set()
|
|
167
|
+
seen: set[tuple[str, frozenset[str]]] = set()
|
|
168
|
+
queue = [Requirement(spec) for spec in requirements]
|
|
169
|
+
while queue:
|
|
170
|
+
requirement = queue.pop()
|
|
171
|
+
name = canonicalize_name(requirement.name)
|
|
172
|
+
key = (name, frozenset(requirement.extras))
|
|
173
|
+
if key in seen:
|
|
174
|
+
continue
|
|
175
|
+
seen.add(key)
|
|
176
|
+
names.add(name)
|
|
177
|
+
try:
|
|
178
|
+
dist = importlib.metadata.distribution(name)
|
|
179
|
+
except importlib.metadata.PackageNotFoundError:
|
|
180
|
+
continue
|
|
181
|
+
extras = requirement.extras or {""}
|
|
182
|
+
for spec in dist.requires or ():
|
|
183
|
+
dependency = Requirement(spec)
|
|
184
|
+
if dependency.marker is None or any(
|
|
185
|
+
dependency.marker.evaluate({"extra": extra}) for extra in extras
|
|
186
|
+
):
|
|
187
|
+
queue.append(dependency)
|
|
188
|
+
return frozenset(names)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _owning_distribution(file: Path) -> importlib.metadata.Distribution:
|
|
192
|
+
"""The installed distribution whose file list (RECORD) contains `file`.
|
|
193
|
+
Rebuilds the index once on a miss, in case something was installed since it
|
|
194
|
+
was built."""
|
|
195
|
+
path = tuple(sys.path)
|
|
196
|
+
dist = _distribution_index(path).get(file)
|
|
197
|
+
if dist is None:
|
|
198
|
+
_distribution_index.cache_clear()
|
|
199
|
+
dist = _distribution_index(path).get(file)
|
|
200
|
+
if dist is None:
|
|
201
|
+
raise UnownedDependencyError(
|
|
202
|
+
f"{file} is imported from an installed package location, but no "
|
|
203
|
+
"installed distribution lists it among its files, so it can be "
|
|
204
|
+
"neither shipped nor pip-installed on the worker. Install it with "
|
|
205
|
+
"pip or uv so it carries distribution metadata."
|
|
206
|
+
)
|
|
207
|
+
return dist
|
|
78
208
|
|
|
79
209
|
|
|
80
210
|
@functools.lru_cache(maxsize=1)
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
211
|
+
def _distribution_index(
|
|
212
|
+
path: tuple[str, ...],
|
|
213
|
+
) -> dict[Path, importlib.metadata.Distribution]:
|
|
214
|
+
"""Every file installed by a distribution found on `path` (sys.path, which
|
|
215
|
+
the cache is keyed on), mapped to that distribution. Each distribution's
|
|
216
|
+
root is resolved once; its files are joined onto it lexically, which keeps
|
|
217
|
+
this fast for environments with tens of thousands of files."""
|
|
218
|
+
index: dict[Path, importlib.metadata.Distribution] = {}
|
|
219
|
+
for dist in importlib.metadata.distributions(path=list(path)):
|
|
220
|
+
root = Path(dist.locate_file("")).resolve()
|
|
221
|
+
for file in dist.files or ():
|
|
222
|
+
index[Path(os.path.normpath(root / file))] = dist
|
|
223
|
+
return index
|
|
90
224
|
|
|
91
225
|
|
|
92
226
|
def _is_local(file: Path) -> bool:
|
|
@@ -63,6 +63,9 @@ class JobLifecycle:
|
|
|
63
63
|
retry: bool = False # static flag set at job creation
|
|
64
64
|
num_gpus: int = 0
|
|
65
65
|
num_cpus: int = 1
|
|
66
|
+
# static: pinned third-party requirements the worker pip-installs (the
|
|
67
|
+
# bundle's distributions the Ray image does not already provide)
|
|
68
|
+
pip_requirements: list[str] = field(default_factory=list)
|
|
66
69
|
history: list[LifecycleEvent] = field(default_factory=list)
|
|
67
70
|
|
|
68
71
|
def to_json(self) -> str:
|
|
@@ -229,11 +232,17 @@ def schedule_remote_job(
|
|
|
229
232
|
job_id = Haikunator().haikunate(token_length=2, token_chars="0123456789")
|
|
230
233
|
entry_file = Path(inspect.getfile(fn)).resolve()
|
|
231
234
|
driver_file = Path(__file__).with_name("_ray_job_driver.py")
|
|
232
|
-
|
|
235
|
+
desc = bundle(entry_file).merge(bundle(driver_file))
|
|
236
|
+
pip_requirements = desc.pip_requirements(worker_provides())
|
|
233
237
|
with tempfile.TemporaryDirectory() as tmp:
|
|
234
238
|
code_root = Path(tmp, "project_code_root")
|
|
235
|
-
stage(
|
|
236
|
-
log.info(
|
|
239
|
+
stage(desc.local_files, code_root)
|
|
240
|
+
log.info(
|
|
241
|
+
"Submitting job %s (%d files, pip: %s)",
|
|
242
|
+
job_id,
|
|
243
|
+
len(desc.local_files),
|
|
244
|
+
pip_requirements,
|
|
245
|
+
)
|
|
237
246
|
Payload(
|
|
238
247
|
experiment_name=experiment_name,
|
|
239
248
|
run_id=run_id,
|
|
@@ -252,6 +261,7 @@ def schedule_remote_job(
|
|
|
252
261
|
retry=retry,
|
|
253
262
|
num_gpus=num_gpus,
|
|
254
263
|
num_cpus=num_cpus,
|
|
264
|
+
pip_requirements=pip_requirements,
|
|
255
265
|
).save_to_mlflow()
|
|
256
266
|
return job_id
|
|
257
267
|
|
|
@@ -19,11 +19,12 @@ the end-to-end design.
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import inspect
|
|
22
|
+
import json
|
|
22
23
|
import logging
|
|
23
24
|
import shutil
|
|
24
25
|
import tempfile
|
|
25
26
|
import time
|
|
26
|
-
from dataclasses import dataclass
|
|
27
|
+
from dataclasses import dataclass, field
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import Any
|
|
29
30
|
|
|
@@ -90,6 +91,9 @@ class BundleMetadata:
|
|
|
90
91
|
|
|
91
92
|
bundle_url: str
|
|
92
93
|
class_import_path: str
|
|
94
|
+
# pinned third-party requirements the replica pip-installs (the bundle's
|
|
95
|
+
# distributions the Ray image does not already provide)
|
|
96
|
+
pip_requirements: list[str] = field(default_factory=list)
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def bundle_class(
|
|
@@ -103,12 +107,18 @@ def bundle_class(
|
|
|
103
107
|
without holding the class object."""
|
|
104
108
|
entry_file = Path(inspect.getfile(cls)).resolve()
|
|
105
109
|
serve_entry = Path(__file__).with_name("_serve_entry.py")
|
|
106
|
-
|
|
110
|
+
desc = bundle(entry_file).merge(bundle(serve_entry))
|
|
111
|
+
pip_requirements = desc.pip_requirements(worker_provides())
|
|
107
112
|
with tempfile.TemporaryDirectory() as tmp:
|
|
108
113
|
code_root = Path(tmp) / "code"
|
|
109
|
-
stage(
|
|
114
|
+
stage(desc.local_files, code_root)
|
|
110
115
|
log.info(
|
|
111
|
-
"Serve bundle for %s/%s/%s: %d files
|
|
116
|
+
"Serve bundle for %s/%s/%s: %d files, pip: %s",
|
|
117
|
+
family,
|
|
118
|
+
suffix,
|
|
119
|
+
run_name,
|
|
120
|
+
len(desc.local_files),
|
|
121
|
+
pip_requirements,
|
|
112
122
|
)
|
|
113
123
|
zip_base = Path(tmp) / f"{family}__{suffix}"
|
|
114
124
|
shutil.make_archive(str(zip_base), "zip", root_dir=str(code_root))
|
|
@@ -119,6 +129,7 @@ def bundle_class(
|
|
|
119
129
|
return BundleMetadata(
|
|
120
130
|
bundle_url=bundle_url,
|
|
121
131
|
class_import_path=f"{cls.__module__}:{cls.__name__}",
|
|
132
|
+
pip_requirements=pip_requirements,
|
|
122
133
|
)
|
|
123
134
|
|
|
124
135
|
|
|
@@ -126,6 +137,13 @@ def _build_application_spec(
|
|
|
126
137
|
family: str, suffix: str, run_name: str, meta: BundleMetadata
|
|
127
138
|
) -> dict[str, Any]:
|
|
128
139
|
"""Assemble a Ray Serve application schema from pre-bundled metadata."""
|
|
140
|
+
# working_dir carries the serve-app's own source; Ray pip-installs the
|
|
141
|
+
# third-party distributions the image lacks into a per-node cached
|
|
142
|
+
# virtualenv layered on the image. No pip key when there are none, so Ray
|
|
143
|
+
# builds no virtualenv.
|
|
144
|
+
runtime_env: dict[str, Any] = {"working_dir": meta.bundle_url}
|
|
145
|
+
if meta.pip_requirements:
|
|
146
|
+
runtime_env["pip"] = meta.pip_requirements
|
|
129
147
|
return {
|
|
130
148
|
"name": _app_name(family, suffix, run_name),
|
|
131
149
|
"route_prefix": _route_prefix(family, suffix, run_name),
|
|
@@ -140,9 +158,7 @@ def _build_application_spec(
|
|
|
140
158
|
"suffix": suffix,
|
|
141
159
|
"run_name": run_name,
|
|
142
160
|
},
|
|
143
|
-
|
|
144
|
-
# alone makes the serve app importable; nothing is pip-installed.
|
|
145
|
-
"runtime_env": {"working_dir": meta.bundle_url},
|
|
161
|
+
"runtime_env": runtime_env,
|
|
146
162
|
}
|
|
147
163
|
|
|
148
164
|
|
|
@@ -150,6 +166,7 @@ def _build_application_spec(
|
|
|
150
166
|
# `deploy_model` reads back.
|
|
151
167
|
_CLASS_IMPORT_PATH_TAG = "class_import_path"
|
|
152
168
|
_BUNDLE_URL_TAG = "serve_bundle_url"
|
|
169
|
+
_PIP_REQUIREMENTS_TAG = "serve_pip_requirements"
|
|
153
170
|
|
|
154
171
|
|
|
155
172
|
def metadata_to_tags(meta: BundleMetadata) -> dict[str, str]:
|
|
@@ -159,6 +176,7 @@ def metadata_to_tags(meta: BundleMetadata) -> dict[str, str]:
|
|
|
159
176
|
return {
|
|
160
177
|
_CLASS_IMPORT_PATH_TAG: meta.class_import_path,
|
|
161
178
|
_BUNDLE_URL_TAG: meta.bundle_url,
|
|
179
|
+
_PIP_REQUIREMENTS_TAG: json.dumps(meta.pip_requirements),
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
|
|
@@ -180,6 +198,8 @@ def _load_bundle_metadata(
|
|
|
180
198
|
return BundleMetadata(
|
|
181
199
|
bundle_url=tags[_BUNDLE_URL_TAG],
|
|
182
200
|
class_import_path=tags[_CLASS_IMPORT_PATH_TAG],
|
|
201
|
+
# Absent on models saved before dependencies were pip-installed.
|
|
202
|
+
pip_requirements=json.loads(tags.get(_PIP_REQUIREMENTS_TAG, "[]")),
|
|
183
203
|
)
|
|
184
204
|
except KeyError as exc:
|
|
185
205
|
raise ValueError(
|
|
@@ -88,9 +88,10 @@ print(f"Submitted: {job_id}")
|
|
|
88
88
|
A separate service — the **jobs control plane** — polls MLflow for pending job requests, matches them against the set of Ray submissions the cluster already has, and submits anything missing. It is also responsible for retrying failed jobs and honouring user-requested stops.
|
|
89
89
|
|
|
90
90
|
Each submission captures the code and dependencies the entry function needs automatically ([_bundle.py](https://github.com/robodatalab/cortexgrid/blob/main/cortexgrid/_bundle.py)):
|
|
91
|
-
- `bundle(entry)` traces the import graph from the function's source file, resolving each import the way the interpreter does (via `sys.path`)
|
|
92
|
-
-
|
|
93
|
-
-
|
|
91
|
+
- `bundle(entry)` traces the import graph from the function's source file, resolving each import the way the interpreter does (via `sys.path`). The standard library is excluded (it ships with the interpreter)
|
|
92
|
+
- Your own modules -- anything outside site-packages / dist-packages -- ship **as source**: they are staged at their import paths and tarred into the Ray `working_dir`
|
|
93
|
+
- Third-party packages are recorded as the installed distribution that owns the imported file, pinned to its installed version (`tqdm==4.67.3`), and Ray **pip-installs** them on the worker into a per-node cached virtualenv layered on the image (`runtime_env["pip"]`). An import into site-packages that no installed distribution owns fails `cortexgrid.remote` with `UnownedDependencyError`
|
|
94
|
+
- Distributions the worker image already has are not installed again: `worker_provides()` is the dependency closure of the packages baked into the ray image (torch and its CUDA stack, ray, mlflow, ...), and is subtracted from the pip list. See [k8s/docker/ray/Dockerfile](https://github.com/robodatalab/cortexgrid/blob/main/k8s/docker/ray/Dockerfile) and `_WORKER_BAKED` in `_bundle.py`, which must list the same packages
|
|
94
95
|
- Injects MLflow/S3 credentials so task code running on the DGX can reach all services
|
|
95
96
|
|
|
96
97
|
##### Retries
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cortexgrid"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.86"
|
|
4
4
|
description = "Connect your ML code to the RoboLab compute cluster — Ray, MLflow, and S3"
|
|
5
5
|
readme = "docs/cortexgrid/README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -9,6 +9,7 @@ requires-python = ">=3.11"
|
|
|
9
9
|
dependencies = [
|
|
10
10
|
"ray[default]>=2.9,<3",
|
|
11
11
|
"mlflow>=3.11,<4",
|
|
12
|
+
"packaging>=24",
|
|
12
13
|
"boto3>=1.34",
|
|
13
14
|
"setuptools>=82.0.1",
|
|
14
15
|
"tqdm>=4.60",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|