ls-algorithm-plugin-sdk 0.3.0__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/PKG-INFO +6 -17
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/README.md +5 -16
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/pyproject.toml +1 -1
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/configure.py +86 -46
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/run.py +5 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/serve.py +16 -2
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/deployment.py +14 -0
- ls_algorithm_plugin_sdk-0.3.3/src/algorithm_plugin_sdk/gpu_isolation.py +27 -0
- ls_algorithm_plugin_sdk-0.3.3/src/algorithm_plugin_sdk/log_manager.py +76 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/service.py +80 -2
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/PKG-INFO +6 -17
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/SOURCES.txt +3 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_configure.py +103 -37
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_deployment.py +43 -0
- ls_algorithm_plugin_sdk-0.3.3/tests/test_gpu_isolation.py +38 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_runner.py +26 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_service.py +20 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/setup.cfg +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/parsing.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/context.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/errors.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/examples/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/examples/example_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/examples/simulated_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/loader.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/runner.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/app.css +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/app.js +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/index.html +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui_app.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/dependency_links.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/entry_points.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/requires.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/top_level.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_sdk_automation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ls-algorithm-plugin-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Protocol-independent runtime SDK for dataset algorithms
|
|
5
5
|
Author: Ling Robotics
|
|
6
6
|
License: Proprietary
|
|
@@ -15,10 +15,6 @@ Requires-Dist: build>=1.2; extra == "dev"
|
|
|
15
15
|
|
|
16
16
|
# Algorithm Plugin SDK
|
|
17
17
|
|
|
18
|
-
算法仓库只实现一个 `Algorithm` 类并声明 entry point。SDK 统一提供单次运行、
|
|
19
|
-
WebUI、HTTP service、release manifest、compute 注册和心跳。算法仓库不需要
|
|
20
|
-
Dockerfile 或 Web 框架代码。
|
|
21
|
-
|
|
22
18
|
## 算法侧最小接口
|
|
23
19
|
|
|
24
20
|
```python
|
|
@@ -30,8 +26,7 @@ class MyAlgorithm(Algorithm):
|
|
|
30
26
|
def execute(self, request, context): ...
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point
|
|
34
|
-
会被所有运行方式复用。
|
|
29
|
+
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point。
|
|
35
30
|
|
|
36
31
|
## 单次运行
|
|
37
32
|
|
|
@@ -49,10 +44,9 @@ algorithm-plugin run camera-space-mano \
|
|
|
49
44
|
|
|
50
45
|
目标算法仓库必须满足:
|
|
51
46
|
|
|
52
|
-
- 仓库本身是 Git
|
|
47
|
+
- 仓库本身是 Git 工作树根目录。
|
|
53
48
|
- 仓库包含自己的 `.venv`。
|
|
54
49
|
- `<repository>/.venv/bin/algorithm-plugin` 存在且可执行。
|
|
55
|
-
- 该启动器的 `run --help` 和 `serve --help` 均可正常加载。
|
|
56
50
|
|
|
57
51
|
建议将生成的 `release-manifest.json` 和 `algorithm-plugin.json` 加入算法仓库的
|
|
58
52
|
`.gitignore`,不要手动编辑或提交它们。
|
|
@@ -64,16 +58,11 @@ sudo /srv/camera-space-mano/.venv/bin/algorithm-plugin configure \
|
|
|
64
58
|
--repository /srv/camera-space-mano \
|
|
65
59
|
--compute-url http://compute:5180 \
|
|
66
60
|
--api-key "$LDP_INTERNAL_API_KEY" \
|
|
67
|
-
--
|
|
68
|
-
--input-root /data/jobs/input \
|
|
69
|
-
--workspace-root /data/jobs/workspace \
|
|
61
|
+
--port 9030 \
|
|
70
62
|
--cluster production \
|
|
71
|
-
--namespace camera-space-mano \
|
|
72
|
-
--node-name 10.0.0.4 \
|
|
73
63
|
--gpu-ids 0,1
|
|
74
64
|
```
|
|
75
65
|
|
|
76
|
-
service name 默认使用仓库目录名,也可以通过 `--service-name` 指定。
|
|
77
66
|
重复执行 configure 需要显式提供 `--force`。
|
|
78
67
|
|
|
79
68
|
确认输出后执行:
|
|
@@ -82,7 +71,7 @@ service name 默认使用仓库目录名,也可以通过 `--service-name` 指
|
|
|
82
71
|
sudo systemctl daemon-reload
|
|
83
72
|
sudo systemctl enable camera-space-mano.service
|
|
84
73
|
sudo systemctl restart camera-space-mano.service
|
|
85
|
-
sudo systemctl status camera-space-mano.service
|
|
74
|
+
sudo systemctl status camera-space-mano.service --no-pager
|
|
86
75
|
```
|
|
87
76
|
|
|
88
77
|
## serve
|
|
@@ -95,6 +84,6 @@ sudo systemctl status camera-space-mano.service
|
|
|
95
84
|
## 安装与测试
|
|
96
85
|
|
|
97
86
|
```bash
|
|
98
|
-
python -m pip install
|
|
87
|
+
python -m pip install .
|
|
99
88
|
python -m unittest discover -s tests -v
|
|
100
89
|
```
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# Algorithm Plugin SDK
|
|
2
2
|
|
|
3
|
-
算法仓库只实现一个 `Algorithm` 类并声明 entry point。SDK 统一提供单次运行、
|
|
4
|
-
WebUI、HTTP service、release manifest、compute 注册和心跳。算法仓库不需要
|
|
5
|
-
Dockerfile 或 Web 框架代码。
|
|
6
|
-
|
|
7
3
|
## 算法侧最小接口
|
|
8
4
|
|
|
9
5
|
```python
|
|
@@ -15,8 +11,7 @@ class MyAlgorithm(Algorithm):
|
|
|
15
11
|
def execute(self, request, context): ...
|
|
16
12
|
```
|
|
17
13
|
|
|
18
|
-
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point
|
|
19
|
-
会被所有运行方式复用。
|
|
14
|
+
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point。
|
|
20
15
|
|
|
21
16
|
## 单次运行
|
|
22
17
|
|
|
@@ -34,10 +29,9 @@ algorithm-plugin run camera-space-mano \
|
|
|
34
29
|
|
|
35
30
|
目标算法仓库必须满足:
|
|
36
31
|
|
|
37
|
-
- 仓库本身是 Git
|
|
32
|
+
- 仓库本身是 Git 工作树根目录。
|
|
38
33
|
- 仓库包含自己的 `.venv`。
|
|
39
34
|
- `<repository>/.venv/bin/algorithm-plugin` 存在且可执行。
|
|
40
|
-
- 该启动器的 `run --help` 和 `serve --help` 均可正常加载。
|
|
41
35
|
|
|
42
36
|
建议将生成的 `release-manifest.json` 和 `algorithm-plugin.json` 加入算法仓库的
|
|
43
37
|
`.gitignore`,不要手动编辑或提交它们。
|
|
@@ -49,16 +43,11 @@ sudo /srv/camera-space-mano/.venv/bin/algorithm-plugin configure \
|
|
|
49
43
|
--repository /srv/camera-space-mano \
|
|
50
44
|
--compute-url http://compute:5180 \
|
|
51
45
|
--api-key "$LDP_INTERNAL_API_KEY" \
|
|
52
|
-
--
|
|
53
|
-
--input-root /data/jobs/input \
|
|
54
|
-
--workspace-root /data/jobs/workspace \
|
|
46
|
+
--port 9030 \
|
|
55
47
|
--cluster production \
|
|
56
|
-
--namespace camera-space-mano \
|
|
57
|
-
--node-name 10.0.0.4 \
|
|
58
48
|
--gpu-ids 0,1
|
|
59
49
|
```
|
|
60
50
|
|
|
61
|
-
service name 默认使用仓库目录名,也可以通过 `--service-name` 指定。
|
|
62
51
|
重复执行 configure 需要显式提供 `--force`。
|
|
63
52
|
|
|
64
53
|
确认输出后执行:
|
|
@@ -67,7 +56,7 @@ service name 默认使用仓库目录名,也可以通过 `--service-name` 指
|
|
|
67
56
|
sudo systemctl daemon-reload
|
|
68
57
|
sudo systemctl enable camera-space-mano.service
|
|
69
58
|
sudo systemctl restart camera-space-mano.service
|
|
70
|
-
sudo systemctl status camera-space-mano.service
|
|
59
|
+
sudo systemctl status camera-space-mano.service --no-pager
|
|
71
60
|
```
|
|
72
61
|
|
|
73
62
|
## serve
|
|
@@ -80,6 +69,6 @@ sudo systemctl status camera-space-mano.service
|
|
|
80
69
|
## 安装与测试
|
|
81
70
|
|
|
82
71
|
```bash
|
|
83
|
-
python -m pip install
|
|
72
|
+
python -m pip install .
|
|
84
73
|
python -m unittest discover -s tests -v
|
|
85
74
|
```
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import ipaddress
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
7
|
+
import socket
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlparse
|
|
9
12
|
|
|
10
13
|
from ..deployment import (
|
|
11
14
|
DEFAULT_CONFIG_NAME,
|
|
@@ -13,10 +16,15 @@ from ..deployment import (
|
|
|
13
16
|
repository_root,
|
|
14
17
|
stable_instance_key,
|
|
15
18
|
)
|
|
16
|
-
from .parsing import environment, gpu_ids
|
|
19
|
+
from .parsing import environment, gpu_ids, json_object
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
SYSTEMD_UNIT_DIR = Path("/etc/systemd/system")
|
|
23
|
+
INPUT_ROOT = Path("/mnt/ldp_uploads/ldp-uploads")
|
|
24
|
+
PRIVATE_IPV4_NETWORKS = tuple(
|
|
25
|
+
ipaddress.ip_network(network)
|
|
26
|
+
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
|
27
|
+
)
|
|
20
28
|
|
|
21
29
|
SERVICE_NAME_PATTERN = re.compile(
|
|
22
30
|
r"^[A-Za-z0-9][A-Za-z0-9_.@-]*$"
|
|
@@ -48,7 +56,11 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
48
56
|
action=argparse.BooleanOptionalAction,
|
|
49
57
|
default=True,
|
|
50
58
|
)
|
|
51
|
-
parser.add_argument(
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--max-concurrency",
|
|
61
|
+
type=int,
|
|
62
|
+
help="maximum concurrent executions (default: GPU count, or 1)",
|
|
63
|
+
)
|
|
52
64
|
parser.add_argument("--scratch-dir")
|
|
53
65
|
parser.add_argument(
|
|
54
66
|
"--gpu-ids",
|
|
@@ -56,6 +68,12 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
56
68
|
default=[],
|
|
57
69
|
help="GPU IDs injected into every service execution",
|
|
58
70
|
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--default-parameters",
|
|
73
|
+
type=json_object,
|
|
74
|
+
default={},
|
|
75
|
+
help="JSON object used to fill missing request parameters",
|
|
76
|
+
)
|
|
59
77
|
parser.add_argument("--service-token")
|
|
60
78
|
parser.add_argument(
|
|
61
79
|
"--compute-url",
|
|
@@ -65,18 +83,6 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
65
83
|
"--api-key",
|
|
66
84
|
default=os.getenv("LDP_INTERNAL_API_KEY"),
|
|
67
85
|
)
|
|
68
|
-
parser.add_argument(
|
|
69
|
-
"--public-url",
|
|
70
|
-
default=os.getenv("LDP_PLUGIN_PUBLIC_URL"),
|
|
71
|
-
)
|
|
72
|
-
parser.add_argument(
|
|
73
|
-
"--input-root",
|
|
74
|
-
default=os.getenv("LDP_JOB_INPUT_ROOT"),
|
|
75
|
-
)
|
|
76
|
-
parser.add_argument(
|
|
77
|
-
"--workspace-root",
|
|
78
|
-
default=os.getenv("LDP_JOB_WORKSPACE_ROOT"),
|
|
79
|
-
)
|
|
80
86
|
parser.add_argument(
|
|
81
87
|
"--instance-key",
|
|
82
88
|
default=os.getenv("LDP_PLUGIN_INSTANCE_KEY"),
|
|
@@ -85,14 +91,6 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
85
91
|
"--cluster",
|
|
86
92
|
default=os.getenv("LDP_CLUSTER"),
|
|
87
93
|
)
|
|
88
|
-
parser.add_argument(
|
|
89
|
-
"--namespace",
|
|
90
|
-
default=os.getenv("POD_NAMESPACE"),
|
|
91
|
-
)
|
|
92
|
-
parser.add_argument(
|
|
93
|
-
"--node-name",
|
|
94
|
-
default=os.getenv("NODE_NAME"),
|
|
95
|
-
)
|
|
96
94
|
parser.add_argument(
|
|
97
95
|
"--local",
|
|
98
96
|
action="store_true",
|
|
@@ -105,10 +103,6 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
105
103
|
default=[],
|
|
106
104
|
metavar="NAME=VALUE",
|
|
107
105
|
)
|
|
108
|
-
parser.add_argument(
|
|
109
|
-
"--service-name",
|
|
110
|
-
help="systemd unit name without .service; defaults to repository name",
|
|
111
|
-
)
|
|
112
106
|
parser.add_argument(
|
|
113
107
|
"--force",
|
|
114
108
|
action="store_true",
|
|
@@ -119,33 +113,35 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
119
113
|
|
|
120
114
|
def resolve_service_name(
|
|
121
115
|
repository: Path,
|
|
122
|
-
requested: str | None,
|
|
123
116
|
) -> str:
|
|
124
|
-
name =
|
|
117
|
+
name = repository.name
|
|
125
118
|
if name.endswith(".service"):
|
|
126
119
|
raise ValueError(
|
|
127
|
-
"
|
|
120
|
+
"repository name must not include the .service suffix"
|
|
128
121
|
)
|
|
129
122
|
if not SERVICE_NAME_PATTERN.fullmatch(name):
|
|
130
123
|
raise ValueError(
|
|
131
|
-
"
|
|
124
|
+
"repository name may contain only letters, numbers, "
|
|
132
125
|
"underscore, dot, @, and hyphen"
|
|
133
126
|
)
|
|
134
127
|
return name
|
|
135
128
|
|
|
136
129
|
|
|
137
|
-
def
|
|
130
|
+
def _escape_systemd_path(value: Path) -> str:
|
|
138
131
|
text = str(value)
|
|
139
132
|
if "\n" in text or "\r" in text or "\0" in text:
|
|
140
133
|
raise ValueError(
|
|
141
134
|
f"systemd path contains control characters: {value}"
|
|
142
135
|
)
|
|
143
|
-
|
|
136
|
+
return (
|
|
144
137
|
text.replace("\\", "\\\\")
|
|
145
138
|
.replace('"', '\\"')
|
|
146
139
|
.replace("%", "%%")
|
|
147
140
|
)
|
|
148
|
-
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _quote_systemd_path(value: Path) -> str:
|
|
144
|
+
return f'"{_escape_systemd_path(value)}"'
|
|
149
145
|
|
|
150
146
|
|
|
151
147
|
def render_systemd_unit(
|
|
@@ -153,10 +149,18 @@ def render_systemd_unit(
|
|
|
153
149
|
repository: Path,
|
|
154
150
|
config_path: Path,
|
|
155
151
|
service_name: str,
|
|
152
|
+
gpu_ids: list[int] | None = None,
|
|
156
153
|
) -> str:
|
|
157
154
|
root = repository.resolve()
|
|
155
|
+
working_directory = root / "jobs"
|
|
158
156
|
launcher = root / ".venv" / "bin" / "algorithm-plugin"
|
|
159
157
|
config = config_path.resolve()
|
|
158
|
+
environment = ["Environment=PYTHONUNBUFFERED=1"]
|
|
159
|
+
if gpu_ids:
|
|
160
|
+
environment.append(
|
|
161
|
+
"Environment=CUDA_VISIBLE_DEVICES="
|
|
162
|
+
+ ",".join(str(gpu_id) for gpu_id in gpu_ids)
|
|
163
|
+
)
|
|
160
164
|
return "\n".join(
|
|
161
165
|
(
|
|
162
166
|
"[Unit]",
|
|
@@ -167,12 +171,13 @@ def render_systemd_unit(
|
|
|
167
171
|
"[Service]",
|
|
168
172
|
"Type=simple",
|
|
169
173
|
"User=root",
|
|
170
|
-
f"WorkingDirectory={
|
|
174
|
+
f"WorkingDirectory={_escape_systemd_path(working_directory)}",
|
|
171
175
|
(
|
|
172
176
|
f"ExecStart={_quote_systemd_path(launcher)} "
|
|
173
177
|
f"serve --config {_quote_systemd_path(config)}"
|
|
174
178
|
),
|
|
175
|
-
|
|
179
|
+
*environment,
|
|
180
|
+
"LimitNOFILE=524288",
|
|
176
181
|
"Restart=on-failure",
|
|
177
182
|
"RestartSec=5s",
|
|
178
183
|
"",
|
|
@@ -317,6 +322,35 @@ def resolve_config_path(
|
|
|
317
322
|
return path.resolve()
|
|
318
323
|
|
|
319
324
|
|
|
325
|
+
def resolve_private_ipv4(compute_url: str) -> str:
|
|
326
|
+
parsed = urlparse(compute_url)
|
|
327
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
328
|
+
raise ValueError("compute URL must be an http or https URL")
|
|
329
|
+
try:
|
|
330
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
331
|
+
except ValueError as exc:
|
|
332
|
+
raise ValueError("compute URL has an invalid port") from exc
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
|
|
336
|
+
probe.connect((parsed.hostname, port))
|
|
337
|
+
value = probe.getsockname()[0]
|
|
338
|
+
except OSError as exc:
|
|
339
|
+
raise RuntimeError(
|
|
340
|
+
f"cannot resolve the local IP used to reach {parsed.hostname}: {exc}"
|
|
341
|
+
) from exc
|
|
342
|
+
|
|
343
|
+
address = ipaddress.ip_address(value)
|
|
344
|
+
if not isinstance(address, ipaddress.IPv4Address) or not any(
|
|
345
|
+
address in network for network in PRIVATE_IPV4_NETWORKS
|
|
346
|
+
):
|
|
347
|
+
raise RuntimeError(
|
|
348
|
+
f"the local IP used to reach {parsed.hostname} is not a private IPv4: "
|
|
349
|
+
f"{address}"
|
|
350
|
+
)
|
|
351
|
+
return str(address)
|
|
352
|
+
|
|
353
|
+
|
|
320
354
|
def registration_from_args(
|
|
321
355
|
args: argparse.Namespace,
|
|
322
356
|
repository: Path,
|
|
@@ -327,12 +361,9 @@ def registration_from_args(
|
|
|
327
361
|
supplied = {
|
|
328
362
|
"computeUrl": args.compute_url,
|
|
329
363
|
"apiKey": args.api_key,
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"workspaceRoot": args.workspace_root,
|
|
364
|
+
"inputRoot": str(INPUT_ROOT),
|
|
365
|
+
"workspaceRoot": str(repository.resolve()),
|
|
333
366
|
"cluster": args.cluster,
|
|
334
|
-
"namespace": args.namespace,
|
|
335
|
-
"nodeName": args.node_name,
|
|
336
367
|
}
|
|
337
368
|
missing = [
|
|
338
369
|
name
|
|
@@ -350,6 +381,9 @@ def registration_from_args(
|
|
|
350
381
|
name: str(value)
|
|
351
382
|
for name, value in supplied.items()
|
|
352
383
|
}
|
|
384
|
+
private_ipv4 = resolve_private_ipv4(registration["computeUrl"])
|
|
385
|
+
registration["publicUrl"] = f"http://{private_ipv4}:{args.port}"
|
|
386
|
+
registration["nodeName"] = private_ipv4
|
|
353
387
|
registration["instanceKey"] = stable_instance_key(
|
|
354
388
|
repository / ".algorithm-plugin" / "instance-key",
|
|
355
389
|
requested=args.instance_key,
|
|
@@ -361,6 +395,11 @@ def generate_config(
|
|
|
361
395
|
args: argparse.Namespace,
|
|
362
396
|
repository: Path,
|
|
363
397
|
) -> DeploymentConfig:
|
|
398
|
+
max_concurrency = (
|
|
399
|
+
args.max_concurrency
|
|
400
|
+
if args.max_concurrency is not None
|
|
401
|
+
else max(len(args.gpu_ids), 1)
|
|
402
|
+
)
|
|
364
403
|
config = DeploymentConfig.generate(
|
|
365
404
|
output=args.config_output,
|
|
366
405
|
root=repository,
|
|
@@ -368,12 +407,13 @@ def generate_config(
|
|
|
368
407
|
host=args.host,
|
|
369
408
|
port=args.port,
|
|
370
409
|
webui=args.webui,
|
|
371
|
-
max_concurrency=
|
|
410
|
+
max_concurrency=max_concurrency,
|
|
372
411
|
scratch_dir=args.scratch_dir,
|
|
373
412
|
token=args.service_token,
|
|
374
413
|
registration=registration_from_args(args, repository),
|
|
375
414
|
gpu_ids=args.gpu_ids,
|
|
376
415
|
environment=dict(args.env),
|
|
416
|
+
default_parameters=args.default_parameters,
|
|
377
417
|
)
|
|
378
418
|
config.save()
|
|
379
419
|
return config
|
|
@@ -383,10 +423,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
383
423
|
require_root()
|
|
384
424
|
repository = validate_repository(args.repository)
|
|
385
425
|
validate_configure_runtime(repository)
|
|
386
|
-
service_name = resolve_service_name(
|
|
387
|
-
repository,
|
|
388
|
-
args.service_name,
|
|
389
|
-
)
|
|
426
|
+
service_name = resolve_service_name(repository)
|
|
390
427
|
config_path = resolve_config_path(
|
|
391
428
|
repository,
|
|
392
429
|
args.config_output,
|
|
@@ -395,6 +432,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
395
432
|
repository=repository,
|
|
396
433
|
config_path=config_path,
|
|
397
434
|
service_name=service_name,
|
|
435
|
+
gpu_ids=args.gpu_ids,
|
|
398
436
|
)
|
|
399
437
|
|
|
400
438
|
inspect_systemd_unit(
|
|
@@ -402,6 +440,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
402
440
|
content=unit,
|
|
403
441
|
force=args.force,
|
|
404
442
|
)
|
|
443
|
+
(repository / "jobs").mkdir(exist_ok=True)
|
|
405
444
|
config = generate_config(args, repository)
|
|
406
445
|
unit_path, changed = write_systemd_unit(
|
|
407
446
|
service_name=service_name,
|
|
@@ -418,4 +457,5 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
418
457
|
print(" sudo systemctl daemon-reload")
|
|
419
458
|
print(f" sudo systemctl enable {service_name}.service")
|
|
420
459
|
print(f" sudo systemctl restart {service_name}.service")
|
|
421
|
-
|
|
460
|
+
print(f" sudo systemctl status {service_name}.service --no-pager")
|
|
461
|
+
return 0
|
|
@@ -17,6 +17,7 @@ from typing import Any, Iterator
|
|
|
17
17
|
|
|
18
18
|
from ..context import ExecutionContext, ProgressSnapshot
|
|
19
19
|
from ..errors import ExecutionCancelled
|
|
20
|
+
from ..gpu_isolation import apply_gpu_isolation
|
|
20
21
|
from ..loader import load_algorithm
|
|
21
22
|
from ..models import AlgorithmRequest
|
|
22
23
|
from ..runner import AlgorithmRunner
|
|
@@ -125,6 +126,10 @@ def cancel_on_interrupt(
|
|
|
125
126
|
|
|
126
127
|
def execute(args: argparse.Namespace) -> int:
|
|
127
128
|
request = request_from_args(args)
|
|
129
|
+
request = replace(
|
|
130
|
+
request,
|
|
131
|
+
gpu_ids=apply_gpu_isolation(request.gpu_ids),
|
|
132
|
+
)
|
|
128
133
|
progress = TerminalProgress()
|
|
129
134
|
context = ExecutionContext(
|
|
130
135
|
f"cli-{id(args)}",
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from ..deployment import DEFAULT_CONFIG_NAME, DeploymentConfig
|
|
8
|
+
from ..gpu_isolation import apply_gpu_isolation
|
|
8
9
|
from ..loader import load_algorithm
|
|
9
10
|
from ..registration import PluginRegistrationAgent
|
|
10
11
|
from ..release import ReleaseManifest
|
|
@@ -78,9 +79,12 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
def execute(args: argparse.Namespace) -> int:
|
|
82
|
+
config: DeploymentConfig | None = None
|
|
83
|
+
service: dict[str, object] | None = None
|
|
81
84
|
if args.config:
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
# Apply the process mask before refreshing/loading the algorithm. A
|
|
86
|
+
# repository module may import CUDA during module import.
|
|
87
|
+
config = DeploymentConfig.load(args.config, refresh=False)
|
|
84
88
|
service = config.payload["service"]
|
|
85
89
|
args.algorithm = config.algorithm_reference
|
|
86
90
|
args.host = str(service["host"])
|
|
@@ -96,6 +100,11 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
96
100
|
elif not args.algorithm:
|
|
97
101
|
raise ValueError("algorithm is required unless --config is provided")
|
|
98
102
|
|
|
103
|
+
args.gpu_ids = apply_gpu_isolation(args.gpu_ids)
|
|
104
|
+
if config is not None:
|
|
105
|
+
config.refresh()
|
|
106
|
+
config.save()
|
|
107
|
+
|
|
99
108
|
algorithm_reference = str(args.algorithm)
|
|
100
109
|
algorithm = load_algorithm(algorithm_reference)
|
|
101
110
|
|
|
@@ -105,6 +114,10 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
105
114
|
release = ReleaseManifest.discover(algorithm, start=Path.cwd(), required=False)
|
|
106
115
|
manager = ExecutionManager(
|
|
107
116
|
AlgorithmRunner(algorithm),
|
|
117
|
+
default_parameters=(
|
|
118
|
+
service.get("defaultParameters", {})
|
|
119
|
+
if service is not None else None
|
|
120
|
+
),
|
|
108
121
|
max_concurrent_executions=args.max_concurrent_executions,
|
|
109
122
|
scratch_dir=args.scratch_dir,
|
|
110
123
|
gpu_ids=args.gpu_ids,
|
|
@@ -113,6 +126,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
113
126
|
retry_seconds=args.retry_seconds,
|
|
114
127
|
retry_jitter_ratio=args.retry_jitter_ratio,
|
|
115
128
|
release_manifest=release,
|
|
129
|
+
repository=(config.repository if config is not None else Path.cwd()),
|
|
116
130
|
)
|
|
117
131
|
registration = PluginRegistrationAgent.from_env(manager.manifest, manager.heartbeat)
|
|
118
132
|
if registration is not None and release is None:
|
|
@@ -145,6 +145,7 @@ class DeploymentConfig:
|
|
|
145
145
|
registration: dict[str, str] | None,
|
|
146
146
|
gpu_ids: list[int] | None = None,
|
|
147
147
|
environment: dict[str, str] | None = None,
|
|
148
|
+
default_parameters: dict[str, Any] | None = None,
|
|
148
149
|
) -> "DeploymentConfig":
|
|
149
150
|
repository = repository_root(root)
|
|
150
151
|
_make_repository_importable(repository)
|
|
@@ -154,6 +155,8 @@ class DeploymentConfig:
|
|
|
154
155
|
instance,
|
|
155
156
|
repository=repository,
|
|
156
157
|
)
|
|
158
|
+
if registration is not None:
|
|
159
|
+
registration = {**registration, "namespace": release.name}
|
|
157
160
|
release_path = release.save(
|
|
158
161
|
repository / "release-manifest.json"
|
|
159
162
|
)
|
|
@@ -188,6 +191,7 @@ class DeploymentConfig:
|
|
|
188
191
|
"scratchDir": _absolute(scratch_dir, repository),
|
|
189
192
|
"gpuIds": configured_gpu_ids,
|
|
190
193
|
"token": token or None,
|
|
194
|
+
"defaultParameters": dict(default_parameters or {}),
|
|
191
195
|
},
|
|
192
196
|
"registration": registration,
|
|
193
197
|
"environment": dict(sorted((environment or {}).items())),
|
|
@@ -271,6 +275,16 @@ class DeploymentConfig:
|
|
|
271
275
|
) or len(gpu_ids) != len(set(gpu_ids)):
|
|
272
276
|
raise RuntimeError("service gpuIds must be unique non-negative integers")
|
|
273
277
|
|
|
278
|
+
default_parameters = service.get("defaultParameters", {})
|
|
279
|
+
if not isinstance(default_parameters, dict):
|
|
280
|
+
raise RuntimeError("defaultParameters must be an object")
|
|
281
|
+
if any(not isinstance(key, str) for key in default_parameters):
|
|
282
|
+
raise RuntimeError("defaultParameters keys must be strings")
|
|
283
|
+
try:
|
|
284
|
+
json.dumps(default_parameters)
|
|
285
|
+
except (TypeError, ValueError) as exc:
|
|
286
|
+
raise RuntimeError("defaultParameters must be JSON serializable") from exc
|
|
287
|
+
|
|
274
288
|
def _validate_registration(self) -> None:
|
|
275
289
|
registration = self.payload.get("registration")
|
|
276
290
|
if registration is None:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def apply_gpu_isolation(gpu_ids: Sequence[int] | None) -> list[int]:
|
|
8
|
+
"""Mask physical GPUs for this process and return local GPU ordinals."""
|
|
9
|
+
if gpu_ids is None:
|
|
10
|
+
return []
|
|
11
|
+
|
|
12
|
+
physical_gpu_ids = list(gpu_ids)
|
|
13
|
+
if any(
|
|
14
|
+
isinstance(gpu_id, bool)
|
|
15
|
+
or not isinstance(gpu_id, int)
|
|
16
|
+
or gpu_id < 0
|
|
17
|
+
for gpu_id in physical_gpu_ids
|
|
18
|
+
) or len(physical_gpu_ids) != len(set(physical_gpu_ids)):
|
|
19
|
+
raise ValueError("gpu_ids must be unique non-negative integers")
|
|
20
|
+
|
|
21
|
+
if not physical_gpu_ids:
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
os.environ["CUDA_VISIBLE_DEVICES"] = ",".join(
|
|
25
|
+
str(gpu_id) for gpu_id in physical_gpu_ids
|
|
26
|
+
)
|
|
27
|
+
return list(range(len(physical_gpu_ids)))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .context import utc_now
|
|
11
|
+
|
|
12
|
+
_SAFE_NAME = re.compile(r"[^A-Za-z0-9._-]+")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_name(value: str) -> str:
|
|
16
|
+
cleaned = _SAFE_NAME.sub("_", str(value).strip()).strip("._")
|
|
17
|
+
return cleaned or "job"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExecutionLog:
|
|
21
|
+
"""Per-execution, line-buffered log sink attached to context.logger."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, *, log_dir: str | Path, job_id: str, execution_id: str, logger: logging.Logger) -> None:
|
|
24
|
+
directory = Path(log_dir).expanduser()
|
|
25
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
path = directory / f"{_safe_name(job_id)}.log"
|
|
27
|
+
if path.exists():
|
|
28
|
+
path = directory / f"{_safe_name(job_id)}-{_safe_name(execution_id)}.log"
|
|
29
|
+
self.path = path
|
|
30
|
+
self._stream = path.open("a", encoding="utf-8", buffering=1)
|
|
31
|
+
self._lock = threading.RLock()
|
|
32
|
+
self._handler = _FlushFileHandler(self._stream)
|
|
33
|
+
self._handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
|
|
34
|
+
self._handler.setLevel(logging.DEBUG)
|
|
35
|
+
self.logger = logger
|
|
36
|
+
self.logger.setLevel(logging.DEBUG)
|
|
37
|
+
self.logger.addHandler(self._handler)
|
|
38
|
+
self._closed = False
|
|
39
|
+
|
|
40
|
+
def write_event(self, event: str, **values: Any) -> None:
|
|
41
|
+
payload = {"event": event, "timestamp": utc_now(), **values}
|
|
42
|
+
with self._lock:
|
|
43
|
+
if self._closed:
|
|
44
|
+
return
|
|
45
|
+
self._stream.write(json.dumps(payload, ensure_ascii=False, default=str) + "\n")
|
|
46
|
+
self._stream.flush()
|
|
47
|
+
|
|
48
|
+
def close(self) -> None:
|
|
49
|
+
with self._lock:
|
|
50
|
+
if self._closed:
|
|
51
|
+
return
|
|
52
|
+
self._closed = True
|
|
53
|
+
self.logger.removeHandler(self._handler)
|
|
54
|
+
self._handler.close()
|
|
55
|
+
self._stream.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _FlushFileHandler(logging.Handler):
|
|
59
|
+
def __init__(self, stream: Any) -> None:
|
|
60
|
+
super().__init__()
|
|
61
|
+
self.stream = stream
|
|
62
|
+
self._lock = threading.RLock()
|
|
63
|
+
|
|
64
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
65
|
+
try:
|
|
66
|
+
with self._lock:
|
|
67
|
+
self.stream.write(self.format(record) + "\n")
|
|
68
|
+
self.stream.flush()
|
|
69
|
+
except Exception:
|
|
70
|
+
self.handleError(record)
|
|
71
|
+
|
|
72
|
+
def close(self) -> None:
|
|
73
|
+
try:
|
|
74
|
+
self.stream.flush()
|
|
75
|
+
finally:
|
|
76
|
+
super().close()
|