ls-algorithm-plugin-sdk 0.3.1__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.1 → ls_algorithm_plugin_sdk-0.3.3}/PKG-INFO +1 -1
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/pyproject.toml +1 -1
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/configure.py +32 -5
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/run.py +5 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/serve.py +16 -2
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/deployment.py +12 -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.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/service.py +80 -2
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/PKG-INFO +1 -1
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/SOURCES.txt +3 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_configure.py +34 -2
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_deployment.py +5 -0
- ls_algorithm_plugin_sdk-0.3.3/tests/test_gpu_isolation.py +38 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_runner.py +26 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_service.py +20 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/README.md +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/setup.cfg +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli_impl/parsing.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/context.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/errors.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/examples/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/examples/example_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/examples/simulated_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/loader.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/runner.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/app.css +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/app.js +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui/index.html +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/webui_app.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → 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.1 → 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.1 → ls_algorithm_plugin_sdk-0.3.3}/src/ls_algorithm_plugin_sdk.egg-info/requires.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → 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.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_sdk_automation.py +0 -0
|
@@ -16,7 +16,7 @@ from ..deployment import (
|
|
|
16
16
|
repository_root,
|
|
17
17
|
stable_instance_key,
|
|
18
18
|
)
|
|
19
|
-
from .parsing import environment, gpu_ids
|
|
19
|
+
from .parsing import environment, gpu_ids, json_object
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
SYSTEMD_UNIT_DIR = Path("/etc/systemd/system")
|
|
@@ -56,7 +56,11 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
56
56
|
action=argparse.BooleanOptionalAction,
|
|
57
57
|
default=True,
|
|
58
58
|
)
|
|
59
|
-
parser.add_argument(
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--max-concurrency",
|
|
61
|
+
type=int,
|
|
62
|
+
help="maximum concurrent executions (default: GPU count, or 1)",
|
|
63
|
+
)
|
|
60
64
|
parser.add_argument("--scratch-dir")
|
|
61
65
|
parser.add_argument(
|
|
62
66
|
"--gpu-ids",
|
|
@@ -64,6 +68,12 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
64
68
|
default=[],
|
|
65
69
|
help="GPU IDs injected into every service execution",
|
|
66
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
|
+
)
|
|
67
77
|
parser.add_argument("--service-token")
|
|
68
78
|
parser.add_argument(
|
|
69
79
|
"--compute-url",
|
|
@@ -139,10 +149,18 @@ def render_systemd_unit(
|
|
|
139
149
|
repository: Path,
|
|
140
150
|
config_path: Path,
|
|
141
151
|
service_name: str,
|
|
152
|
+
gpu_ids: list[int] | None = None,
|
|
142
153
|
) -> str:
|
|
143
154
|
root = repository.resolve()
|
|
155
|
+
working_directory = root / "jobs"
|
|
144
156
|
launcher = root / ".venv" / "bin" / "algorithm-plugin"
|
|
145
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
|
+
)
|
|
146
164
|
return "\n".join(
|
|
147
165
|
(
|
|
148
166
|
"[Unit]",
|
|
@@ -153,12 +171,13 @@ def render_systemd_unit(
|
|
|
153
171
|
"[Service]",
|
|
154
172
|
"Type=simple",
|
|
155
173
|
"User=root",
|
|
156
|
-
f"WorkingDirectory={_escape_systemd_path(
|
|
174
|
+
f"WorkingDirectory={_escape_systemd_path(working_directory)}",
|
|
157
175
|
(
|
|
158
176
|
f"ExecStart={_quote_systemd_path(launcher)} "
|
|
159
177
|
f"serve --config {_quote_systemd_path(config)}"
|
|
160
178
|
),
|
|
161
|
-
|
|
179
|
+
*environment,
|
|
180
|
+
"LimitNOFILE=524288",
|
|
162
181
|
"Restart=on-failure",
|
|
163
182
|
"RestartSec=5s",
|
|
164
183
|
"",
|
|
@@ -376,6 +395,11 @@ def generate_config(
|
|
|
376
395
|
args: argparse.Namespace,
|
|
377
396
|
repository: Path,
|
|
378
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
|
+
)
|
|
379
403
|
config = DeploymentConfig.generate(
|
|
380
404
|
output=args.config_output,
|
|
381
405
|
root=repository,
|
|
@@ -383,12 +407,13 @@ def generate_config(
|
|
|
383
407
|
host=args.host,
|
|
384
408
|
port=args.port,
|
|
385
409
|
webui=args.webui,
|
|
386
|
-
max_concurrency=
|
|
410
|
+
max_concurrency=max_concurrency,
|
|
387
411
|
scratch_dir=args.scratch_dir,
|
|
388
412
|
token=args.service_token,
|
|
389
413
|
registration=registration_from_args(args, repository),
|
|
390
414
|
gpu_ids=args.gpu_ids,
|
|
391
415
|
environment=dict(args.env),
|
|
416
|
+
default_parameters=args.default_parameters,
|
|
392
417
|
)
|
|
393
418
|
config.save()
|
|
394
419
|
return config
|
|
@@ -407,6 +432,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
407
432
|
repository=repository,
|
|
408
433
|
config_path=config_path,
|
|
409
434
|
service_name=service_name,
|
|
435
|
+
gpu_ids=args.gpu_ids,
|
|
410
436
|
)
|
|
411
437
|
|
|
412
438
|
inspect_systemd_unit(
|
|
@@ -414,6 +440,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
414
440
|
content=unit,
|
|
415
441
|
force=args.force,
|
|
416
442
|
)
|
|
443
|
+
(repository / "jobs").mkdir(exist_ok=True)
|
|
417
444
|
config = generate_config(args, repository)
|
|
418
445
|
unit_path, changed = write_systemd_unit(
|
|
419
446
|
service_name=service_name,
|
|
@@ -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)
|
|
@@ -190,6 +191,7 @@ class DeploymentConfig:
|
|
|
190
191
|
"scratchDir": _absolute(scratch_dir, repository),
|
|
191
192
|
"gpuIds": configured_gpu_ids,
|
|
192
193
|
"token": token or None,
|
|
194
|
+
"defaultParameters": dict(default_parameters or {}),
|
|
193
195
|
},
|
|
194
196
|
"registration": registration,
|
|
195
197
|
"environment": dict(sorted((environment or {}).items())),
|
|
@@ -273,6 +275,16 @@ class DeploymentConfig:
|
|
|
273
275
|
) or len(gpu_ids) != len(set(gpu_ids)):
|
|
274
276
|
raise RuntimeError("service gpuIds must be unique non-negative integers")
|
|
275
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
|
+
|
|
276
288
|
def _validate_registration(self) -> None:
|
|
277
289
|
registration = self.payload.get("registration")
|
|
278
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()
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/service.py
RENAMED
|
@@ -10,12 +10,14 @@ import uuid
|
|
|
10
10
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
11
11
|
from contextlib import asynccontextmanager
|
|
12
12
|
from dataclasses import dataclass, field, replace
|
|
13
|
+
from pathlib import Path
|
|
13
14
|
from typing import Any, Callable
|
|
14
15
|
|
|
15
16
|
from .context import ExecutionContext, ProgressSnapshot, utc_now
|
|
16
17
|
from .errors import (ExecutionCancelled, ExecutionNotFinished,
|
|
17
18
|
ExecutionNotFound, IdempotencyConflict)
|
|
18
19
|
from .models import AlgorithmRequest, AlgorithmResult
|
|
20
|
+
from .log_manager import ExecutionLog
|
|
19
21
|
from .release import ReleaseManifest
|
|
20
22
|
from .runner import AlgorithmRunner
|
|
21
23
|
from .webui_app import announce_webui, mount_webui
|
|
@@ -34,6 +36,8 @@ logger = logging.getLogger(__name__)
|
|
|
34
36
|
class ExecutionRecord:
|
|
35
37
|
execution_id: str
|
|
36
38
|
request: AlgorithmRequest
|
|
39
|
+
job_id: str
|
|
40
|
+
task_id: str
|
|
37
41
|
accepted_at: str
|
|
38
42
|
state: str = "accepted"
|
|
39
43
|
started_at: str | None = None
|
|
@@ -41,6 +45,7 @@ class ExecutionRecord:
|
|
|
41
45
|
result: AlgorithmResult | None = None
|
|
42
46
|
error: str | None = None
|
|
43
47
|
context: ExecutionContext | None = None
|
|
48
|
+
log: ExecutionLog | None = None
|
|
44
49
|
future: Future[None] | None = None
|
|
45
50
|
attempt: int = 0
|
|
46
51
|
max_attempts: int = 1
|
|
@@ -113,8 +118,11 @@ class ExecutionManager:
|
|
|
113
118
|
self,
|
|
114
119
|
runner: AlgorithmRunner,
|
|
115
120
|
*,
|
|
121
|
+
default_parameters: dict[str, Any] | None = None,
|
|
116
122
|
max_concurrent_executions: int = 1,
|
|
117
123
|
scratch_dir: str | None = None,
|
|
124
|
+
repository: str | Path | None = None,
|
|
125
|
+
log_dir: str | Path | None = None,
|
|
118
126
|
gpu_ids: list[int] | None = None,
|
|
119
127
|
runner_factory: RunnerFactory | None = None,
|
|
120
128
|
max_attempts: int = MAX_ATTEMPTS,
|
|
@@ -144,7 +152,15 @@ class ExecutionManager:
|
|
|
144
152
|
self._runner_factory = runner_factory or (
|
|
145
153
|
lambda: AlgorithmRunner(algorithm_class())
|
|
146
154
|
)
|
|
155
|
+
if default_parameters is not None and not isinstance(default_parameters, dict):
|
|
156
|
+
raise ValueError("default_parameters must be a JSON object")
|
|
157
|
+
self.default_parameters = dict(default_parameters or {})
|
|
147
158
|
self.scratch_dir = scratch_dir
|
|
159
|
+
self.log_dir = (
|
|
160
|
+
Path(log_dir).expanduser()
|
|
161
|
+
if log_dir is not None
|
|
162
|
+
else (Path(repository).expanduser() / "logs" if repository is not None else None)
|
|
163
|
+
)
|
|
148
164
|
self.gpu_ids = configured_gpu_ids
|
|
149
165
|
self.max_concurrent_executions = max_concurrent_executions
|
|
150
166
|
self.max_attempts = max_attempts
|
|
@@ -180,9 +196,16 @@ class ExecutionManager:
|
|
|
180
196
|
request: AlgorithmRequest,
|
|
181
197
|
*,
|
|
182
198
|
idempotency_key: str | None = None,
|
|
199
|
+
job_id: str | None = None,
|
|
200
|
+
task_id: str | None = None,
|
|
183
201
|
) -> dict[str, Any]:
|
|
184
202
|
if self.gpu_ids is not None:
|
|
185
203
|
request = replace(request, gpu_ids=self.gpu_ids)
|
|
204
|
+
if self.default_parameters:
|
|
205
|
+
request = replace(
|
|
206
|
+
request,
|
|
207
|
+
parameters={**self.default_parameters, **request.parameters},
|
|
208
|
+
)
|
|
186
209
|
self.start()
|
|
187
210
|
canonical_request = json.dumps(
|
|
188
211
|
request.to_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
@@ -201,9 +224,13 @@ class ExecutionManager:
|
|
|
201
224
|
"acceptedAt": record.accepted_at,
|
|
202
225
|
}
|
|
203
226
|
execution_id = f"exec-{uuid.uuid4().hex}"
|
|
227
|
+
resolved_job_id = str(job_id or execution_id)
|
|
228
|
+
resolved_task_id = str(task_id or execution_id)
|
|
204
229
|
record = ExecutionRecord(
|
|
205
230
|
execution_id=execution_id,
|
|
206
231
|
request=request,
|
|
232
|
+
job_id=resolved_job_id,
|
|
233
|
+
task_id=resolved_task_id,
|
|
207
234
|
accepted_at=utc_now(),
|
|
208
235
|
max_attempts=self.max_attempts,
|
|
209
236
|
)
|
|
@@ -217,6 +244,30 @@ class ExecutionManager:
|
|
|
217
244
|
),
|
|
218
245
|
progress_callback=record.add_progress,
|
|
219
246
|
)
|
|
247
|
+
if self.log_dir is not None:
|
|
248
|
+
record.log = ExecutionLog(
|
|
249
|
+
log_dir=self.log_dir,
|
|
250
|
+
job_id=record.job_id,
|
|
251
|
+
execution_id=record.execution_id,
|
|
252
|
+
logger=record.context.logger,
|
|
253
|
+
)
|
|
254
|
+
workspace = record.request.workspace or {}
|
|
255
|
+
record.log.write_event(
|
|
256
|
+
"request",
|
|
257
|
+
jobId=record.job_id,
|
|
258
|
+
taskId=record.task_id,
|
|
259
|
+
job_id=record.job_id,
|
|
260
|
+
task_id=record.task_id,
|
|
261
|
+
executionId=record.execution_id,
|
|
262
|
+
execution_id=record.execution_id,
|
|
263
|
+
inputs=[item.to_dict() for item in record.request.inputs],
|
|
264
|
+
inputRoot=workspace.get("inputRoot"),
|
|
265
|
+
outputRoot=workspace.get("outputRoot"),
|
|
266
|
+
scratchRoot=workspace.get("scratchRoot") or self.scratch_dir,
|
|
267
|
+
merge=record.request.merge,
|
|
268
|
+
parameters=record.request.parameters,
|
|
269
|
+
gpuIds=record.request.gpu_ids,
|
|
270
|
+
)
|
|
220
271
|
with self._lock:
|
|
221
272
|
if self._closed:
|
|
222
273
|
raise RuntimeError("execution manager is closed")
|
|
@@ -328,6 +379,9 @@ class ExecutionManager:
|
|
|
328
379
|
cancelled_before_start = bool(record.future and record.future.cancel())
|
|
329
380
|
if cancelled_before_start:
|
|
330
381
|
record.set_state("cancelled", error=reason)
|
|
382
|
+
if record.log is not None:
|
|
383
|
+
record.log.write_event("result", state="cancelled", error=reason)
|
|
384
|
+
record.log.close()
|
|
331
385
|
return {
|
|
332
386
|
"executionId": execution_id,
|
|
333
387
|
"state": record.state,
|
|
@@ -349,6 +403,9 @@ class ExecutionManager:
|
|
|
349
403
|
record.context.cancel("service shutdown")
|
|
350
404
|
if record.future and record.future.cancel():
|
|
351
405
|
record.set_state("cancelled", error="service shutdown")
|
|
406
|
+
if record.log is not None:
|
|
407
|
+
record.log.write_event("result", state="cancelled", error="service shutdown")
|
|
408
|
+
record.log.close()
|
|
352
409
|
self._executor.shutdown(wait=True, cancel_futures=True)
|
|
353
410
|
self.runner.close()
|
|
354
411
|
|
|
@@ -362,17 +419,28 @@ class ExecutionManager:
|
|
|
362
419
|
def _run(self, record: ExecutionRecord) -> None:
|
|
363
420
|
record.set_state("running")
|
|
364
421
|
assert record.context is not None
|
|
422
|
+
if record.log is not None:
|
|
423
|
+
record.log.write_event("started", jobId=record.job_id, taskId=record.task_id)
|
|
365
424
|
try:
|
|
366
425
|
result = self._run_with_restarts(record)
|
|
367
426
|
except ExecutionCancelled as exc:
|
|
368
427
|
record.context.mark_unfinished("cancelled", str(exc))
|
|
369
428
|
record.set_state("cancelled", error=str(exc))
|
|
429
|
+
if record.log is not None:
|
|
430
|
+
record.log.write_event("result", state="cancelled", error=str(exc))
|
|
370
431
|
except Exception as exc:
|
|
371
432
|
error = f"{type(exc).__name__}: {exc}"
|
|
372
433
|
record.context.mark_unfinished("failed", error)
|
|
373
434
|
record.set_state("failed", error=error)
|
|
435
|
+
if record.log is not None:
|
|
436
|
+
record.log.write_event("result", state="failed", error=error)
|
|
374
437
|
else:
|
|
375
438
|
record.set_state(result.status, result=result)
|
|
439
|
+
if record.log is not None:
|
|
440
|
+
record.log.write_event("result", state=record.state, result=result.to_dict())
|
|
441
|
+
finally:
|
|
442
|
+
if record.log is not None:
|
|
443
|
+
record.log.close()
|
|
376
444
|
|
|
377
445
|
def _run_with_restarts(self, record: ExecutionRecord) -> AlgorithmResult:
|
|
378
446
|
assert record.context is not None
|
|
@@ -585,7 +653,12 @@ def create_app(
|
|
|
585
653
|
async def create_execution(request: Request) -> Any:
|
|
586
654
|
try:
|
|
587
655
|
body = await request.json()
|
|
588
|
-
|
|
656
|
+
if not isinstance(body, dict):
|
|
657
|
+
raise ValueError("request body must be a mapping")
|
|
658
|
+
job_id = next((body.get(name) for name in ("jobId", "job_id") if body.get(name)), None)
|
|
659
|
+
task_id = next((body.get(name) for name in ("taskId", "task_id") if body.get(name)), None)
|
|
660
|
+
request_body = {key: value for key, value in body.items() if key not in {"jobId", "job_id", "taskId", "task_id"}}
|
|
661
|
+
execution_request = AlgorithmRequest.from_dict(request_body)
|
|
589
662
|
if execution_request.workspace is None:
|
|
590
663
|
raise ValueError("workspace is required")
|
|
591
664
|
except Exception as exc:
|
|
@@ -593,9 +666,14 @@ def create_app(
|
|
|
593
666
|
status_code=422,
|
|
594
667
|
content={"error": f"{type(exc).__name__}: {exc}"},
|
|
595
668
|
)
|
|
669
|
+
idempotency_key = request.headers.get("Idempotency-Key")
|
|
670
|
+
job_id = job_id or next((request.headers.get(name) for name in ("X-LDP-Job-ID", "X-Job-ID", "Job-ID") if request.headers.get(name)), None)
|
|
671
|
+
task_id = task_id or next((request.headers.get(name) for name in ("X-LDP-Task-ID", "X-Task-ID", "Task-ID") if request.headers.get(name)), None)
|
|
596
672
|
return manager.submit(
|
|
597
673
|
execution_request,
|
|
598
|
-
idempotency_key=
|
|
674
|
+
idempotency_key=idempotency_key,
|
|
675
|
+
job_id=job_id or idempotency_key,
|
|
676
|
+
task_id=task_id,
|
|
599
677
|
)
|
|
600
678
|
|
|
601
679
|
@application.get("/v1/executions/{execution_id}")
|
|
@@ -6,7 +6,9 @@ src/algorithm_plugin_sdk/cli.py
|
|
|
6
6
|
src/algorithm_plugin_sdk/context.py
|
|
7
7
|
src/algorithm_plugin_sdk/deployment.py
|
|
8
8
|
src/algorithm_plugin_sdk/errors.py
|
|
9
|
+
src/algorithm_plugin_sdk/gpu_isolation.py
|
|
9
10
|
src/algorithm_plugin_sdk/loader.py
|
|
11
|
+
src/algorithm_plugin_sdk/log_manager.py
|
|
10
12
|
src/algorithm_plugin_sdk/models.py
|
|
11
13
|
src/algorithm_plugin_sdk/registration.py
|
|
12
14
|
src/algorithm_plugin_sdk/release.py
|
|
@@ -34,6 +36,7 @@ src/ls_algorithm_plugin_sdk.egg-info/top_level.txt
|
|
|
34
36
|
tests/test_cli.py
|
|
35
37
|
tests/test_configure.py
|
|
36
38
|
tests/test_deployment.py
|
|
39
|
+
tests/test_gpu_isolation.py
|
|
37
40
|
tests/test_models.py
|
|
38
41
|
tests/test_registration.py
|
|
39
42
|
tests/test_release.py
|
|
@@ -142,6 +142,7 @@ class ConfigureTests(unittest.TestCase):
|
|
|
142
142
|
"--scratch-dir", "scratch",
|
|
143
143
|
"--gpu-ids", "2,0",
|
|
144
144
|
"--service-token", "secret",
|
|
145
|
+
"--default-parameters", '{"batchSize": 8, "mode": "fast"}',
|
|
145
146
|
"--env", "DEMO_MODE=fast",
|
|
146
147
|
"--force",
|
|
147
148
|
]
|
|
@@ -156,6 +157,10 @@ class ConfigureTests(unittest.TestCase):
|
|
|
156
157
|
self.assertEqual(args.scratch_dir, "scratch")
|
|
157
158
|
self.assertEqual(args.gpu_ids, [2, 0])
|
|
158
159
|
self.assertEqual(args.service_token, "secret")
|
|
160
|
+
self.assertEqual(
|
|
161
|
+
args.default_parameters,
|
|
162
|
+
{"batchSize": 8, "mode": "fast"},
|
|
163
|
+
)
|
|
159
164
|
self.assertEqual(args.env, [("DEMO_MODE", "fast")])
|
|
160
165
|
self.assertTrue(args.force)
|
|
161
166
|
|
|
@@ -200,15 +205,29 @@ class ConfigureTests(unittest.TestCase):
|
|
|
200
205
|
host="0.0.0.0",
|
|
201
206
|
port=9000,
|
|
202
207
|
webui=True,
|
|
203
|
-
max_concurrency=
|
|
208
|
+
max_concurrency=2,
|
|
204
209
|
scratch_dir=None,
|
|
205
210
|
token=None,
|
|
206
211
|
registration=None,
|
|
207
212
|
gpu_ids=[3, 1],
|
|
208
213
|
environment={"DEMO_MODE": "fast"},
|
|
214
|
+
default_parameters={},
|
|
209
215
|
)
|
|
210
216
|
config.save.assert_called_once_with()
|
|
211
217
|
|
|
218
|
+
def test_defaults_max_concurrency_to_one_without_gpus(self) -> None:
|
|
219
|
+
repository = Path("/srv/demo").resolve()
|
|
220
|
+
args = build_parser().parse_args(["configure", "--local"])
|
|
221
|
+
config = Mock()
|
|
222
|
+
|
|
223
|
+
with patch(
|
|
224
|
+
"algorithm_plugin_sdk.cli_impl.configure.DeploymentConfig.generate",
|
|
225
|
+
return_value=config,
|
|
226
|
+
) as generate:
|
|
227
|
+
generate_config(args, repository)
|
|
228
|
+
|
|
229
|
+
self.assertEqual(generate.call_args.kwargs["max_concurrency"], 1)
|
|
230
|
+
|
|
212
231
|
def test_requires_complete_registration_unless_local(self) -> None:
|
|
213
232
|
with patch.dict(os.environ, {}, clear=True):
|
|
214
233
|
args = build_parser().parse_args(["configure"])
|
|
@@ -323,10 +342,11 @@ class ConfigureTests(unittest.TestCase):
|
|
|
323
342
|
"[Service]\n"
|
|
324
343
|
"Type=simple\n"
|
|
325
344
|
"User=root\n"
|
|
326
|
-
"WorkingDirectory=/srv/demo algorithm\n"
|
|
345
|
+
"WorkingDirectory=/srv/demo algorithm/jobs\n"
|
|
327
346
|
'ExecStart="/srv/demo algorithm/.venv/bin/algorithm-plugin" '
|
|
328
347
|
'serve --config "/srv/demo algorithm/algorithm-plugin.json"\n'
|
|
329
348
|
"Environment=PYTHONUNBUFFERED=1\n"
|
|
349
|
+
"LimitNOFILE=524288\n"
|
|
330
350
|
"Restart=on-failure\n"
|
|
331
351
|
"RestartSec=5s\n"
|
|
332
352
|
"\n"
|
|
@@ -490,6 +510,9 @@ class ConfigureTests(unittest.TestCase):
|
|
|
490
510
|
"algorithm_plugin_sdk.cli_impl.configure.inspect_systemd_unit",
|
|
491
511
|
return_value=(unit_path, True),
|
|
492
512
|
) as inspect_unit,
|
|
513
|
+
patch(
|
|
514
|
+
"algorithm_plugin_sdk.cli_impl.configure.Path.mkdir",
|
|
515
|
+
) as mkdir,
|
|
493
516
|
patch(
|
|
494
517
|
"algorithm_plugin_sdk.cli_impl.configure.generate_config",
|
|
495
518
|
return_value=config,
|
|
@@ -505,10 +528,19 @@ class ConfigureTests(unittest.TestCase):
|
|
|
505
528
|
self.assertEqual(result, 0)
|
|
506
529
|
require_root.assert_called_once_with()
|
|
507
530
|
validate_runtime.assert_called_once_with(repository)
|
|
531
|
+
mkdir.assert_called_once_with(exist_ok=True)
|
|
508
532
|
generate.assert_called_once_with(args, repository)
|
|
509
533
|
inspect_unit.assert_called_once()
|
|
510
534
|
write_unit.assert_called_once()
|
|
511
535
|
self.assertIn("User=root", inspect_unit.call_args.kwargs["content"])
|
|
536
|
+
self.assertIn(
|
|
537
|
+
"LimitNOFILE=524288",
|
|
538
|
+
inspect_unit.call_args.kwargs["content"],
|
|
539
|
+
)
|
|
540
|
+
self.assertIn(
|
|
541
|
+
"WorkingDirectory=/srv/demo/jobs",
|
|
542
|
+
inspect_unit.call_args.kwargs["content"],
|
|
543
|
+
)
|
|
512
544
|
self.assertEqual(
|
|
513
545
|
output.getvalue(),
|
|
514
546
|
"Configured repository: /srv/demo\n"
|
|
@@ -70,6 +70,7 @@ class DeploymentConfigTests(unittest.TestCase):
|
|
|
70
70
|
registration=None,
|
|
71
71
|
gpu_ids=[3, 1],
|
|
72
72
|
environment={"DEMO_SETTING": "yes"},
|
|
73
|
+
default_parameters={"batchSize": 8, "mode": "fast"},
|
|
73
74
|
)
|
|
74
75
|
config.save()
|
|
75
76
|
loaded = DeploymentConfig.load(config.path)
|
|
@@ -77,6 +78,10 @@ class DeploymentConfigTests(unittest.TestCase):
|
|
|
77
78
|
self.assertEqual(loaded.payload["schemaVersion"], CONFIG_SCHEMA)
|
|
78
79
|
self.assertEqual(loaded.payload["algorithm"]["type"], "demo_algorithm")
|
|
79
80
|
self.assertEqual(loaded.payload["algorithm"]["version"], "1.2.3")
|
|
81
|
+
self.assertEqual(
|
|
82
|
+
loaded.payload["service"]["defaultParameters"],
|
|
83
|
+
{"batchSize": 8, "mode": "fast"},
|
|
84
|
+
)
|
|
80
85
|
self.assertEqual(loaded.payload["service"]["gpuIds"], [3, 1])
|
|
81
86
|
manifest_path = root / "release-manifest.json"
|
|
82
87
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from algorithm_plugin_sdk.cli_impl.configure import render_systemd_unit
|
|
7
|
+
from algorithm_plugin_sdk.gpu_isolation import apply_gpu_isolation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GpuIsolationTests(unittest.TestCase):
|
|
11
|
+
@patch.dict(os.environ, {}, clear=True)
|
|
12
|
+
def test_masks_physical_ids_and_returns_local_ordinals(self) -> None:
|
|
13
|
+
self.assertEqual(apply_gpu_isolation([3, 1]), [0, 1])
|
|
14
|
+
self.assertEqual(os.environ["CUDA_VISIBLE_DEVICES"], "3,1")
|
|
15
|
+
|
|
16
|
+
@patch.dict(os.environ, {"CUDA_VISIBLE_DEVICES": "7"}, clear=True)
|
|
17
|
+
def test_empty_gpu_ids_preserve_existing_mask(self) -> None:
|
|
18
|
+
self.assertEqual(apply_gpu_isolation(None), [])
|
|
19
|
+
self.assertEqual(apply_gpu_isolation([]), [])
|
|
20
|
+
self.assertEqual(os.environ["CUDA_VISIBLE_DEVICES"], "7")
|
|
21
|
+
|
|
22
|
+
def test_rejects_invalid_gpu_ids(self) -> None:
|
|
23
|
+
for gpu_ids in ([1, 1], [-1], [True]):
|
|
24
|
+
with self.subTest(gpu_ids=gpu_ids), self.assertRaises(ValueError):
|
|
25
|
+
apply_gpu_isolation(gpu_ids)
|
|
26
|
+
|
|
27
|
+
def test_systemd_unit_sets_physical_gpu_mask(self) -> None:
|
|
28
|
+
unit = render_systemd_unit(
|
|
29
|
+
repository=Path("/srv/demo"),
|
|
30
|
+
config_path=Path("/srv/demo/algorithm-plugin.json"),
|
|
31
|
+
service_name="demo",
|
|
32
|
+
gpu_ids=[3, 1],
|
|
33
|
+
)
|
|
34
|
+
self.assertIn("Environment=CUDA_VISIBLE_DEVICES=3,1", unit)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
unittest.main()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import unittest
|
|
2
3
|
from io import StringIO
|
|
3
4
|
from unittest.mock import Mock, patch
|
|
@@ -115,6 +116,31 @@ class AlgorithmRunnerTests(unittest.TestCase):
|
|
|
115
116
|
|
|
116
117
|
self.assertEqual(request.gpu_ids, [3, 1])
|
|
117
118
|
self.assertEqual(request.inputs[0].input_dataset, "/data/input")
|
|
119
|
+
def test_execute_isolates_and_maps_cli_gpu_ids(self) -> None:
|
|
120
|
+
args = build_parser().parse_args(
|
|
121
|
+
[
|
|
122
|
+
"run",
|
|
123
|
+
"test:Algorithm",
|
|
124
|
+
"--input",
|
|
125
|
+
'{"input_dataset":"/data/input","output":"/data/output"}',
|
|
126
|
+
"--gpu-ids",
|
|
127
|
+
"3,1",
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
algorithm = RecordingAlgorithm()
|
|
131
|
+
|
|
132
|
+
with (
|
|
133
|
+
patch.dict(os.environ, {}, clear=True),
|
|
134
|
+
patch(
|
|
135
|
+
"algorithm_plugin_sdk.cli_impl.run.load_algorithm",
|
|
136
|
+
return_value=algorithm,
|
|
137
|
+
),
|
|
138
|
+
):
|
|
139
|
+
self.assertEqual(execute_run(args), 0)
|
|
140
|
+
self.assertEqual(os.environ["CUDA_VISIBLE_DEVICES"], "3,1")
|
|
141
|
+
|
|
142
|
+
self.assertEqual(algorithm.calls[0].gpu_ids, [0, 1])
|
|
143
|
+
|
|
118
144
|
|
|
119
145
|
def test_serve_uses_service_restart_defaults(self) -> None:
|
|
120
146
|
args = build_parser().parse_args(["serve", "test:Algorithm"])
|
|
@@ -37,6 +37,26 @@ class ExecutionManagerTests(unittest.TestCase):
|
|
|
37
37
|
manager.close()
|
|
38
38
|
self.assertEqual(algorithm.calls[0].gpu_ids, [3, 1])
|
|
39
39
|
|
|
40
|
+
def test_fills_missing_parameters_from_configured_defaults(self) -> None:
|
|
41
|
+
algorithm = RecordingAlgorithm()
|
|
42
|
+
manager = ExecutionManager(
|
|
43
|
+
AlgorithmRunner(algorithm),
|
|
44
|
+
default_parameters={"batchSize": 8, "mode": "accurate"},
|
|
45
|
+
)
|
|
46
|
+
request = AlgorithmRequest(
|
|
47
|
+
inputs=self._request().inputs,
|
|
48
|
+
parameters={"batchSize": 16},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
accepted = manager.submit(request)
|
|
52
|
+
manager._record(accepted["executionId"]).future.result(timeout=2)
|
|
53
|
+
manager.close()
|
|
54
|
+
|
|
55
|
+
self.assertEqual(
|
|
56
|
+
algorithm.calls[0].parameters,
|
|
57
|
+
{"batchSize": 16, "mode": "accurate"},
|
|
58
|
+
)
|
|
59
|
+
|
|
40
60
|
def test_runs_the_same_algorithm_contract(self) -> None:
|
|
41
61
|
manager = ExecutionManager(AlgorithmRunner(RecordingAlgorithm()))
|
|
42
62
|
accepted = manager.submit(
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/context.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/loader.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/release.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/src/algorithm_plugin_sdk/runner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.1 → ls_algorithm_plugin_sdk-0.3.3}/tests/test_sdk_automation.py
RENAMED
|
File without changes
|