ls-algorithm-plugin-sdk 0.2.5__py3-none-any.whl → 0.3.0__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.
- algorithm_plugin_sdk/cli.py +12 -510
- algorithm_plugin_sdk/cli_impl/__init__.py +1 -0
- algorithm_plugin_sdk/cli_impl/configure.py +421 -0
- algorithm_plugin_sdk/cli_impl/parsing.py +45 -0
- algorithm_plugin_sdk/cli_impl/run.py +164 -0
- algorithm_plugin_sdk/cli_impl/serve.py +130 -0
- algorithm_plugin_sdk/deployment.py +50 -16
- algorithm_plugin_sdk/examples/example_algorithm.py +2 -2
- algorithm_plugin_sdk/examples/simulated_algorithm.py +1 -1
- algorithm_plugin_sdk/models.py +78 -99
- algorithm_plugin_sdk/release.py +81 -0
- algorithm_plugin_sdk/runner.py +23 -45
- algorithm_plugin_sdk/service.py +1 -2
- algorithm_plugin_sdk/webui/index.html +1 -1
- ls_algorithm_plugin_sdk-0.3.0.dist-info/METADATA +100 -0
- ls_algorithm_plugin_sdk-0.3.0.dist-info/RECORD +30 -0
- ls_algorithm_plugin_sdk-0.2.5.dist-info/METADATA +0 -87
- ls_algorithm_plugin_sdk-0.2.5.dist-info/RECORD +0 -25
- {ls_algorithm_plugin_sdk-0.2.5.dist-info → ls_algorithm_plugin_sdk-0.3.0.dist-info}/WHEEL +0 -0
- {ls_algorithm_plugin_sdk-0.2.5.dist-info → ls_algorithm_plugin_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
- {ls_algorithm_plugin_sdk-0.2.5.dist-info → ls_algorithm_plugin_sdk-0.3.0.dist-info}/top_level.txt +0 -0
algorithm_plugin_sdk/cli.py
CHANGED
|
@@ -1,536 +1,38 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import json
|
|
5
|
-
from dataclasses import replace
|
|
6
4
|
import logging
|
|
7
|
-
import os
|
|
8
|
-
import signal
|
|
9
|
-
import subprocess
|
|
10
5
|
import sys
|
|
11
|
-
import threading
|
|
12
|
-
import time
|
|
13
|
-
from contextlib import ExitStack, contextmanager, redirect_stderr, redirect_stdout
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
16
6
|
|
|
17
|
-
from .
|
|
18
|
-
from .
|
|
19
|
-
from .
|
|
20
|
-
from .loader import load_algorithm
|
|
21
|
-
from .models import AlgorithmRequest
|
|
22
|
-
from .registration import PluginRegistrationAgent
|
|
23
|
-
from .release import ReleaseManifest
|
|
24
|
-
from .runner import AlgorithmRunner
|
|
25
|
-
from .service import (
|
|
26
|
-
MAX_ATTEMPTS,
|
|
27
|
-
RETRY_JITTER_RATIO,
|
|
28
|
-
RETRY_SECONDS,
|
|
29
|
-
ExecutionManager,
|
|
30
|
-
run_server
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _env_bool(name: str, default: bool = False) -> bool:
|
|
35
|
-
value = os.getenv(name)
|
|
36
|
-
if value is None:
|
|
37
|
-
return default
|
|
38
|
-
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _json_object(value: str) -> dict[str, Any]:
|
|
42
|
-
try:
|
|
43
|
-
parsed = json.loads(value)
|
|
44
|
-
except json.JSONDecodeError as exc:
|
|
45
|
-
raise argparse.ArgumentTypeError(f"invalid JSON: {exc}") from exc
|
|
46
|
-
if not isinstance(parsed, dict):
|
|
47
|
-
raise argparse.ArgumentTypeError("value must be a JSON object")
|
|
48
|
-
return parsed
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _gpu_ids(value: str) -> list[int]:
|
|
52
|
-
if not value.strip():
|
|
53
|
-
return []
|
|
54
|
-
try:
|
|
55
|
-
return [int(item.strip()) for item in value.split(",")]
|
|
56
|
-
except ValueError as exc:
|
|
57
|
-
raise argparse.ArgumentTypeError(
|
|
58
|
-
"GPU IDs must be comma-separated integers"
|
|
59
|
-
) from exc
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _environment(value: str) -> tuple[str, str]:
|
|
63
|
-
name, separator, setting = value.partition("=")
|
|
64
|
-
if not separator or not name or not name.replace("_", "").isalnum():
|
|
65
|
-
raise argparse.ArgumentTypeError("environment must use NAME=VALUE")
|
|
66
|
-
if name.startswith("LDP_") or name.startswith("ALGORITHM_"):
|
|
67
|
-
raise argparse.ArgumentTypeError(
|
|
68
|
-
"SDK-managed LDP_/ALGORITHM_ settings must use dedicated options"
|
|
69
|
-
)
|
|
70
|
-
return name, setting
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _add_serve_options(parser: argparse.ArgumentParser) -> None:
|
|
74
|
-
parser.add_argument("algorithm", help="entry-point name or package.module:AlgorithmClass")
|
|
75
|
-
parser.add_argument("--host", default=os.getenv("ALGORITHM_HOST", "0.0.0.0"))
|
|
76
|
-
parser.add_argument(
|
|
77
|
-
"--port",
|
|
78
|
-
type=int,
|
|
79
|
-
default=int(os.getenv("ALGORITHM_PORT", os.getenv("LDP_PLUGIN_PORT", "9000"))),
|
|
80
|
-
)
|
|
81
|
-
parser.add_argument(
|
|
82
|
-
"--max-concurrent-executions",
|
|
83
|
-
type=int,
|
|
84
|
-
default=int(
|
|
85
|
-
os.getenv(
|
|
86
|
-
"ALGORITHM_MAX_CONCURRENT_EXECUTIONS",
|
|
87
|
-
os.getenv("LDP_PLUGIN_MAX_CONCURRENCY", "1"),
|
|
88
|
-
)
|
|
89
|
-
),
|
|
90
|
-
)
|
|
91
|
-
parser.add_argument("--scratch-dir", default=os.getenv("ALGORITHM_SCRATCH_DIR"))
|
|
92
|
-
parser.add_argument(
|
|
93
|
-
"--gpu-ids", type=_gpu_ids, default=None,
|
|
94
|
-
help="GPU IDs injected into every service execution",
|
|
95
|
-
)
|
|
96
|
-
parser.add_argument(
|
|
97
|
-
"--max-attempts",
|
|
98
|
-
type=int,
|
|
99
|
-
default=int(os.getenv("ALGORITHM_MAX_ATTEMPTS", str(MAX_ATTEMPTS))),
|
|
100
|
-
)
|
|
101
|
-
parser.add_argument(
|
|
102
|
-
"--retry-seconds",
|
|
103
|
-
type=float,
|
|
104
|
-
default=float(os.getenv("ALGORITHM_RETRY_SECONDS", str(RETRY_SECONDS))),
|
|
105
|
-
)
|
|
106
|
-
parser.add_argument(
|
|
107
|
-
"--retry-jitter-ratio",
|
|
108
|
-
type=float,
|
|
109
|
-
default=float(
|
|
110
|
-
os.getenv("ALGORITHM_RETRY_JITTER_RATIO", str(RETRY_JITTER_RATIO))
|
|
111
|
-
),
|
|
112
|
-
)
|
|
113
|
-
parser.add_argument(
|
|
114
|
-
"--webui",
|
|
115
|
-
action=argparse.BooleanOptionalAction,
|
|
116
|
-
default=_env_bool("ALGORITHM_WEBUI"),
|
|
117
|
-
help="serve the SDK processing UI at /ui/",
|
|
118
|
-
)
|
|
7
|
+
from .cli_impl import configure as configure_command
|
|
8
|
+
from .cli_impl import run as run_command
|
|
9
|
+
from .cli_impl import serve as serve_command
|
|
119
10
|
|
|
120
11
|
|
|
121
12
|
def build_parser() -> argparse.ArgumentParser:
|
|
122
13
|
parser = argparse.ArgumentParser(prog="algorithm-plugin")
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
run_parser = subparsers.add_parser("run", help="run datasets once")
|
|
126
|
-
run_parser.add_argument("algorithm", help="entry-point name or package.module:AlgorithmClass")
|
|
127
|
-
run_parser.add_argument("--input", action="append", type=_json_object, default=[])
|
|
128
|
-
run_parser.add_argument("--merge", action="store_true", default=None)
|
|
129
|
-
run_parser.add_argument("--gpu-ids", type=_gpu_ids)
|
|
130
|
-
run_parser.add_argument("--parameters", type=_json_object)
|
|
131
|
-
run_parser.add_argument("--request-file", type=Path)
|
|
132
|
-
run_parser.add_argument("--scratch-dir")
|
|
133
|
-
run_parser.add_argument("--algorithm-log", type=Path)
|
|
134
|
-
|
|
135
|
-
serve_parser = subparsers.add_parser("serve", help="run the generated HTTP service")
|
|
136
|
-
_add_serve_options(serve_parser)
|
|
14
|
+
subs = parser.add_subparsers(dest="command", required=True)
|
|
137
15
|
|
|
138
|
-
|
|
139
|
-
"
|
|
140
|
-
)
|
|
141
|
-
config_parser.add_argument("algorithm", nargs="?", help="normally auto-detected")
|
|
142
|
-
config_parser.add_argument("--output", type=Path, default=Path(DEFAULT_CONFIG_NAME))
|
|
143
|
-
config_parser.add_argument("--repository", type=Path, default=Path.cwd())
|
|
144
|
-
config_parser.add_argument("--host", default="0.0.0.0")
|
|
145
|
-
config_parser.add_argument("--port", type=int, default=9000)
|
|
146
|
-
config_parser.add_argument(
|
|
147
|
-
"--webui", action=argparse.BooleanOptionalAction, default=True
|
|
148
|
-
)
|
|
149
|
-
config_parser.add_argument("--max-concurrency", type=int, default=1)
|
|
150
|
-
config_parser.add_argument("--scratch-dir")
|
|
151
|
-
config_parser.add_argument(
|
|
152
|
-
"--gpu-ids", type=_gpu_ids, default=[],
|
|
153
|
-
help="GPU IDs injected into every service execution",
|
|
16
|
+
run_command.configure_parser(
|
|
17
|
+
subs.add_parser("run", help="run datasets once")
|
|
154
18
|
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
config_parser.add_argument("--api-key", default=os.getenv("LDP_INTERNAL_API_KEY"))
|
|
158
|
-
config_parser.add_argument("--public-url", default=os.getenv("LDP_PLUGIN_PUBLIC_URL"))
|
|
159
|
-
config_parser.add_argument("--input-root", default=os.getenv("LDP_JOB_INPUT_ROOT"))
|
|
160
|
-
config_parser.add_argument(
|
|
161
|
-
"--workspace-root", default=os.getenv("LDP_JOB_WORKSPACE_ROOT")
|
|
19
|
+
serve_command.configure_parser(
|
|
20
|
+
subs.add_parser("serve", help="run the generated HTTP service")
|
|
162
21
|
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
config_parser.add_argument("--namespace", default=os.getenv("POD_NAMESPACE"))
|
|
166
|
-
config_parser.add_argument("--node-name", default=os.getenv("NODE_NAME"))
|
|
167
|
-
config_parser.add_argument(
|
|
168
|
-
"--local",
|
|
169
|
-
action="store_true",
|
|
170
|
-
help="generate a local config without compute registration",
|
|
22
|
+
configure_command.configure_parser(
|
|
23
|
+
subs.add_parser("configure", help="configure an existing Algorithm repository for systemd")
|
|
171
24
|
)
|
|
172
|
-
config_parser.add_argument(
|
|
173
|
-
"--env", action="append", type=_environment, default=[], metavar="NAME=VALUE"
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
start_parser = subparsers.add_parser("start", help="start the configured service")
|
|
177
|
-
start_parser.add_argument("--config", type=Path, default=Path(DEFAULT_CONFIG_NAME))
|
|
178
|
-
start_parser.add_argument(
|
|
179
|
-
"--foreground", action="store_true", help="stay in foreground for systemd"
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
stop_parser = subparsers.add_parser("stop", help="stop the configured service")
|
|
183
|
-
stop_parser.add_argument("--config", type=Path, default=Path(DEFAULT_CONFIG_NAME))
|
|
184
|
-
stop_parser.add_argument("--timeout", type=float, default=30.0)
|
|
185
25
|
return parser
|
|
186
26
|
|
|
187
27
|
|
|
188
|
-
def _request_from_args(args: argparse.Namespace) -> AlgorithmRequest:
|
|
189
|
-
values: dict[str, Any] = {}
|
|
190
|
-
if args.request_file is not None:
|
|
191
|
-
try:
|
|
192
|
-
values = json.loads(args.request_file.read_text(encoding="utf-8"))
|
|
193
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
194
|
-
raise ValueError(f"cannot read request file: {exc}") from exc
|
|
195
|
-
if not isinstance(values, dict):
|
|
196
|
-
raise ValueError("request file must contain a JSON object")
|
|
197
|
-
if args.input:
|
|
198
|
-
values["input"] = args.input
|
|
199
|
-
if args.merge is not None:
|
|
200
|
-
values["merge"] = args.merge
|
|
201
|
-
if args.gpu_ids is not None:
|
|
202
|
-
values["gpuIds"] = args.gpu_ids
|
|
203
|
-
if args.parameters is not None:
|
|
204
|
-
values["parameters"] = args.parameters
|
|
205
|
-
request = AlgorithmRequest.from_dict(values)
|
|
206
|
-
if args.gpu_ids is not None:
|
|
207
|
-
request = replace(request, gpu_ids=args.gpu_ids)
|
|
208
|
-
return request
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class TerminalProgress:
|
|
212
|
-
def __init__(self, stream: Any = None) -> None:
|
|
213
|
-
self.stream = stream or sys.stderr
|
|
214
|
-
self.enabled = bool(getattr(self.stream, "isatty", lambda: False)())
|
|
215
|
-
self._line_count = 0
|
|
216
|
-
self._lock = threading.Lock()
|
|
217
|
-
|
|
218
|
-
def update(self, snapshot: ProgressSnapshot) -> None:
|
|
219
|
-
if not self.enabled:
|
|
220
|
-
return
|
|
221
|
-
lines = []
|
|
222
|
-
for dataset, progress in snapshot.datasets.items():
|
|
223
|
-
lines.append(self._format_line(dataset, progress))
|
|
224
|
-
lines.extend(
|
|
225
|
-
self._format_line(f" {episode}", episode_progress)
|
|
226
|
-
for episode, episode_progress in progress.episodes.items()
|
|
227
|
-
)
|
|
228
|
-
with self._lock:
|
|
229
|
-
if self._line_count:
|
|
230
|
-
self.stream.write(f"\x1b[{self._line_count}F")
|
|
231
|
-
for line in lines:
|
|
232
|
-
self.stream.write(f"\x1b[2K{line}\n")
|
|
233
|
-
self.stream.flush()
|
|
234
|
-
self._line_count = len(lines)
|
|
235
|
-
|
|
236
|
-
def close(self) -> None:
|
|
237
|
-
if self.enabled:
|
|
238
|
-
self.stream.flush()
|
|
239
|
-
|
|
240
|
-
@staticmethod
|
|
241
|
-
def _format_line(dataset: str, progress: Any) -> str:
|
|
242
|
-
name = dataset.rstrip("/") or dataset
|
|
243
|
-
percent = " --%" if progress.percent is None else f"{progress.percent:5.1f}%"
|
|
244
|
-
filled = 0 if progress.percent is None else round(progress.percent / 5)
|
|
245
|
-
bar = f"[{'#' * filled}{'-' * (20 - filled)}]"
|
|
246
|
-
message = f" {progress.message}" if progress.message else ""
|
|
247
|
-
return f"{name:24.24} {bar} {percent} {progress.stage:16} {progress.status:9}{message}"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
@contextmanager
|
|
251
|
-
def _cancel_on_interrupt(context: ExecutionContext, stream: Any):
|
|
252
|
-
interrupted = False
|
|
253
|
-
previous_handler = signal.getsignal(signal.SIGINT)
|
|
254
|
-
|
|
255
|
-
def handle_interrupt(_signum: int, _frame: Any) -> None:
|
|
256
|
-
nonlocal interrupted
|
|
257
|
-
if interrupted:
|
|
258
|
-
raise KeyboardInterrupt
|
|
259
|
-
interrupted = True
|
|
260
|
-
context.cancel("interrupted by user")
|
|
261
|
-
stream.write("\nCancellation requested; waiting for the algorithm to stop.\n")
|
|
262
|
-
stream.flush()
|
|
263
|
-
|
|
264
|
-
signal.signal(signal.SIGINT, handle_interrupt)
|
|
265
|
-
try:
|
|
266
|
-
yield
|
|
267
|
-
finally:
|
|
268
|
-
signal.signal(signal.SIGINT, previous_handler)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def _run(args: argparse.Namespace) -> int:
|
|
272
|
-
request = _request_from_args(args)
|
|
273
|
-
progress = TerminalProgress()
|
|
274
|
-
context = ExecutionContext(
|
|
275
|
-
f"cli-{id(args)}",
|
|
276
|
-
[item.dataset for item in request.inputs],
|
|
277
|
-
scratch_dir=args.scratch_dir,
|
|
278
|
-
progress_callback=progress.update,
|
|
279
|
-
)
|
|
280
|
-
try:
|
|
281
|
-
with ExitStack() as stack:
|
|
282
|
-
if args.algorithm_log is not None:
|
|
283
|
-
args.algorithm_log.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
-
stream = stack.enter_context(
|
|
285
|
-
args.algorithm_log.open("w", encoding="utf-8", buffering=1)
|
|
286
|
-
)
|
|
287
|
-
stack.enter_context(redirect_stdout(stream))
|
|
288
|
-
stack.enter_context(redirect_stderr(stream))
|
|
289
|
-
runner = AlgorithmRunner(load_algorithm(args.algorithm))
|
|
290
|
-
try:
|
|
291
|
-
with _cancel_on_interrupt(context, progress.stream):
|
|
292
|
-
result = runner.run(request, context=context)
|
|
293
|
-
except ExecutionCancelled as exc:
|
|
294
|
-
print(f"CANCELLED: {exc}", file=progress.stream)
|
|
295
|
-
return 130
|
|
296
|
-
finally:
|
|
297
|
-
runner.close()
|
|
298
|
-
finally:
|
|
299
|
-
progress.close()
|
|
300
|
-
print(json.dumps({"type": "result", **result.to_dict()}, ensure_ascii=False))
|
|
301
|
-
return 0 if result.status == "succeeded" else 1
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def _serve(args: argparse.Namespace) -> int:
|
|
305
|
-
algorithm = load_algorithm(args.algorithm)
|
|
306
|
-
|
|
307
|
-
def create_runner() -> AlgorithmRunner:
|
|
308
|
-
return AlgorithmRunner(load_algorithm(args.algorithm))
|
|
309
|
-
|
|
310
|
-
release = ReleaseManifest.discover(algorithm, start=Path.cwd(), required=False)
|
|
311
|
-
manager = ExecutionManager(
|
|
312
|
-
AlgorithmRunner(algorithm),
|
|
313
|
-
max_concurrent_executions=args.max_concurrent_executions,
|
|
314
|
-
scratch_dir=args.scratch_dir,
|
|
315
|
-
gpu_ids=args.gpu_ids,
|
|
316
|
-
runner_factory=create_runner,
|
|
317
|
-
max_attempts=args.max_attempts,
|
|
318
|
-
retry_seconds=args.retry_seconds,
|
|
319
|
-
retry_jitter_ratio=args.retry_jitter_ratio,
|
|
320
|
-
release_manifest=release,
|
|
321
|
-
)
|
|
322
|
-
registration = PluginRegistrationAgent.from_env(manager.manifest, manager.heartbeat)
|
|
323
|
-
if registration is not None and release is None:
|
|
324
|
-
raise RuntimeError(
|
|
325
|
-
"compute registration requires release-manifest.json in the Algorithm repository"
|
|
326
|
-
)
|
|
327
|
-
run_server(
|
|
328
|
-
manager,
|
|
329
|
-
host=args.host,
|
|
330
|
-
port=args.port,
|
|
331
|
-
webui=args.webui,
|
|
332
|
-
token=os.getenv("LDP_PLUGIN_TOKEN") or None,
|
|
333
|
-
lifecycle=registration,
|
|
334
|
-
)
|
|
335
|
-
return 0
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def _config(args: argparse.Namespace) -> int:
|
|
339
|
-
registration = None
|
|
340
|
-
if not args.local:
|
|
341
|
-
supplied = {
|
|
342
|
-
"computeUrl": args.compute_url,
|
|
343
|
-
"apiKey": args.api_key,
|
|
344
|
-
"publicUrl": args.public_url,
|
|
345
|
-
"inputRoot": args.input_root,
|
|
346
|
-
"workspaceRoot": args.workspace_root,
|
|
347
|
-
"cluster": args.cluster,
|
|
348
|
-
"namespace": args.namespace,
|
|
349
|
-
"nodeName": args.node_name,
|
|
350
|
-
}
|
|
351
|
-
missing = [name for name, value in supplied.items() if not str(value or "").strip()]
|
|
352
|
-
if missing:
|
|
353
|
-
raise ValueError(
|
|
354
|
-
"deployment registration requires "
|
|
355
|
-
+ ", ".join(missing)
|
|
356
|
-
+ "; use --local to disable registration"
|
|
357
|
-
)
|
|
358
|
-
registration = {name: str(value) for name, value in supplied.items()}
|
|
359
|
-
if args.instance_key:
|
|
360
|
-
registration["instanceKey"] = args.instance_key
|
|
361
|
-
root = repository_root(args.repository)
|
|
362
|
-
output = args.output if args.output.is_absolute() else root / args.output
|
|
363
|
-
config = DeploymentConfig.generate(
|
|
364
|
-
output=output,
|
|
365
|
-
root=root,
|
|
366
|
-
algorithm=args.algorithm,
|
|
367
|
-
host=args.host,
|
|
368
|
-
port=args.port,
|
|
369
|
-
webui=args.webui,
|
|
370
|
-
max_concurrency=args.max_concurrency,
|
|
371
|
-
scratch_dir=args.scratch_dir,
|
|
372
|
-
token=args.service_token,
|
|
373
|
-
gpu_ids=args.gpu_ids,
|
|
374
|
-
registration=registration,
|
|
375
|
-
environment=dict(args.env),
|
|
376
|
-
)
|
|
377
|
-
config.save()
|
|
378
|
-
identity = config.payload["algorithm"]
|
|
379
|
-
print(
|
|
380
|
-
f"Generated {config.path} for {identity['type']}@{identity['version']} "
|
|
381
|
-
f"({identity['implementationDigest']})"
|
|
382
|
-
)
|
|
383
|
-
return 0
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def _proc_start_time(pid: int) -> str | None:
|
|
387
|
-
try:
|
|
388
|
-
fields = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8").split()
|
|
389
|
-
except OSError:
|
|
390
|
-
return None
|
|
391
|
-
return fields[21] if len(fields) > 21 else None
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def _read_process_state(config: DeploymentConfig) -> dict[str, Any] | None:
|
|
395
|
-
try:
|
|
396
|
-
state = json.loads(config.pid_file.read_text(encoding="utf-8"))
|
|
397
|
-
except FileNotFoundError:
|
|
398
|
-
return None
|
|
399
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
400
|
-
raise RuntimeError(f"cannot read PID state {config.pid_file}: {exc}") from exc
|
|
401
|
-
if not isinstance(state, dict) or not isinstance(state.get("pid"), int):
|
|
402
|
-
raise RuntimeError(f"invalid PID state: {config.pid_file}")
|
|
403
|
-
return state
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def _is_managed_process(config: DeploymentConfig, state: dict[str, Any]) -> bool:
|
|
407
|
-
pid = int(state["pid"])
|
|
408
|
-
if _proc_start_time(pid) != str(state.get("startTime")):
|
|
409
|
-
return False
|
|
410
|
-
try:
|
|
411
|
-
command = Path(f"/proc/{pid}/cmdline").read_bytes().replace(b"\0", b" ").decode()
|
|
412
|
-
except OSError:
|
|
413
|
-
return False
|
|
414
|
-
return (
|
|
415
|
-
"algorithm_plugin_sdk.cli" in command
|
|
416
|
-
and " serve " in f" {command} "
|
|
417
|
-
and config.algorithm_reference in command
|
|
418
|
-
)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
def _start(args: argparse.Namespace) -> int:
|
|
422
|
-
config = DeploymentConfig.load(args.config)
|
|
423
|
-
config.save() # Persist any auto-refreshed release identity.
|
|
424
|
-
service = config.payload["service"]
|
|
425
|
-
command = [
|
|
426
|
-
sys.executable,
|
|
427
|
-
"-m",
|
|
428
|
-
"algorithm_plugin_sdk.cli",
|
|
429
|
-
"serve",
|
|
430
|
-
config.algorithm_reference,
|
|
431
|
-
"--host",
|
|
432
|
-
str(service["host"]),
|
|
433
|
-
"--port",
|
|
434
|
-
str(service["port"]),
|
|
435
|
-
"--max-concurrent-executions",
|
|
436
|
-
str(service["maxConcurrency"]),
|
|
437
|
-
]
|
|
438
|
-
command.append("--webui" if service.get("webui") else "--no-webui")
|
|
439
|
-
if service.get("scratchDir"):
|
|
440
|
-
command.extend(("--scratch-dir", str(service["scratchDir"])))
|
|
441
|
-
if "gpuIds" in service:
|
|
442
|
-
command.extend(("--gpu-ids", ",".join(str(value) for value in service["gpuIds"])))
|
|
443
|
-
|
|
444
|
-
if args.foreground:
|
|
445
|
-
os.chdir(config.repository)
|
|
446
|
-
os.execve(sys.executable, command, config.process_environment())
|
|
447
|
-
|
|
448
|
-
existing = _read_process_state(config)
|
|
449
|
-
if existing and _is_managed_process(config, existing):
|
|
450
|
-
print(f"Algorithm service is already running (pid {existing['pid']})")
|
|
451
|
-
return 0
|
|
452
|
-
if existing:
|
|
453
|
-
config.pid_file.unlink(missing_ok=True)
|
|
454
|
-
|
|
455
|
-
config.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
456
|
-
with config.log_file.open("ab", buffering=0) as log:
|
|
457
|
-
process = subprocess.Popen(
|
|
458
|
-
command,
|
|
459
|
-
cwd=config.repository,
|
|
460
|
-
env=config.process_environment(),
|
|
461
|
-
stdin=subprocess.DEVNULL,
|
|
462
|
-
stdout=log,
|
|
463
|
-
stderr=subprocess.STDOUT,
|
|
464
|
-
start_new_session=True,
|
|
465
|
-
)
|
|
466
|
-
time.sleep(0.25)
|
|
467
|
-
status = process.poll()
|
|
468
|
-
if status is not None:
|
|
469
|
-
raise RuntimeError(
|
|
470
|
-
f"algorithm service exited with status {status}; see {config.log_file}"
|
|
471
|
-
)
|
|
472
|
-
start_time = _proc_start_time(process.pid)
|
|
473
|
-
if start_time is None:
|
|
474
|
-
raise RuntimeError("algorithm service disappeared during startup")
|
|
475
|
-
config.pid_file.write_text(
|
|
476
|
-
json.dumps(
|
|
477
|
-
{
|
|
478
|
-
"pid": process.pid,
|
|
479
|
-
"startTime": start_time,
|
|
480
|
-
"algorithm": config.algorithm_reference,
|
|
481
|
-
"config": str(config.path),
|
|
482
|
-
},
|
|
483
|
-
indent=2,
|
|
484
|
-
)
|
|
485
|
-
+ "\n",
|
|
486
|
-
encoding="utf-8",
|
|
487
|
-
)
|
|
488
|
-
print(f"Started algorithm service (pid {process.pid}); log: {config.log_file}")
|
|
489
|
-
return 0
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
def _stop(args: argparse.Namespace) -> int:
|
|
493
|
-
if args.timeout < 0:
|
|
494
|
-
raise ValueError("timeout cannot be negative")
|
|
495
|
-
config = DeploymentConfig.load(args.config, refresh=False)
|
|
496
|
-
state = _read_process_state(config)
|
|
497
|
-
if state is None:
|
|
498
|
-
print("Algorithm service is not running")
|
|
499
|
-
return 0
|
|
500
|
-
if not _is_managed_process(config, state):
|
|
501
|
-
config.pid_file.unlink(missing_ok=True)
|
|
502
|
-
print("Algorithm service is not running; removed stale PID state")
|
|
503
|
-
return 0
|
|
504
|
-
pid = int(state["pid"])
|
|
505
|
-
os.kill(pid, signal.SIGTERM)
|
|
506
|
-
deadline = time.monotonic() + args.timeout
|
|
507
|
-
while _is_managed_process(config, state) and time.monotonic() < deadline:
|
|
508
|
-
time.sleep(0.1)
|
|
509
|
-
if _is_managed_process(config, state):
|
|
510
|
-
os.kill(pid, signal.SIGKILL)
|
|
511
|
-
while _is_managed_process(config, state):
|
|
512
|
-
time.sleep(0.05)
|
|
513
|
-
config.pid_file.unlink(missing_ok=True)
|
|
514
|
-
print(f"Stopped algorithm service (pid {pid})")
|
|
515
|
-
return 0
|
|
516
|
-
|
|
517
|
-
|
|
518
28
|
def main(argv: list[str] | None = None) -> int:
|
|
519
29
|
logging.basicConfig(
|
|
520
30
|
level=logging.INFO,
|
|
521
31
|
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
|
522
32
|
)
|
|
523
|
-
|
|
524
|
-
args = parser.parse_args(argv)
|
|
525
|
-
handlers = {
|
|
526
|
-
"run": _run,
|
|
527
|
-
"serve": _serve,
|
|
528
|
-
"config": _config,
|
|
529
|
-
"start": _start,
|
|
530
|
-
"stop": _stop,
|
|
531
|
-
}
|
|
33
|
+
args = build_parser().parse_args(argv)
|
|
532
34
|
try:
|
|
533
|
-
return
|
|
35
|
+
return args.handler(args)
|
|
534
36
|
except KeyboardInterrupt:
|
|
535
37
|
return 130
|
|
536
38
|
except Exception as exc:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Implementation modules for the algorithm-plugin command line interface."""
|