gpustack-runner 0.1.23.post5__py3-none-any.whl → 0.1.24.post1__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.
- gpustack_runner/__init__.py +10 -0
- gpustack_runner/__main__.py +2 -0
- gpustack_runner/__utils__.py +155 -0
- gpustack_runner/_version.py +2 -2
- gpustack_runner/_version_appendix.py +1 -1
- gpustack_runner/cmds/__init__.py +2 -0
- gpustack_runner/cmds/images.py +372 -99
- gpustack_runner/envs.py +19 -3
- gpustack_runner/runner.py +1 -7
- gpustack_runner/runner.py.json +22 -0
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.post1.dist-info}/METADATA +27 -39
- gpustack_runner-0.1.24.post1.dist-info/RECORD +17 -0
- gpustack_runner-0.1.23.post5.dist-info/RECORD +0 -16
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.post1.dist-info}/WHEEL +0 -0
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.post1.dist-info}/entry_points.txt +0 -0
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.post1.dist-info}/licenses/LICENSE +0 -0
gpustack_runner/__init__.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from .__utils__ import (
|
|
4
|
+
merge_image,
|
|
5
|
+
parse_image,
|
|
6
|
+
replace_image_with,
|
|
7
|
+
split_image,
|
|
8
|
+
)
|
|
3
9
|
from ._version import commit_id, version, version_tuple
|
|
4
10
|
from .runner import (
|
|
5
11
|
BackendRunners,
|
|
@@ -21,7 +27,11 @@ __all__ = [
|
|
|
21
27
|
"list_backend_runners",
|
|
22
28
|
"list_runners",
|
|
23
29
|
"list_service_runners",
|
|
30
|
+
"merge_image",
|
|
31
|
+
"parse_image",
|
|
32
|
+
"replace_image_with",
|
|
24
33
|
"set_re_docker_image",
|
|
34
|
+
"split_image",
|
|
25
35
|
"version",
|
|
26
36
|
"version_tuple",
|
|
27
37
|
]
|
gpustack_runner/__main__.py
CHANGED
|
@@ -11,6 +11,7 @@ from .cmds import (
|
|
|
11
11
|
CompareImagesSubCommand,
|
|
12
12
|
CopyImagesSubCommand,
|
|
13
13
|
ListImagesSubCommand,
|
|
14
|
+
LoadImagesSubCommand,
|
|
14
15
|
SaveImagesSubCommand,
|
|
15
16
|
)
|
|
16
17
|
|
|
@@ -35,6 +36,7 @@ def main():
|
|
|
35
36
|
SaveImagesSubCommand.register(subcommand_parser)
|
|
36
37
|
CopyImagesSubCommand.register(subcommand_parser)
|
|
37
38
|
CompareImagesSubCommand.register(subcommand_parser)
|
|
39
|
+
LoadImagesSubCommand.register(subcommand_parser)
|
|
38
40
|
|
|
39
41
|
# Autocomplete
|
|
40
42
|
argcomplete.autocomplete(parser)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
_BLANK_TAG = "latest"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def split_image(image: str, fill_blank_tag: bool = False) -> tuple:
|
|
7
|
+
"""
|
|
8
|
+
Split the Docker completed image string into its image_name([registry/][namespace/]repository) and image_tag.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
image:
|
|
12
|
+
The Docker completed image string to split.
|
|
13
|
+
fill_blank_tag:
|
|
14
|
+
If True, fill the blank tag with `latest`.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A tuple of (image_name, image_tag).
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
parts = image.rsplit("@", maxsplit=1)
|
|
21
|
+
if len(parts) == 2:
|
|
22
|
+
return tuple(parts)
|
|
23
|
+
parts = image.rsplit(":", maxsplit=1)
|
|
24
|
+
if len(parts) == 2 and "/" not in parts[1]:
|
|
25
|
+
if fill_blank_tag:
|
|
26
|
+
parts[1] = parts[1] or _BLANK_TAG
|
|
27
|
+
return tuple(parts)
|
|
28
|
+
return image, _BLANK_TAG if fill_blank_tag else None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def merge_image(image_name: str, image_tag: str | None = None) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Merge the Docker image and image_tag into a single string.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
image_name:
|
|
37
|
+
The Docker image name, in form of [registry/][namespace/]repository.
|
|
38
|
+
image_tag:
|
|
39
|
+
The Docker image tag.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The completed Docker image string.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
if not image_tag:
|
|
46
|
+
return image_name
|
|
47
|
+
if image_tag.startswith("sha256:"):
|
|
48
|
+
return f"{image_name}@{image_tag}"
|
|
49
|
+
return f"{image_name}:{image_tag}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_image(
|
|
53
|
+
image: str,
|
|
54
|
+
fill_blank_tag: bool = False,
|
|
55
|
+
) -> tuple[str | None, str | None, str, str | None] | None:
|
|
56
|
+
"""
|
|
57
|
+
Parse the Docker image string into its components:
|
|
58
|
+
registry, namespace, repository, and tag.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
image:
|
|
62
|
+
The Docker image string to parse.
|
|
63
|
+
fill_blank_tag:
|
|
64
|
+
If True, fill the blank tag with `latest`.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A tuple of (registry, namespace, repository, tag).
|
|
68
|
+
Registry, namespace, and tag can be None if not present.
|
|
69
|
+
If the image string is invalid, return None.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
image_reg, image_ns, image_repo, image_tag = (
|
|
73
|
+
None,
|
|
74
|
+
None,
|
|
75
|
+
None,
|
|
76
|
+
None,
|
|
77
|
+
)
|
|
78
|
+
image_rest = image.strip()
|
|
79
|
+
|
|
80
|
+
# Get tag.
|
|
81
|
+
image_rest, image_tag = split_image(image_rest, fill_blank_tag=fill_blank_tag)
|
|
82
|
+
if not image_rest:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Get repository.
|
|
86
|
+
parts = image_rest.rsplit("/", maxsplit=1)
|
|
87
|
+
if len(parts) == 2:
|
|
88
|
+
image_rest, image_repo = parts
|
|
89
|
+
else:
|
|
90
|
+
image_rest, image_repo = None, image_rest
|
|
91
|
+
|
|
92
|
+
# Get namespace.
|
|
93
|
+
if image_rest:
|
|
94
|
+
parts = image_rest.rsplit("/", maxsplit=1)
|
|
95
|
+
if len(parts) == 2:
|
|
96
|
+
image_reg, image_ns = parts
|
|
97
|
+
else:
|
|
98
|
+
image_reg, image_ns = None, image_rest
|
|
99
|
+
|
|
100
|
+
return image_reg, image_ns, image_repo, image_tag
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def replace_image_with(
|
|
104
|
+
image: str,
|
|
105
|
+
registry: str | None = None,
|
|
106
|
+
namespace: str | None = None,
|
|
107
|
+
repository: str | None = None,
|
|
108
|
+
) -> str:
|
|
109
|
+
"""
|
|
110
|
+
Replace the registry, namespace, and repository of a Docker image string.
|
|
111
|
+
|
|
112
|
+
The given image string is parsed into its components (registry, namespace, repository, tag),
|
|
113
|
+
and the specified components are replaced with the provided values.
|
|
114
|
+
|
|
115
|
+
The format of a Docker image string is:
|
|
116
|
+
[registry/][namespace/]repository[:tag|@digest]
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
image:
|
|
120
|
+
The original Docker image string.
|
|
121
|
+
registry:
|
|
122
|
+
The new registry to use. If None, keep the original registry.
|
|
123
|
+
namespace:
|
|
124
|
+
The new namespace to use. If None, keep the original namespace.
|
|
125
|
+
repository:
|
|
126
|
+
The new repository to use. If None, keep the original repository.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The modified Docker image string.
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
if not image or (not registry and not namespace and not repository):
|
|
133
|
+
return image
|
|
134
|
+
|
|
135
|
+
registry = registry.strip() if registry else None
|
|
136
|
+
namespace = namespace.strip() if namespace else None
|
|
137
|
+
repository = repository.strip() if repository else None
|
|
138
|
+
|
|
139
|
+
image_reg, image_ns, image_repo, image_tag = parse_image(image)
|
|
140
|
+
|
|
141
|
+
registry = registry or image_reg
|
|
142
|
+
namespace = namespace or image_ns
|
|
143
|
+
repository = repository or image_repo
|
|
144
|
+
|
|
145
|
+
image_name = ""
|
|
146
|
+
if registry:
|
|
147
|
+
image_name += f"{registry}/"
|
|
148
|
+
if namespace:
|
|
149
|
+
image_name += f"{namespace}/"
|
|
150
|
+
elif registry:
|
|
151
|
+
image_name += "library/"
|
|
152
|
+
image_name += repository
|
|
153
|
+
|
|
154
|
+
image = merge_image(image_name, image_tag)
|
|
155
|
+
return image
|
gpustack_runner/_version.py
CHANGED
|
@@ -27,8 +27,8 @@ version_tuple: VERSION_TUPLE
|
|
|
27
27
|
__commit_id__: COMMIT_ID
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
|
|
30
|
-
__version__ = version = '0.1.
|
|
31
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
30
|
+
__version__ = version = '0.1.24.post1'
|
|
31
|
+
__version_tuple__ = version_tuple = (0, 1, 24, 'post1')
|
|
32
32
|
try:
|
|
33
33
|
from ._version_appendix import git_commit
|
|
34
34
|
__commit_id__ = commit_id = git_commit
|
|
@@ -1 +1 @@
|
|
|
1
|
-
git_commit = "
|
|
1
|
+
git_commit = "ed41ee9"
|
gpustack_runner/cmds/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ from .images import (
|
|
|
4
4
|
CompareImagesSubCommand,
|
|
5
5
|
CopyImagesSubCommand,
|
|
6
6
|
ListImagesSubCommand,
|
|
7
|
+
LoadImagesSubCommand,
|
|
7
8
|
PlatformedImage,
|
|
8
9
|
SaveImagesSubCommand,
|
|
9
10
|
append_images,
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
"CompareImagesSubCommand",
|
|
15
16
|
"CopyImagesSubCommand",
|
|
16
17
|
"ListImagesSubCommand",
|
|
18
|
+
"LoadImagesSubCommand",
|
|
17
19
|
"PlatformedImage",
|
|
18
20
|
"SaveImagesSubCommand",
|
|
19
21
|
"append_images",
|
gpustack_runner/cmds/images.py
CHANGED
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
import tempfile
|
|
10
10
|
import time
|
|
11
11
|
from argparse import OPTIONAL
|
|
12
|
-
from concurrent.futures import CancelledError, ThreadPoolExecutor, as_completed
|
|
12
|
+
from concurrent.futures import CancelledError, Future, ThreadPoolExecutor, as_completed
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
@@ -17,7 +17,14 @@ from typing import TYPE_CHECKING
|
|
|
17
17
|
import requests
|
|
18
18
|
from dataclasses_json import dataclass_json
|
|
19
19
|
|
|
20
|
-
from gpustack_runner import
|
|
20
|
+
from gpustack_runner import (
|
|
21
|
+
BackendRunners,
|
|
22
|
+
envs,
|
|
23
|
+
list_backend_runners,
|
|
24
|
+
merge_image,
|
|
25
|
+
replace_image_with,
|
|
26
|
+
split_image,
|
|
27
|
+
)
|
|
21
28
|
|
|
22
29
|
from .__types__ import SubCommand
|
|
23
30
|
|
|
@@ -29,6 +36,7 @@ _AVAILABLE_BACKENDS = [
|
|
|
29
36
|
"corex",
|
|
30
37
|
"cuda",
|
|
31
38
|
"dtk",
|
|
39
|
+
"hggc",
|
|
32
40
|
"maca",
|
|
33
41
|
"musa",
|
|
34
42
|
"neuware",
|
|
@@ -46,10 +54,6 @@ _AVAILABLE_PLATFORMS = [
|
|
|
46
54
|
]
|
|
47
55
|
|
|
48
56
|
|
|
49
|
-
# Disable overriding default namespace at images operations.
|
|
50
|
-
os.environ["GPUSTACK_RUNNER_DEFAULT_NAMESPACE"] = "gpustack"
|
|
51
|
-
|
|
52
|
-
|
|
53
57
|
class ListImagesSubCommand(SubCommand):
|
|
54
58
|
"""
|
|
55
59
|
Command to list images.
|
|
@@ -187,7 +191,7 @@ class ListImagesSubCommand(SubCommand):
|
|
|
187
191
|
|
|
188
192
|
class SaveImagesSubCommand(SubCommand):
|
|
189
193
|
"""
|
|
190
|
-
Command to save images to local that matched Docker
|
|
194
|
+
Command to save images to local that matched Docker/OCI archive.
|
|
191
195
|
"""
|
|
192
196
|
|
|
193
197
|
backend: str
|
|
@@ -206,13 +210,14 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
206
210
|
source_namespace: str
|
|
207
211
|
source_username: str
|
|
208
212
|
source_password: str
|
|
213
|
+
archive_format: str
|
|
209
214
|
output: Path
|
|
210
215
|
|
|
211
216
|
@staticmethod
|
|
212
217
|
def register(parser: _SubParsersAction):
|
|
213
218
|
save_parser = parser.add_parser(
|
|
214
219
|
"save-images",
|
|
215
|
-
help="Save images as Docker Archive to local path, "
|
|
220
|
+
help="Save images as OCI/Docker Archive to local path, "
|
|
216
221
|
"powered by https://github.com/containers/skopeo",
|
|
217
222
|
)
|
|
218
223
|
|
|
@@ -305,10 +310,7 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
305
310
|
"--source-namespace",
|
|
306
311
|
"--src-namespace",
|
|
307
312
|
type=str,
|
|
308
|
-
help="
|
|
309
|
-
"if the namespace has multiple levels, "
|
|
310
|
-
"please specify the parent levels to --source, "
|
|
311
|
-
"e.g --source my.registry.com/a/b --source-namespace c",
|
|
313
|
+
help="Namespace in the source registry",
|
|
312
314
|
)
|
|
313
315
|
|
|
314
316
|
save_parser.add_argument(
|
|
@@ -322,7 +324,16 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
322
324
|
"--source-password",
|
|
323
325
|
"--src-passwd",
|
|
324
326
|
type=str,
|
|
325
|
-
help="Password/Token for source registry authentication
|
|
327
|
+
help="Password/Token for source registry authentication "
|
|
328
|
+
"(env: SOURCE_PASSWORD)",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
save_parser.add_argument(
|
|
332
|
+
"--archive-format",
|
|
333
|
+
type=str,
|
|
334
|
+
choices=["oci", "docker"],
|
|
335
|
+
default="oci",
|
|
336
|
+
help="Archive format to save (default: oci)",
|
|
326
337
|
)
|
|
327
338
|
|
|
328
339
|
save_parser.add_argument(
|
|
@@ -352,16 +363,21 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
352
363
|
self.source_namespace = args.source_namespace
|
|
353
364
|
self.source_username = args.source_username or os.getenv("SOURCE_USERNAME")
|
|
354
365
|
self.source_password = args.source_password or os.getenv("SOURCE_PASSWORD")
|
|
366
|
+
self.archive_format = args.archive_format
|
|
355
367
|
self.output = Path(args.output or Path.cwd())
|
|
356
368
|
|
|
357
369
|
try:
|
|
358
370
|
if not self.output.exists():
|
|
359
371
|
self.output.mkdir(parents=True, exist_ok=True)
|
|
360
372
|
except OSError as e:
|
|
361
|
-
msg =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
) from e
|
|
373
|
+
msg = (
|
|
374
|
+
f"Failed to prepare output directory '{self.output}' for saving images"
|
|
375
|
+
)
|
|
376
|
+
raise RuntimeError(msg) from e
|
|
377
|
+
|
|
378
|
+
if not self.output.is_dir():
|
|
379
|
+
msg = f"Output path '{self.output}' is not a directory"
|
|
380
|
+
raise RuntimeError(msg)
|
|
365
381
|
|
|
366
382
|
def run(self):
|
|
367
383
|
images = list_images(
|
|
@@ -389,11 +405,23 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
389
405
|
|
|
390
406
|
print("\033[2J\033[H", end="")
|
|
391
407
|
|
|
392
|
-
|
|
393
|
-
print(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
408
|
+
saving_tasks: list[tuple[str, str, Path, Path]] = []
|
|
409
|
+
print(
|
|
410
|
+
f"Output: {self.output} | Archive: {self.archive_format} | Platform: {self.platform}",
|
|
411
|
+
)
|
|
412
|
+
print(f"Saving Images ({len(images)}):")
|
|
413
|
+
for task_idx, img in enumerate(images):
|
|
414
|
+
task_name = f"task-{task_idx:0>2d}"
|
|
415
|
+
src_img = f"{self.source}/{img.name}"
|
|
416
|
+
img_name, img_tag = split_image(src_img, fill_blank_tag=True)
|
|
417
|
+
dst_file = self.output / img_name / f"{img_tag}.tar"
|
|
418
|
+
dst_file_relative = dst_file.relative_to(self.output)
|
|
419
|
+
saving_tasks.append(
|
|
420
|
+
(task_name, src_img, dst_file, dst_file_relative),
|
|
421
|
+
)
|
|
422
|
+
print(
|
|
423
|
+
f" - [{task_name}]: {src_img} -> {dst_file_relative}",
|
|
424
|
+
)
|
|
397
425
|
print()
|
|
398
426
|
|
|
399
427
|
for i in range(5, 0, -1):
|
|
@@ -410,37 +438,35 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
410
438
|
max_workers=self.max_workers,
|
|
411
439
|
thread_name_prefix="gpustack-saving-image",
|
|
412
440
|
) as executor:
|
|
413
|
-
futures = {}
|
|
414
|
-
failures = []
|
|
441
|
+
futures: dict[Future, tuple[str, str, Path, Path]] = {}
|
|
442
|
+
failures: list[tuple[str, str, Path, str]] = []
|
|
415
443
|
|
|
416
444
|
def check_result(f):
|
|
417
|
-
|
|
445
|
+
_task_name, _src_img, _dst_file, _dst_file_relative = futures[f]
|
|
418
446
|
try:
|
|
419
447
|
result = f.result()
|
|
420
448
|
if result.returncode == 0:
|
|
421
|
-
print(f"✅
|
|
449
|
+
print(f"✅ Saved {_src_img} -> {_dst_file_relative}")
|
|
422
450
|
return
|
|
423
|
-
|
|
451
|
+
_save_err = result.stderr
|
|
424
452
|
except subprocess.CalledProcessError as cpe:
|
|
425
|
-
|
|
453
|
+
_save_err = cpe.stderr if cpe.stderr else str(cpe)
|
|
426
454
|
except CancelledError:
|
|
427
455
|
return
|
|
428
456
|
except Exception as e:
|
|
429
|
-
|
|
430
|
-
print(f"❌ Error
|
|
431
|
-
|
|
432
|
-
|
|
457
|
+
_save_err = str(e)
|
|
458
|
+
print(f"❌ Error saving {_src_img} -> {_dst_file_relative}")
|
|
459
|
+
_dst_file.unlink(missing_ok=True)
|
|
460
|
+
failures.append((_task_name, _src_img, _dst_file_relative, _save_err))
|
|
433
461
|
|
|
434
462
|
override_os, override_arch = self.platform.split("/", maxsplit=1)
|
|
435
463
|
|
|
436
464
|
# Submit tasks
|
|
437
|
-
for
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
)
|
|
441
|
-
if output_path.exists():
|
|
442
|
-
print(f"Image {img.name} already exists, skipping download.")
|
|
465
|
+
for task_name, src_img, dst_file, dst_file_relative in saving_tasks:
|
|
466
|
+
if dst_file.exists():
|
|
467
|
+
print(f"{dst_file.name} already exists, skipping save {src_img}.")
|
|
443
468
|
continue
|
|
469
|
+
dst_file.parent.mkdir(parents=True, exist_ok=True, mode=0o744)
|
|
444
470
|
|
|
445
471
|
command = [
|
|
446
472
|
"skopeo",
|
|
@@ -462,19 +488,19 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
462
488
|
)
|
|
463
489
|
command.extend(
|
|
464
490
|
[
|
|
465
|
-
f"docker://{
|
|
466
|
-
f"
|
|
491
|
+
f"docker://{src_img}",
|
|
492
|
+
f"{self.archive_format}-archive:{dst_file}:{src_img}",
|
|
467
493
|
],
|
|
468
494
|
)
|
|
469
495
|
|
|
470
496
|
future = executor.submit(
|
|
471
497
|
_execute_command,
|
|
472
|
-
title=
|
|
473
|
-
description=f"⏳
|
|
498
|
+
title=task_name,
|
|
499
|
+
description=f"⏳ Saving {src_img} -> {dst_file_relative}...",
|
|
474
500
|
command=command,
|
|
475
501
|
)
|
|
476
502
|
future.add_done_callback(check_result)
|
|
477
|
-
futures[future] = (
|
|
503
|
+
futures[future] = (task_name, src_img, dst_file, dst_file_relative)
|
|
478
504
|
|
|
479
505
|
# Wait
|
|
480
506
|
try:
|
|
@@ -488,11 +514,11 @@ class SaveImagesSubCommand(SubCommand):
|
|
|
488
514
|
# Review
|
|
489
515
|
print()
|
|
490
516
|
if failures:
|
|
491
|
-
print(f"⚠️ Error
|
|
492
|
-
for
|
|
493
|
-
print(f" - {
|
|
494
|
-
if
|
|
495
|
-
for line in
|
|
517
|
+
print(f"⚠️ Error saving {len(failures)} images:")
|
|
518
|
+
for task_name, src_img, dst_file_relative, save_err in failures:
|
|
519
|
+
print(f" - [{task_name}]: {src_img} -> {dst_file_relative}")
|
|
520
|
+
if save_err:
|
|
521
|
+
for line in save_err.splitlines():
|
|
496
522
|
print(f" {line}")
|
|
497
523
|
else:
|
|
498
524
|
print(" (no error message)")
|
|
@@ -622,10 +648,7 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
622
648
|
"--source-namespace",
|
|
623
649
|
"--src-namespace",
|
|
624
650
|
type=str,
|
|
625
|
-
help="
|
|
626
|
-
"if the namespace has multiple levels, "
|
|
627
|
-
"please specify the parent levels to --source, "
|
|
628
|
-
"e.g --source my.registry.com/a/b --source-namespace c",
|
|
651
|
+
help="Namespace in the source registry",
|
|
629
652
|
)
|
|
630
653
|
|
|
631
654
|
copy_parser.add_argument(
|
|
@@ -639,39 +662,40 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
639
662
|
"--source-password",
|
|
640
663
|
"--src-passwd",
|
|
641
664
|
type=str,
|
|
642
|
-
help="Password/Token for source registry authentication
|
|
665
|
+
help="Password/Token for source registry authentication "
|
|
666
|
+
"(env: SOURCE_PASSWORD)",
|
|
643
667
|
)
|
|
644
668
|
|
|
645
669
|
copy_parser.add_argument(
|
|
646
670
|
"--destination",
|
|
647
671
|
"--dest",
|
|
648
672
|
type=str,
|
|
649
|
-
|
|
650
|
-
|
|
673
|
+
help="Destination registry (default: docker.io) "
|
|
674
|
+
"(env: GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY, GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY, GPUSTACK_SYSTEM_DEFAULT_CONTAINER_REGISTRY)",
|
|
651
675
|
)
|
|
652
676
|
|
|
653
677
|
copy_parser.add_argument(
|
|
654
678
|
"--destination-namespace",
|
|
655
679
|
"--dest-namespace",
|
|
656
680
|
type=str,
|
|
657
|
-
help="
|
|
658
|
-
"
|
|
659
|
-
"please specify the parent levels to --destination, "
|
|
660
|
-
"e.g --destination my.registry.com/a/b --destination-namespace c",
|
|
681
|
+
help="Namespace in the destination registry "
|
|
682
|
+
"(env: GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE, GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_NAMESPACE)",
|
|
661
683
|
)
|
|
662
684
|
|
|
663
685
|
copy_parser.add_argument(
|
|
664
686
|
"--destination-username",
|
|
665
687
|
"--dest-user",
|
|
666
688
|
type=str,
|
|
667
|
-
help="Username for destination registry authentication
|
|
689
|
+
help="Username for destination registry authentication "
|
|
690
|
+
"(env: DESTINATION_USERNAME)",
|
|
668
691
|
)
|
|
669
692
|
|
|
670
693
|
copy_parser.add_argument(
|
|
671
694
|
"--destination-password",
|
|
672
695
|
"--dest-passwd",
|
|
673
696
|
type=str,
|
|
674
|
-
help="Password/Token for destination registry authentication
|
|
697
|
+
help="Password/Token for destination registry authentication "
|
|
698
|
+
"(env: DESTINATION_PASSWORD)",
|
|
675
699
|
)
|
|
676
700
|
|
|
677
701
|
copy_parser.set_defaults(func=CopyImagesSubCommand)
|
|
@@ -695,8 +719,15 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
695
719
|
self.source_namespace = args.source_namespace
|
|
696
720
|
self.source_username = args.source_username or os.getenv("SOURCE_USERNAME")
|
|
697
721
|
self.source_password = args.source_password or os.getenv("SOURCE_PASSWORD")
|
|
698
|
-
self.destination =
|
|
699
|
-
|
|
722
|
+
self.destination = (
|
|
723
|
+
args.destination
|
|
724
|
+
or envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY
|
|
725
|
+
or "docker.io"
|
|
726
|
+
)
|
|
727
|
+
self.destination_namespace = (
|
|
728
|
+
args.destination_namespace
|
|
729
|
+
or envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE
|
|
730
|
+
)
|
|
700
731
|
self.destination_username = args.destination_username or os.getenv(
|
|
701
732
|
"DESTINATION_USERNAME",
|
|
702
733
|
)
|
|
@@ -730,11 +761,20 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
730
761
|
|
|
731
762
|
print("\033[2J\033[H", end="")
|
|
732
763
|
|
|
733
|
-
|
|
734
|
-
print(f"
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
764
|
+
copying_tasks: list[tuple[str, str, str]] = []
|
|
765
|
+
print(f"Copying Images ({len(images)}):")
|
|
766
|
+
for task_idx, img in enumerate(images):
|
|
767
|
+
task_name = f"task-{task_idx:0>2d}"
|
|
768
|
+
src_img = f"{self.source}/{img.name}"
|
|
769
|
+
dst_img_name = img.name
|
|
770
|
+
if self.destination_namespace:
|
|
771
|
+
_, suffix = img.name.split("/", maxsplit=1)
|
|
772
|
+
dst_img_name = f"{self.destination_namespace}/{suffix}"
|
|
773
|
+
dst_img = f"{self.destination}/{dst_img_name}"
|
|
774
|
+
copying_tasks.append(
|
|
775
|
+
(task_name, src_img, dst_img),
|
|
776
|
+
)
|
|
777
|
+
print(f" - [{task_name}]: {src_img} -> {dst_img}")
|
|
738
778
|
print()
|
|
739
779
|
|
|
740
780
|
for i in range(5, 0, -1):
|
|
@@ -751,32 +791,32 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
751
791
|
max_workers=self.max_workers,
|
|
752
792
|
thread_name_prefix="gpustack-copying-image",
|
|
753
793
|
) as executor:
|
|
754
|
-
futures = {}
|
|
755
|
-
failures = []
|
|
794
|
+
futures: dict[Future, tuple[str, str, str]] = {}
|
|
795
|
+
failures: list[tuple[str, str, str, str]] = []
|
|
756
796
|
|
|
757
797
|
def check_result(f):
|
|
758
|
-
|
|
798
|
+
_task_name, _src_img, _dst_img = futures[f]
|
|
759
799
|
try:
|
|
760
800
|
result = f.result()
|
|
761
801
|
if result.returncode == 0:
|
|
762
|
-
print(f"✅
|
|
802
|
+
print(f"✅ Copied {_src_img} -> {_dst_img}")
|
|
763
803
|
return
|
|
764
|
-
|
|
804
|
+
_copy_err = result.stderr
|
|
765
805
|
except subprocess.CalledProcessError as cpe:
|
|
766
|
-
|
|
806
|
+
_copy_err = cpe.stderr if cpe.stderr else str(cpe)
|
|
767
807
|
except CancelledError:
|
|
768
808
|
return
|
|
769
809
|
except Exception as e:
|
|
770
|
-
|
|
771
|
-
print(f"❌ Error
|
|
772
|
-
failures.append((
|
|
810
|
+
_copy_err = str(e)
|
|
811
|
+
print(f"❌ Error copying {_src_img} -> {_dst_img}")
|
|
812
|
+
failures.append((_task_name, _src_img, _dst_img, _copy_err))
|
|
773
813
|
|
|
774
814
|
override_os, override_arch = None, None
|
|
775
815
|
if self.platform:
|
|
776
816
|
override_os, override_arch = self.platform.split("/", maxsplit=1)
|
|
777
817
|
|
|
778
818
|
# Submit tasks
|
|
779
|
-
for
|
|
819
|
+
for task_name, src_img, dst_img in copying_tasks:
|
|
780
820
|
command = [
|
|
781
821
|
"skopeo",
|
|
782
822
|
"copy",
|
|
@@ -810,29 +850,265 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
810
850
|
f"{self.destination_username}:{self.destination_password}",
|
|
811
851
|
],
|
|
812
852
|
)
|
|
813
|
-
dest_img_name = img.name
|
|
814
|
-
if self.destination_namespace:
|
|
815
|
-
_, suffix = img.name.split("/", maxsplit=1)
|
|
816
|
-
dest_img_name = f"{self.destination_namespace}/{suffix}"
|
|
817
853
|
command.extend(
|
|
818
854
|
[
|
|
819
|
-
f"docker://{
|
|
820
|
-
f"docker://{
|
|
855
|
+
f"docker://{src_img}",
|
|
856
|
+
f"docker://{dst_img}",
|
|
821
857
|
],
|
|
822
858
|
)
|
|
823
859
|
|
|
824
860
|
future = executor.submit(
|
|
825
861
|
_execute_command,
|
|
826
|
-
title=
|
|
827
|
-
description=
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
862
|
+
title=task_name,
|
|
863
|
+
description=f"⏳ Copying {src_img} -> {dst_img}...",
|
|
864
|
+
command=command,
|
|
865
|
+
)
|
|
866
|
+
future.add_done_callback(check_result)
|
|
867
|
+
futures[future] = (task_name, src_img, dst_img)
|
|
868
|
+
|
|
869
|
+
# Wait
|
|
870
|
+
try:
|
|
871
|
+
for _ in as_completed(futures):
|
|
872
|
+
pass
|
|
873
|
+
except Exception:
|
|
874
|
+
for future in futures:
|
|
875
|
+
future.cancel()
|
|
876
|
+
raise
|
|
877
|
+
|
|
878
|
+
# Review
|
|
879
|
+
print()
|
|
880
|
+
if failures:
|
|
881
|
+
print(f"⚠️ Error copying {len(failures)} images:")
|
|
882
|
+
for task_name, src_img, dst_img, copy_err in failures:
|
|
883
|
+
print(f" - [{task_name}]: {src_img} -> {dst_img}:")
|
|
884
|
+
if copy_err:
|
|
885
|
+
for line in copy_err.splitlines():
|
|
886
|
+
print(f" {line}")
|
|
887
|
+
else:
|
|
888
|
+
print(" (no error message)")
|
|
889
|
+
sys.exit(1)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
class LoadImagesSubCommand(SubCommand):
|
|
893
|
+
"""
|
|
894
|
+
Command to load images to local container image storage that matched Docker/OCI archive.
|
|
895
|
+
|
|
896
|
+
"""
|
|
897
|
+
|
|
898
|
+
repository: str
|
|
899
|
+
platform: str
|
|
900
|
+
max_workers: int
|
|
901
|
+
max_retries: int
|
|
902
|
+
destination: str
|
|
903
|
+
destination_namespace: str
|
|
904
|
+
archive_format: str
|
|
905
|
+
storage: str
|
|
906
|
+
input: Path
|
|
907
|
+
archives: list[Path]
|
|
908
|
+
|
|
909
|
+
@staticmethod
|
|
910
|
+
def register(parser: _SubParsersAction):
|
|
911
|
+
load_parser = parser.add_parser(
|
|
912
|
+
"load-images",
|
|
913
|
+
help="Load images from OCI/Docker Archive to local container image storage, "
|
|
914
|
+
"powered by https://github.com/containers/skopeo",
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
load_parser.add_argument(
|
|
918
|
+
"--repository",
|
|
919
|
+
type=str,
|
|
920
|
+
help="Filter images by repository name",
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
load_parser.add_argument(
|
|
924
|
+
"--platform",
|
|
925
|
+
type=str,
|
|
926
|
+
help="Filter images by platform (default: current platform)",
|
|
927
|
+
choices=_AVAILABLE_PLATFORMS,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
load_parser.add_argument(
|
|
931
|
+
"--max-workers",
|
|
932
|
+
type=int,
|
|
933
|
+
default=1,
|
|
934
|
+
help="Maximum number of worker threads to use for loading images concurrently (default: 1)",
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
load_parser.add_argument(
|
|
938
|
+
"--max-retries",
|
|
939
|
+
type=int,
|
|
940
|
+
default=1,
|
|
941
|
+
help="Maximum number of retries for loading an image (default: 1)",
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
load_parser.add_argument(
|
|
945
|
+
"--destination",
|
|
946
|
+
"--dest",
|
|
947
|
+
type=str,
|
|
948
|
+
help="Override destination registry "
|
|
949
|
+
"(env: GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY, GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY, GPUSTACK_SYSTEM_DEFAULT_CONTAINER_REGISTRY)",
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
load_parser.add_argument(
|
|
953
|
+
"--destination-namespace",
|
|
954
|
+
"--dest-namespace",
|
|
955
|
+
type=str,
|
|
956
|
+
help="Override namespace in the destination registry "
|
|
957
|
+
"(env: GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE, GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_NAMESPACE)",
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
load_parser.add_argument(
|
|
961
|
+
"--archive-format",
|
|
962
|
+
type=str,
|
|
963
|
+
choices=["oci", "docker"],
|
|
964
|
+
default="oci",
|
|
965
|
+
help="Archive format to load (default: oci)",
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
load_parser.add_argument(
|
|
969
|
+
"--storage",
|
|
970
|
+
type=str,
|
|
971
|
+
choices=["docker", "podman"],
|
|
972
|
+
default="docker",
|
|
973
|
+
help="Container image storage to load images into (default: docker)",
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
load_parser.add_argument(
|
|
977
|
+
"input",
|
|
978
|
+
nargs=OPTIONAL,
|
|
979
|
+
help="Input directory to load images (default: current working directory)",
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
load_parser.set_defaults(func=LoadImagesSubCommand)
|
|
983
|
+
|
|
984
|
+
def __init__(self, args: Namespace):
|
|
985
|
+
_ensure_required_tools()
|
|
986
|
+
|
|
987
|
+
self.repository = args.repository
|
|
988
|
+
self.platform = args.platform or _get_current_platform()
|
|
989
|
+
self.max_workers = args.max_workers
|
|
990
|
+
self.max_retries = args.max_retries
|
|
991
|
+
self.destination = (
|
|
992
|
+
args.destination or envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY
|
|
993
|
+
)
|
|
994
|
+
self.destination_namespace = (
|
|
995
|
+
args.destination_namespace
|
|
996
|
+
or envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE
|
|
997
|
+
)
|
|
998
|
+
self.archive_format = args.archive_format
|
|
999
|
+
self.storage = args.storage
|
|
1000
|
+
self.input = Path(args.input)
|
|
1001
|
+
self.archives = []
|
|
1002
|
+
|
|
1003
|
+
if not self.input.exists() or not self.input.is_dir():
|
|
1004
|
+
msg = f"Input path '{self.input}' is not a valid directory"
|
|
1005
|
+
raise RuntimeError(msg)
|
|
1006
|
+
|
|
1007
|
+
if self.input.exists() and self.input.is_dir():
|
|
1008
|
+
for archive_path in self.input.rglob("*.tar"):
|
|
1009
|
+
if not archive_path.is_file():
|
|
1010
|
+
continue
|
|
1011
|
+
if self.repository and archive_path.parent.name != self.repository:
|
|
1012
|
+
continue
|
|
1013
|
+
self.archives.append(archive_path)
|
|
1014
|
+
|
|
1015
|
+
def run(self):
|
|
1016
|
+
if not self.archives:
|
|
1017
|
+
print("No matching image archives found.")
|
|
1018
|
+
return
|
|
1019
|
+
|
|
1020
|
+
print("\033[2J\033[H", end="")
|
|
1021
|
+
|
|
1022
|
+
loading_tasks: list[tuple[str, Path, Path, str]] = []
|
|
1023
|
+
print(
|
|
1024
|
+
f"Input: {self.input} | Archive: {self.archive_format} | Storage: {self.storage}",
|
|
1025
|
+
)
|
|
1026
|
+
print(f"Loading Images ({len(self.archives)}):")
|
|
1027
|
+
for task_idx, src_file in enumerate(self.archives):
|
|
1028
|
+
task_name = f"task-{task_idx:0>2d}"
|
|
1029
|
+
src_file_relative = src_file.relative_to(self.input)
|
|
1030
|
+
dst_img_repo = str(src_file.parent.relative_to(self.input))
|
|
1031
|
+
dst_img_tag = src_file.name.removesuffix(src_file.suffix)
|
|
1032
|
+
dst_img = replace_image_with(
|
|
1033
|
+
merge_image(dst_img_repo, dst_img_tag),
|
|
1034
|
+
registry=self.destination,
|
|
1035
|
+
namespace=self.destination_namespace,
|
|
1036
|
+
)
|
|
1037
|
+
loading_tasks.append(
|
|
1038
|
+
(task_name, src_file, src_file_relative, dst_img),
|
|
1039
|
+
)
|
|
1040
|
+
print(f" - [{task_name}]: {src_file_relative} -> {dst_img}")
|
|
1041
|
+
print()
|
|
1042
|
+
|
|
1043
|
+
for i in range(5, 0, -1):
|
|
1044
|
+
if sys.stdout.isatty():
|
|
1045
|
+
print(f"\rStarting in {i} seconds... ", end="", flush=True)
|
|
1046
|
+
else:
|
|
1047
|
+
print(f"Starting in {i} seconds...")
|
|
1048
|
+
time.sleep(1)
|
|
1049
|
+
if sys.stdout.isatty():
|
|
1050
|
+
print("\rStarting now... ", end="", flush=True)
|
|
1051
|
+
print()
|
|
1052
|
+
|
|
1053
|
+
with ThreadPoolExecutor(
|
|
1054
|
+
max_workers=self.max_workers,
|
|
1055
|
+
thread_name_prefix="gpustack-loading-image",
|
|
1056
|
+
) as executor:
|
|
1057
|
+
futures: dict[Future, tuple[str, Path, str]] = {}
|
|
1058
|
+
failures: list[tuple[str, Path, str, str]] = []
|
|
1059
|
+
|
|
1060
|
+
def check_result(f):
|
|
1061
|
+
_task_name, _src_file_relative, _dst_img = futures[f]
|
|
1062
|
+
try:
|
|
1063
|
+
result = f.result()
|
|
1064
|
+
if result.returncode == 0:
|
|
1065
|
+
print(
|
|
1066
|
+
f"✅ Loaded {_src_file_relative} -> {_dst_img}",
|
|
1067
|
+
)
|
|
1068
|
+
return
|
|
1069
|
+
_load_err = result.stderr
|
|
1070
|
+
except subprocess.CalledProcessError as cpe:
|
|
1071
|
+
_load_err = cpe.stderr if cpe.stderr else str(cpe)
|
|
1072
|
+
except CancelledError:
|
|
1073
|
+
return
|
|
1074
|
+
except Exception as e:
|
|
1075
|
+
_load_err = str(e)
|
|
1076
|
+
print(f"❌ Error loading {_src_file_relative} -> {_dst_img}")
|
|
1077
|
+
failures.append((_task_name, _src_file_relative, _dst_img, _load_err))
|
|
1078
|
+
|
|
1079
|
+
override_os, override_arch = self.platform.split("/", maxsplit=1)
|
|
1080
|
+
|
|
1081
|
+
# Submit tasks
|
|
1082
|
+
for task_name, src_file, src_file_relative, dst_img in loading_tasks:
|
|
1083
|
+
command = [
|
|
1084
|
+
"skopeo",
|
|
1085
|
+
"copy",
|
|
1086
|
+
"--dest-tls-verify=false",
|
|
1087
|
+
"--retry-times",
|
|
1088
|
+
str(self.max_retries),
|
|
1089
|
+
"--override-os",
|
|
1090
|
+
override_os,
|
|
1091
|
+
"--override-arch",
|
|
1092
|
+
override_arch,
|
|
1093
|
+
f"{self.archive_format}-archive:{src_file}",
|
|
1094
|
+
]
|
|
1095
|
+
if self.storage == "docker":
|
|
1096
|
+
command.append(
|
|
1097
|
+
f"docker-daemon:{dst_img}",
|
|
1098
|
+
)
|
|
1099
|
+
else:
|
|
1100
|
+
command.append(
|
|
1101
|
+
f"containers-storage:{dst_img}",
|
|
1102
|
+
)
|
|
1103
|
+
|
|
1104
|
+
future = executor.submit(
|
|
1105
|
+
_execute_command,
|
|
1106
|
+
title=task_name,
|
|
1107
|
+
description=f"⏳ Loading {src_file_relative} -> {dst_img}...",
|
|
832
1108
|
command=command,
|
|
833
1109
|
)
|
|
834
1110
|
future.add_done_callback(check_result)
|
|
835
|
-
futures[future] =
|
|
1111
|
+
futures[future] = (task_name, src_file_relative, dst_img)
|
|
836
1112
|
|
|
837
1113
|
# Wait
|
|
838
1114
|
try:
|
|
@@ -846,11 +1122,11 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
846
1122
|
# Review
|
|
847
1123
|
print()
|
|
848
1124
|
if failures:
|
|
849
|
-
print(f"⚠️ Error
|
|
850
|
-
for
|
|
851
|
-
print(f" - {
|
|
852
|
-
if
|
|
853
|
-
for line in
|
|
1125
|
+
print(f"⚠️ Error loading {len(failures)} images:")
|
|
1126
|
+
for task_name, src_file_relative, dst_img, load_err in failures:
|
|
1127
|
+
print(f" - [{task_name}]: {src_file_relative} -> {dst_img}:")
|
|
1128
|
+
if load_err:
|
|
1129
|
+
for line in load_err.splitlines():
|
|
854
1130
|
print(f" {line}")
|
|
855
1131
|
else:
|
|
856
1132
|
print(" (no error message)")
|
|
@@ -1108,9 +1384,6 @@ def list_images(**kwargs) -> list[PlatformedImage]:
|
|
|
1108
1384
|
name = img.name
|
|
1109
1385
|
if not name:
|
|
1110
1386
|
continue
|
|
1111
|
-
if namespace := envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE:
|
|
1112
|
-
name = name.replace("gpustack/", f"{namespace}/")
|
|
1113
|
-
img.name = name
|
|
1114
1387
|
if name not in image_names_index:
|
|
1115
1388
|
image_names_index[name] = len(images)
|
|
1116
1389
|
images.append(img)
|
gpustack_runner/envs.py
CHANGED
|
@@ -9,6 +9,11 @@ if TYPE_CHECKING:
|
|
|
9
9
|
|
|
10
10
|
# Global
|
|
11
11
|
|
|
12
|
+
GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY: str | None = None
|
|
13
|
+
"""
|
|
14
|
+
Default container registry for copying images.
|
|
15
|
+
If not set, it should be "docker.io".
|
|
16
|
+
"""
|
|
12
17
|
GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE: str | None = None
|
|
13
18
|
"""
|
|
14
19
|
Namespace for default runner container images.
|
|
@@ -19,13 +24,24 @@ if TYPE_CHECKING:
|
|
|
19
24
|
|
|
20
25
|
variables: dict[str, Callable[[], Any]] = {
|
|
21
26
|
# Global
|
|
27
|
+
"GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY": lambda: trim_str(
|
|
28
|
+
getenvs(
|
|
29
|
+
[
|
|
30
|
+
"GPUSTACK_RUNNER_DEFAULT_CONTAINER_REGISTRY",
|
|
31
|
+
# Compatible with gpustack/gpustack_runtime.
|
|
32
|
+
"GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_REGISTRY",
|
|
33
|
+
# Compatible with gpustack/gpustack.
|
|
34
|
+
"GPUSTACK_SYSTEM_DEFAULT_CONTAINER_REGISTRY",
|
|
35
|
+
],
|
|
36
|
+
),
|
|
37
|
+
),
|
|
22
38
|
"GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE": lambda: trim_str(
|
|
23
39
|
getenvs(
|
|
24
|
-
|
|
40
|
+
[
|
|
25
41
|
"GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE",
|
|
26
|
-
|
|
42
|
+
# Compatible with gpustack/gpustack_runtime.
|
|
27
43
|
"GPUSTACK_RUNTIME_DEPLOY_DEFAULT_CONTAINER_NAMESPACE",
|
|
28
|
-
|
|
44
|
+
# Legacy compatibility.
|
|
29
45
|
"GPUSTACK_RUNNER_DEFAULT_IMAGE_NAMESPACE",
|
|
30
46
|
"GPUSTACK_RUNTIME_DEPLOY_DEFAULT_IMAGE_NAMESPACE",
|
|
31
47
|
],
|
gpustack_runner/runner.py
CHANGED
|
@@ -10,10 +10,8 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from dataclasses_json import dataclass_json
|
|
12
12
|
|
|
13
|
-
from . import envs
|
|
14
|
-
|
|
15
13
|
_RE_DOCKER_IMAGE = re.compile(
|
|
16
|
-
r"(?:(?P<prefix>[\w\\.\-]+(?:/[\w\\.\-]+)*)/)?runner:(?P<backend>(Host|cann|corex|cuda|dtk|maca|musa|rocm))(?P<backend_version>[XY\d\\.]+)(?:-(?P<backend_variant>\w+))?-(?P<service>(vllm|voxbox|mindie|sglang))(?P<service_version>[\w\\.]+)(?:-(?P<suffix>\w+))?",
|
|
14
|
+
r"(?:(?P<prefix>[\w\\.\-]+(?:/[\w\\.\-]+)*)/)?runner:(?P<backend>(Host|cann|corex|cuda|dtk|hggc|maca|musa|rocm))(?P<backend_version>[XY\d\\.]+)(?:-(?P<backend_variant>\w+))?-(?P<service>(vllm|voxbox|mindie|sglang))(?P<service_version>[\w\\.]+)(?:-(?P<suffix>\w+))?",
|
|
17
15
|
)
|
|
18
16
|
"""
|
|
19
17
|
Regex for Docker image parsing,
|
|
@@ -239,10 +237,6 @@ def list_runners(**kwargs) -> Runners | list[dict]:
|
|
|
239
237
|
json_list = json.load(f)
|
|
240
238
|
runners = []
|
|
241
239
|
for item in json_list:
|
|
242
|
-
if namespace := envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE:
|
|
243
|
-
docker_image = item["docker_image"]
|
|
244
|
-
docker_image = docker_image.replace("gpustack/", f"{namespace}/")
|
|
245
|
-
item["docker_image"] = docker_image
|
|
246
240
|
runners.append(Runner.from_dict(item))
|
|
247
241
|
|
|
248
242
|
todict = kwargs.pop("todict", False)
|
gpustack_runner/runner.py.json
CHANGED
|
@@ -1396,6 +1396,28 @@
|
|
|
1396
1396
|
"docker_image": "gpustack/runner:dtk25.04-vllm0.8.5",
|
|
1397
1397
|
"deprecated": false
|
|
1398
1398
|
},
|
|
1399
|
+
{
|
|
1400
|
+
"backend": "hggc",
|
|
1401
|
+
"backend_version": "12.3",
|
|
1402
|
+
"original_backend_version": "12.3",
|
|
1403
|
+
"backend_variant": "",
|
|
1404
|
+
"service": "sglang",
|
|
1405
|
+
"service_version": "0.5.5",
|
|
1406
|
+
"platform": "linux/amd64",
|
|
1407
|
+
"docker_image": "gpustack/runner:hggc12.3-sglang0.5.5",
|
|
1408
|
+
"deprecated": false
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
"backend": "hggc",
|
|
1412
|
+
"backend_version": "12.3",
|
|
1413
|
+
"original_backend_version": "12.3",
|
|
1414
|
+
"backend_variant": "",
|
|
1415
|
+
"service": "vllm",
|
|
1416
|
+
"service_version": "0.11.1",
|
|
1417
|
+
"platform": "linux/amd64",
|
|
1418
|
+
"docker_image": "gpustack/runner:hggc12.3-vllm0.11.1",
|
|
1419
|
+
"deprecated": false
|
|
1420
|
+
},
|
|
1399
1421
|
{
|
|
1400
1422
|
"backend": "maca",
|
|
1401
1423
|
"backend_version": "3.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gpustack-runner
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.24.post1
|
|
4
4
|
Summary: GPUStack Runner is library for registering runnable accelerated backends and services in GPUStack.
|
|
5
5
|
Project-URL: Homepage, https://github.com/gpustack/runner
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/gpustack/gpustack/issues
|
|
@@ -46,24 +46,20 @@ The following table lists the supported accelerated backends and their correspon
|
|
|
46
46
|
|
|
47
47
|
### Ascend CANN
|
|
48
48
|
|
|
49
|
-
> [!CAUTION]
|
|
50
|
-
> Since v0.1.23:
|
|
51
|
-
> - Deprecated MindIE `2.1.rc1`.
|
|
52
|
-
|
|
53
49
|
> [!WARNING]
|
|
54
50
|
> - The Atlas 300I series is currently experimental in vLLM, only supporting eager mode and float16 data type. And there
|
|
55
51
|
are some known issues for running vLLM, you can refer to
|
|
56
52
|
vllm-ascend [#3316](https://github.com/vllm-project/vllm-ascend/issues/3316)
|
|
57
53
|
and [#2795](https://github.com/vllm-project/vllm-ascend/issues/2795).
|
|
58
54
|
|
|
59
|
-
| CANN Version <br/> (Variant) | MindIE
|
|
60
|
-
|
|
61
|
-
| 8.3 (A3/910C) | `2.2.rc1`
|
|
62
|
-
| 8.3 (910B) | `2.2.rc1`
|
|
63
|
-
| 8.3 (310P) | `2.2.rc1`
|
|
64
|
-
| 8.2 (A3/910C) | `2.1.rc2`
|
|
65
|
-
| 8.2 (910B) | `2.1.rc2
|
|
66
|
-
| 8.2 (310P) | `2.1.rc2
|
|
55
|
+
| CANN Version <br/> (Variant) | MindIE | vLLM | SGLang |
|
|
56
|
+
|------------------------------|-----------|------------------------------------------------------------|------------------------|
|
|
57
|
+
| 8.3 (A3/910C) | `2.2.rc1` | `0.13.0`, `0.12.0`, `0.11.0` | `0.5.7`, `0.5.6.post2` |
|
|
58
|
+
| 8.3 (910B) | `2.2.rc1` | `0.13.0`, `0.12.0`, `0.11.0` | `0.5.7`, `0.5.6.post2` |
|
|
59
|
+
| 8.3 (310P) | `2.2.rc1` | | |
|
|
60
|
+
| 8.2 (A3/910C) | `2.1.rc2` | `0.10.2`, `0.10.1.1` | `0.5.2`, `0.5.1.post3` |
|
|
61
|
+
| 8.2 (910B) | `2.1.rc2` | `0.10.2`, `0.10.1.1`, <br/>`0.10.0`, `0.9.2`, <br/>`0.9.1` | `0.5.2`, `0.5.1.post3` |
|
|
62
|
+
| 8.2 (310P) | `2.1.rc2` | `0.10.0`, `0.9.2` | |
|
|
67
63
|
|
|
68
64
|
### Iluvatar CoreX
|
|
69
65
|
|
|
@@ -73,13 +69,6 @@ The following table lists the supported accelerated backends and their correspon
|
|
|
73
69
|
|
|
74
70
|
### NVIDIA CUDA
|
|
75
71
|
|
|
76
|
-
> [!CAUTION]
|
|
77
|
-
> Since v0.1.23:
|
|
78
|
-
> - Deprecated all services for CUDA 12.4.
|
|
79
|
-
> - Deprecated vLLM `0.11.0`, `0.10.1.1`, `0.10.0`.
|
|
80
|
-
> - Deprecated SGLang `0.5.5`.
|
|
81
|
-
> - Deprecated VoxBox `0.0.20`.
|
|
82
|
-
|
|
83
72
|
> [!NOTE]
|
|
84
73
|
> - CUDA 12.9 supports Compute Capabilities:
|
|
85
74
|
`7.5 8.0+PTX 8.9 9.0 10.0 10.3 12.0 12.1+PTX`.
|
|
@@ -88,12 +77,11 @@ The following table lists the supported accelerated backends and their correspon
|
|
|
88
77
|
> - CUDA 12.6/12.4 supports Compute Capabilities:
|
|
89
78
|
`7.5 8.0+PTX 8.9 9.0+PTX`.
|
|
90
79
|
|
|
91
|
-
| CUDA Version <br/> (Variant) | vLLM
|
|
92
|
-
|
|
93
|
-
| 12.9 | `0.13.0`, `0.12.0`, <br/>`0.11.2`
|
|
94
|
-
| 12.8 | `0.13.0`, `0.12.0`, <br/>`0.11.2`,
|
|
95
|
-
| 12.6 | `0.13.0`, `0.12.0`, <br/>`0.11.2
|
|
96
|
-
| 12.4 | ~~`0.11.0`~~, ~~`0.10.2`~~, <br/>~~`0.10.1.1`~~, ~~`0.10.0`~~ | | ~~`0.0.20`~~ |
|
|
80
|
+
| CUDA Version <br/> (Variant) | vLLM | SGLang | VoxBox |
|
|
81
|
+
|------------------------------|---------------------------------------------|-----------------------------------------------------------|----------|
|
|
82
|
+
| 12.9 | `0.13.0`, `0.12.0`, <br/>`0.11.2` | `0.5.7`, `0.5.6.post2` | |
|
|
83
|
+
| 12.8 | `0.13.0`, `0.12.0`, <br/>`0.11.2`, `0.10.2` | `0.5.7`, `0.5.6.post2`, <br/>`0.5.5.post3`, `0.5.4.post3` | `0.0.21` |
|
|
84
|
+
| 12.6 | `0.13.0`, `0.12.0`, <br/>`0.11.2`,`0.10.2` | | `0.0.21` |
|
|
97
85
|
|
|
98
86
|
### Hygon DTK
|
|
99
87
|
|
|
@@ -101,6 +89,12 @@ The following table lists the supported accelerated backends and their correspon
|
|
|
101
89
|
|-----------------------------|----------------------------|
|
|
102
90
|
| 25.04 | `0.11.0`, `0.9.2`, `0.8.5` |
|
|
103
91
|
|
|
92
|
+
### THead HGGC
|
|
93
|
+
|
|
94
|
+
| HGGC Version <br/> (Variant) | vLLM | SGLang |
|
|
95
|
+
|------------------------------|----------|---------|
|
|
96
|
+
| 12.3 | `0.11.1` | `0.5.5` |
|
|
97
|
+
|
|
104
98
|
### MetaX MACA
|
|
105
99
|
|
|
106
100
|
| MACA Version <br/> (Variant) | vLLM |
|
|
@@ -117,29 +111,23 @@ The following table lists the supported accelerated backends and their correspon
|
|
|
117
111
|
|
|
118
112
|
### AMD ROCm
|
|
119
113
|
|
|
120
|
-
> [!CAUTION]
|
|
121
|
-
> Since v0.1.23:
|
|
122
|
-
> - Deprecated all services for ROCm 6.3.
|
|
123
|
-
> - Deprecated vLLM `0.11.0`.
|
|
124
|
-
|
|
125
114
|
> [!NOTE]
|
|
126
115
|
> - ROCm 7.0 supports LLVM targets:
|
|
127
116
|
`gfx908 gfx90a gfx942 gfx950 gfx1030 gfx1100 gfx1101 gfx1200 gfx1201 gfx1150 gfx1151`.
|
|
128
|
-
> - ROCm 6.4
|
|
117
|
+
> - ROCm 6.4 supports LLVM targets:
|
|
129
118
|
`gfx908 gfx90a gfx942 gfx1030 gfx1100`.
|
|
130
119
|
|
|
131
120
|
> [!WARNING]
|
|
132
121
|
> - ROCm 7.0 vLLM `0.11.2/0.11.0` are reusing the official ROCm 6.4 PyTorch 2.9 wheel package rather than a ROCm
|
|
133
|
-
7.0 specific PyTorch build. Although supports ROCm 7.0 in vLLM `0.11.2
|
|
122
|
+
7.0 specific PyTorch build. Although supports ROCm 7.0 in vLLM `0.11.2`, `gfx1150/gfx1151` are not supported yet.
|
|
134
123
|
> - ROCm 6.4 vLLM `0.13.0` supports `gfx903 gfx90a gfx942` only.
|
|
135
124
|
> - ROCm 6.4 SGLang supports `gfx942` only.
|
|
136
125
|
> - ROCm 7.0 SGLang supports `gfx950` only.
|
|
137
126
|
|
|
138
|
-
| ROCm Version <br/> (Variant) | vLLM
|
|
139
|
-
|
|
140
|
-
| 7.0 | `0.13.0`, `0.12.0`, <br/>`0.11.2
|
|
141
|
-
| 6.4 | `0.13.0`, `0.12.0`, <br/>`0.11.2`, `0.10.2`
|
|
142
|
-
| 6.3 | ~~`0.10.1.1`~~, ~~`0.10.0`~~ | |
|
|
127
|
+
| ROCm Version <br/> (Variant) | vLLM | SGLang |
|
|
128
|
+
|------------------------------|---------------------------------------------|--------------------------------------------|
|
|
129
|
+
| 7.0 | `0.13.0`, `0.12.0`, <br/>`0.11.2` | `0.5.7`, `0.5.6.post2` |
|
|
130
|
+
| 6.4 | `0.13.0`, `0.12.0`, <br/>`0.11.2`, `0.10.2` | `0.5.7`, `0.5.6.post2`, <br/>`0.5.5.post3` |
|
|
143
131
|
|
|
144
132
|
## Directory Structure
|
|
145
133
|
|
|
@@ -251,7 +239,7 @@ To add support for a new accelerated backend:
|
|
|
251
239
|
|
|
252
240
|
1. Create a new directory under `pack/` named with the new backend.
|
|
253
241
|
2. Add a `Dockerfile` in the new directory following the [Dockerfile Convention](#dockerfile-convention).
|
|
254
|
-
3. Update [pack.yml](.github/workflows/pack.yml) to include the new backend in the build matrix.
|
|
242
|
+
3. Update [pack.yml](.github/workflows/pack.yml), [discard.yml](.github/workflows/discard.yml) and [prune.yml](.github/workflows/prune.yml) to include the new backend in the build matrix.
|
|
255
243
|
4. Update [matrix.yml](pack/matrix.yaml) to include the new backend and its variants.
|
|
256
244
|
5. Update `_RE_DOCKER_IMAGE` in [runner.py](gpustack_runner/runner.py) to recognize the new backend.
|
|
257
245
|
6. [Optional] Update [tests](tests/gpustack_runner) if necessary.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
gpustack_runner/__init__.py,sha256=ss8_wsk1oo13qFlP9usrXKX4ypHs1NZb2OVd6uFx5cc,715
|
|
2
|
+
gpustack_runner/__main__.py,sha256=uvpk9GtyJGtHaKM7DyM64N5mwehwpDD3v8ba6Yy8V3A,1364
|
|
3
|
+
gpustack_runner/__utils__.py,sha256=LSo0Iqxd5OjQFncVOYcqC8cncjtywf0qac31UPw7Ou4,4372
|
|
4
|
+
gpustack_runner/_version.py,sha256=Ix4zhdok3sdfQ1mOI_tph_pbj5GmpJ04rohojJ2w17c,792
|
|
5
|
+
gpustack_runner/_version.pyi,sha256=A42NoSgcqEXVy2OeNm4LXC9CbyonbooYrSUBlPm2lGY,156
|
|
6
|
+
gpustack_runner/envs.py,sha256=Wm0GTIiDJIT1zEjLpaPZNLbOs23NNFc2Y6zZuRLlTKQ,3470
|
|
7
|
+
gpustack_runner/runner.py,sha256=Rk4nyHj7Bn7ibAdxspXpXplMgWVMsG9Jb7GV8sHk2Ig,26310
|
|
8
|
+
gpustack_runner/runner.py.json,sha256=rQEZrBRAQngUY9z2AntWTnnwjhTzZI_yywMxhjAAcAw,45403
|
|
9
|
+
gpustack_runner/cmds/__init__.py,sha256=zjdv_OC674KAcitjiHrHbXnAwLtw8Ju3psW0IKFqPIg,471
|
|
10
|
+
gpustack_runner/cmds/__types__.py,sha256=7C4kQM0EHPD8WpJpTo6kh9rEdkrYALcLQ-GAzMMsqV8,789
|
|
11
|
+
gpustack_runner/cmds/images.py,sha256=dH9gSoOpUCyjarfpJ1I7MSR98hYtGVsfcochcC3JqhI,49025
|
|
12
|
+
gpustack_runner/_version_appendix.py,sha256=P3_fpwmk6qL5k6PhLsWQyJeGhOHfewu1ukL_vQh56KU,23
|
|
13
|
+
gpustack_runner-0.1.24.post1.dist-info/METADATA,sha256=owCPWsrDOrEOZTrHTceS4yeMFOwvWkKmAdxNSyFvcgU,12323
|
|
14
|
+
gpustack_runner-0.1.24.post1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
gpustack_runner-0.1.24.post1.dist-info/entry_points.txt,sha256=M1Dxl6cY0kIgf2I4pPsV-_kU6BAtjj93spmsXAdwW3s,66
|
|
16
|
+
gpustack_runner-0.1.24.post1.dist-info/licenses/LICENSE,sha256=OiPibowBvB-NHV3TP_NOj18XNBlXcshXZFMpa3uvKVE,10362
|
|
17
|
+
gpustack_runner-0.1.24.post1.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
gpustack_runner/__init__.py,sha256=0_0jsxo1xjLtHTOIEU0_-A1qFEANzsVw-uXGjcILDwk,530
|
|
2
|
-
gpustack_runner/__main__.py,sha256=wtcp9lwMkaXGbQkuOY08EQhKfIHcTLSjMdnj2W3UGwk,1285
|
|
3
|
-
gpustack_runner/_version.py,sha256=NDbGgX_SVQwK4ytZFVrGe_PzIARK2sZAIaj9J28Xql4,792
|
|
4
|
-
gpustack_runner/_version.pyi,sha256=A42NoSgcqEXVy2OeNm4LXC9CbyonbooYrSUBlPm2lGY,156
|
|
5
|
-
gpustack_runner/envs.py,sha256=EVQvU1bdF5DHSY83X1mHkQWiF4jlLhre1YCArAFe5Wk,2862
|
|
6
|
-
gpustack_runner/runner.py,sha256=PlwYWNj_9kVB-Aoo0dmCHVPiAjKVT3ZoezhAiYtJiJA,26589
|
|
7
|
-
gpustack_runner/runner.py.json,sha256=a6U7utY4wZ2yEGZ2wEGmbDzoVb3GvzbK_1GrIcM9yQA,44801
|
|
8
|
-
gpustack_runner/cmds/__init__.py,sha256=Os8FdvqNjLYiVn_jnDo7rFEtAeVLJJI1odKHEqWF-Fw,417
|
|
9
|
-
gpustack_runner/cmds/__types__.py,sha256=7C4kQM0EHPD8WpJpTo6kh9rEdkrYALcLQ-GAzMMsqV8,789
|
|
10
|
-
gpustack_runner/cmds/images.py,sha256=UM-fO_7lKzfaFep8_kHxy36AkjSw-ZUk3Ty2dCQmvnQ,38909
|
|
11
|
-
gpustack_runner/_version_appendix.py,sha256=YRx5Cq7oDt_x3DaU5ub-D03qpMmWfqMHO-Dr6CBv2fY,23
|
|
12
|
-
gpustack_runner-0.1.23.post5.dist-info/METADATA,sha256=BFlwdr9yVQiF0uYhilCS-GL3EAPhraqf9kf_T5Kcq4g,13295
|
|
13
|
-
gpustack_runner-0.1.23.post5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
14
|
-
gpustack_runner-0.1.23.post5.dist-info/entry_points.txt,sha256=M1Dxl6cY0kIgf2I4pPsV-_kU6BAtjj93spmsXAdwW3s,66
|
|
15
|
-
gpustack_runner-0.1.23.post5.dist-info/licenses/LICENSE,sha256=OiPibowBvB-NHV3TP_NOj18XNBlXcshXZFMpa3uvKVE,10362
|
|
16
|
-
gpustack_runner-0.1.23.post5.dist-info/RECORD,,
|
|
File without changes
|
{gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.post1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.post1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|