gpustack-runner 0.1.23.post5__py3-none-any.whl → 0.1.24__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 +381 -99
- gpustack_runner/envs.py +19 -3
- gpustack_runner/runner.py +1 -1
- gpustack_runner/runner.py.json +22 -0
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.dist-info}/METADATA +8 -2
- gpustack_runner-0.1.24.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.dist-info}/WHEEL +0 -0
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.dist-info}/entry_points.txt +0 -0
- {gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.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'
|
|
31
|
+
__version_tuple__ = version_tuple = (0, 1, 24)
|
|
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 = "c2b7172"
|
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,21 @@ 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
|
-
f"⏳ Syncing image '{img.name}' to '{dest_img_name}'..."
|
|
829
|
-
if img.name != dest_img_name
|
|
830
|
-
else f"⏳ Syncing image '{img.name}'..."
|
|
831
|
-
),
|
|
862
|
+
title=task_name,
|
|
863
|
+
description=f"⏳ Copying {src_img} -> {dst_img}...",
|
|
832
864
|
command=command,
|
|
833
865
|
)
|
|
834
866
|
future.add_done_callback(check_result)
|
|
835
|
-
futures[future] =
|
|
867
|
+
futures[future] = (task_name, src_img, dst_img)
|
|
836
868
|
|
|
837
869
|
# Wait
|
|
838
870
|
try:
|
|
@@ -846,11 +878,255 @@ class CopyImagesSubCommand(SubCommand):
|
|
|
846
878
|
# Review
|
|
847
879
|
print()
|
|
848
880
|
if failures:
|
|
849
|
-
print(f"⚠️ Error
|
|
850
|
-
for
|
|
851
|
-
print(f" - {
|
|
852
|
-
if
|
|
853
|
-
for line in
|
|
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}...",
|
|
1108
|
+
command=command,
|
|
1109
|
+
)
|
|
1110
|
+
future.add_done_callback(check_result)
|
|
1111
|
+
futures[future] = (task_name, src_file_relative, dst_img)
|
|
1112
|
+
|
|
1113
|
+
# Wait
|
|
1114
|
+
try:
|
|
1115
|
+
for _ in as_completed(futures):
|
|
1116
|
+
pass
|
|
1117
|
+
except Exception:
|
|
1118
|
+
for future in futures:
|
|
1119
|
+
future.cancel()
|
|
1120
|
+
raise
|
|
1121
|
+
|
|
1122
|
+
# Review
|
|
1123
|
+
print()
|
|
1124
|
+
if failures:
|
|
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)")
|
|
@@ -1072,6 +1348,10 @@ def list_images(**kwargs) -> list[PlatformedImage]:
|
|
|
1072
1348
|
A list of platformed images.
|
|
1073
1349
|
|
|
1074
1350
|
"""
|
|
1351
|
+
# Reset to default for listing images,
|
|
1352
|
+
# in case the env is set to other value.
|
|
1353
|
+
envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE = None
|
|
1354
|
+
|
|
1075
1355
|
platform = kwargs.pop("platform", None)
|
|
1076
1356
|
repository = kwargs.pop("repository", None)
|
|
1077
1357
|
|
|
@@ -1108,9 +1388,6 @@ def list_images(**kwargs) -> list[PlatformedImage]:
|
|
|
1108
1388
|
name = img.name
|
|
1109
1389
|
if not name:
|
|
1110
1390
|
continue
|
|
1111
|
-
if namespace := envs.GPUSTACK_RUNNER_DEFAULT_CONTAINER_NAMESPACE:
|
|
1112
|
-
name = name.replace("gpustack/", f"{namespace}/")
|
|
1113
|
-
img.name = name
|
|
1114
1391
|
if name not in image_names_index:
|
|
1115
1392
|
image_names_index[name] = len(images)
|
|
1116
1393
|
images.append(img)
|
|
@@ -1220,3 +1497,8 @@ def _execute_command(
|
|
|
1220
1497
|
args=command,
|
|
1221
1498
|
returncode=returncode,
|
|
1222
1499
|
)
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
append_images(
|
|
1503
|
+
"gpustack/runtime:pause",
|
|
1504
|
+
)
|
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
|
@@ -13,7 +13,7 @@ from dataclasses_json import dataclass_json
|
|
|
13
13
|
from . import envs
|
|
14
14
|
|
|
15
15
|
_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+))?",
|
|
16
|
+
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
17
|
)
|
|
18
18
|
"""
|
|
19
19
|
Regex for Docker image parsing,
|
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
|
|
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
|
|
@@ -101,6 +101,12 @@ The following table lists the supported accelerated backends and their correspon
|
|
|
101
101
|
|-----------------------------|----------------------------|
|
|
102
102
|
| 25.04 | `0.11.0`, `0.9.2`, `0.8.5` |
|
|
103
103
|
|
|
104
|
+
### THead HGGC
|
|
105
|
+
|
|
106
|
+
| HGGC Version <br/> (Variant) | vLLM | SGLang |
|
|
107
|
+
|------------------------------|----------|---------|
|
|
108
|
+
| 12.3 | `0.11.1` | `0.5.5` |
|
|
109
|
+
|
|
104
110
|
### MetaX MACA
|
|
105
111
|
|
|
106
112
|
| MACA Version <br/> (Variant) | vLLM |
|
|
@@ -251,7 +257,7 @@ To add support for a new accelerated backend:
|
|
|
251
257
|
|
|
252
258
|
1. Create a new directory under `pack/` named with the new backend.
|
|
253
259
|
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.
|
|
260
|
+
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
261
|
4. Update [matrix.yml](pack/matrix.yaml) to include the new backend and its variants.
|
|
256
262
|
5. Update `_RE_DOCKER_IMAGE` in [runner.py](gpustack_runner/runner.py) to recognize the new backend.
|
|
257
263
|
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=PGtcaWdpQHoovDMPesJn_siACNRb_TCcIlFvVB6YEig,777
|
|
5
|
+
gpustack_runner/_version.pyi,sha256=A42NoSgcqEXVy2OeNm4LXC9CbyonbooYrSUBlPm2lGY,156
|
|
6
|
+
gpustack_runner/envs.py,sha256=Wm0GTIiDJIT1zEjLpaPZNLbOs23NNFc2Y6zZuRLlTKQ,3470
|
|
7
|
+
gpustack_runner/runner.py,sha256=WOERxhW2huq_zESGaJ7ntlWFeQJkOs1NWc8XSrlTJTI,26594
|
|
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=Bd_yoW7MboVNAxtpMlf8JKm9vPGvuIcHcxInA9VCUsI,49223
|
|
12
|
+
gpustack_runner/_version_appendix.py,sha256=ZgtpyK-U9XcDUsfZR8KHFW6SDKmtMp9w4_lXLNiK5YY,23
|
|
13
|
+
gpustack_runner-0.1.24.dist-info/METADATA,sha256=T6nsHWr00ZtYpB8Ri0ADF5aqhTJxc2V54QKzi_B71mw,13559
|
|
14
|
+
gpustack_runner-0.1.24.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
gpustack_runner-0.1.24.dist-info/entry_points.txt,sha256=M1Dxl6cY0kIgf2I4pPsV-_kU6BAtjj93spmsXAdwW3s,66
|
|
16
|
+
gpustack_runner-0.1.24.dist-info/licenses/LICENSE,sha256=OiPibowBvB-NHV3TP_NOj18XNBlXcshXZFMpa3uvKVE,10362
|
|
17
|
+
gpustack_runner-0.1.24.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.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{gpustack_runner-0.1.23.post5.dist-info → gpustack_runner-0.1.24.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|