badgr-cli 1.1.6 → 1.1.7
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.
- package/README.md +49 -13
- package/package.json +1 -1
- package/src/admin.js +29 -0
- package/src/api.js +35 -29
- package/src/badgr.js +18 -6
- package/src/catalog.js +12 -0
- package/src/commands/capacity.js +38 -1
- package/src/commands/check.js +127 -0
- package/src/commands/connect.js +3 -2
- package/src/commands/diagnose.js +89 -41
- package/src/commands/down.js +16 -7
- package/src/commands/job.js +5 -5
- package/src/commands/launch.js +37 -12
- package/src/commands/node.js +250 -0
- package/src/commands/run.js +14 -4
- package/src/commands/serve.js +42 -8
- package/src/commands/status.js +13 -5
- package/src/commands/train.js +1 -1
- package/src/config.js +12 -19
- package/src/credentials.js +6 -0
- package/src/fallback.js +9 -11
- package/src/gpuDoctor/gpuInfo.js +144 -5
- package/src/nodeWorker/node_worker.py +326 -0
- package/src/nodeWorker/test_node_worker.py +188 -0
- package/src/progress.js +2 -2
- package/src/spec.js +2 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for node_worker.py's three additions to the customer_node BYO GPU
|
|
3
|
+
worker (see backend/customer_node_adapter.py / backend/node_routes.py for
|
|
4
|
+
the control-plane side of the same feature):
|
|
5
|
+
- privileged/host diagnostic execution (docker flags only, no live docker)
|
|
6
|
+
- hard timeout enforcement in the job-streaming loop
|
|
7
|
+
- structured BADGR_RESULT= parsing
|
|
8
|
+
|
|
9
|
+
No real docker/network calls are made -- subprocess.Popen/run are stubbed.
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
16
|
+
|
|
17
|
+
import node_worker # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_parse_diagnostic_result_pass():
|
|
21
|
+
log = "=== RESULT ===\nBADGR_RESULT=PASS\n"
|
|
22
|
+
assert node_worker._parse_diagnostic_result(log) == "PASS"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parse_diagnostic_result_fail():
|
|
26
|
+
log = "some noise\nBADGR_RESULT=FAIL\nmore noise after\n"
|
|
27
|
+
assert node_worker._parse_diagnostic_result(log) == "FAIL"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_parse_diagnostic_result_absent_for_ordinary_job():
|
|
31
|
+
assert node_worker._parse_diagnostic_result("epoch 1/1 done\n") is None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_privileged_plan_adds_host_diagnostic_docker_flags(monkeypatch):
|
|
35
|
+
captured_cmds = []
|
|
36
|
+
|
|
37
|
+
class _FakeProc:
|
|
38
|
+
pid = 4242
|
|
39
|
+
returncode = 0
|
|
40
|
+
stdout = None
|
|
41
|
+
|
|
42
|
+
def poll(self):
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
def fake_popen(cmd, **kwargs):
|
|
46
|
+
captured_cmds.append(cmd)
|
|
47
|
+
return _FakeProc()
|
|
48
|
+
|
|
49
|
+
def fake_run(cmd, **kwargs):
|
|
50
|
+
captured_cmds.append(cmd)
|
|
51
|
+
|
|
52
|
+
class _R:
|
|
53
|
+
returncode = 0
|
|
54
|
+
return _R()
|
|
55
|
+
|
|
56
|
+
reports = []
|
|
57
|
+
monkeypatch.setattr(node_worker.subprocess, "Popen", fake_popen)
|
|
58
|
+
monkeypatch.setattr(node_worker.subprocess, "run", fake_run)
|
|
59
|
+
monkeypatch.setattr(node_worker, "_api", lambda *a, **kw: reports.append(kw) or {})
|
|
60
|
+
|
|
61
|
+
node_worker._run_job(
|
|
62
|
+
"https://x/v1", "key", "node_1", "job_1",
|
|
63
|
+
{"image": "badgr/diagnostics:latest", "command": ["./nvlink_p2p_check.sh"], "privileged": True},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
docker_cmd = captured_cmds[0]
|
|
67
|
+
assert "--privileged" in docker_cmd
|
|
68
|
+
assert "--pid=host" in docker_cmd
|
|
69
|
+
assert "/dev:/dev" in docker_cmd
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_ordinary_job_has_no_privileged_flags(monkeypatch):
|
|
73
|
+
captured_cmds = []
|
|
74
|
+
|
|
75
|
+
class _FakeProc:
|
|
76
|
+
pid = 4242
|
|
77
|
+
returncode = 0
|
|
78
|
+
stdout = None
|
|
79
|
+
|
|
80
|
+
def poll(self):
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
monkeypatch.setattr(node_worker.subprocess, "Popen", lambda cmd, **kw: captured_cmds.append(cmd) or _FakeProc())
|
|
84
|
+
monkeypatch.setattr(node_worker, "_api", lambda *a, **kw: {})
|
|
85
|
+
|
|
86
|
+
node_worker._run_job(
|
|
87
|
+
"https://x/v1", "key", "node_1", "job_2",
|
|
88
|
+
{"image": "python:3.11-slim", "command": ["python", "train.py"]},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert "--privileged" not in captured_cmds[0]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_nvidia_job_uses_gpus_all_flag_by_default(monkeypatch):
|
|
95
|
+
captured_cmds = []
|
|
96
|
+
|
|
97
|
+
class _FakeProc:
|
|
98
|
+
pid = 4242
|
|
99
|
+
returncode = 0
|
|
100
|
+
stdout = None
|
|
101
|
+
|
|
102
|
+
def poll(self):
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
monkeypatch.setattr(node_worker.subprocess, "Popen", lambda cmd, **kw: captured_cmds.append(cmd) or _FakeProc())
|
|
106
|
+
monkeypatch.setattr(node_worker, "_api", lambda *a, **kw: {})
|
|
107
|
+
|
|
108
|
+
node_worker._run_job(
|
|
109
|
+
"https://x/v1", "key", "node_1", "job_4",
|
|
110
|
+
{"image": "python:3.11-slim", "command": ["python", "train.py"]},
|
|
111
|
+
"nvidia",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
docker_cmd = captured_cmds[0]
|
|
115
|
+
assert "--gpus" in docker_cmd and "all" in docker_cmd
|
|
116
|
+
assert "--device=/dev/kfd" not in docker_cmd
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_amd_job_uses_rocm_device_flags(monkeypatch):
|
|
120
|
+
captured_cmds = []
|
|
121
|
+
|
|
122
|
+
class _FakeProc:
|
|
123
|
+
pid = 4242
|
|
124
|
+
returncode = 0
|
|
125
|
+
stdout = None
|
|
126
|
+
|
|
127
|
+
def poll(self):
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
monkeypatch.setattr(node_worker.subprocess, "Popen", lambda cmd, **kw: captured_cmds.append(cmd) or _FakeProc())
|
|
131
|
+
monkeypatch.setattr(node_worker, "_api", lambda *a, **kw: {})
|
|
132
|
+
|
|
133
|
+
node_worker._run_job(
|
|
134
|
+
"https://x/v1", "key", "node_1", "job_5",
|
|
135
|
+
{"image": "rocm/vllm:latest", "command": ["python", "train.py"]},
|
|
136
|
+
"amd",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
docker_cmd = captured_cmds[0]
|
|
140
|
+
assert "--device=/dev/kfd" in docker_cmd
|
|
141
|
+
assert "--device=/dev/dri" in docker_cmd
|
|
142
|
+
assert "--gpus" not in docker_cmd
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_gpu_docker_args_defaults_to_nvidia_flag_when_vendor_unknown():
|
|
146
|
+
assert node_worker._gpu_docker_args(None) == ["--gpus", "all"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_job_exceeding_max_runtime_is_killed_and_reported(monkeypatch):
|
|
150
|
+
"""A hung diagnostic must not wedge the worker forever -- the loop kills
|
|
151
|
+
the named container once max_runtime_seconds elapses and reports a
|
|
152
|
+
FAIL result rather than hanging indefinitely."""
|
|
153
|
+
kill_calls = []
|
|
154
|
+
|
|
155
|
+
class _FakeProc:
|
|
156
|
+
pid = 4242
|
|
157
|
+
returncode = None
|
|
158
|
+
stdout = None
|
|
159
|
+
|
|
160
|
+
def poll(self):
|
|
161
|
+
return None # never exits on its own -- simulates a hang
|
|
162
|
+
|
|
163
|
+
def wait(self):
|
|
164
|
+
self.returncode = -9
|
|
165
|
+
|
|
166
|
+
fake_proc = _FakeProc()
|
|
167
|
+
monkeypatch.setattr(node_worker.subprocess, "Popen", lambda cmd, **kw: fake_proc)
|
|
168
|
+
monkeypatch.setattr(node_worker.subprocess, "run", lambda cmd, **kw: kill_calls.append(cmd))
|
|
169
|
+
|
|
170
|
+
# Force the deadline to already be in the past on the very first loop
|
|
171
|
+
# check, instead of sleeping in the test.
|
|
172
|
+
clock = iter([1000.0, 1000.0, 2000.0])
|
|
173
|
+
monkeypatch.setattr(node_worker.time, "monotonic", lambda: next(clock))
|
|
174
|
+
|
|
175
|
+
reports = []
|
|
176
|
+
monkeypatch.setattr(node_worker, "_api", lambda *a, **kw: reports.append(a[-1]) or {})
|
|
177
|
+
|
|
178
|
+
node_worker._run_job(
|
|
179
|
+
"https://x/v1", "key", "node_1", "job_3",
|
|
180
|
+
{"image": "badgr/diagnostics:latest", "privileged": True, "max_runtime_seconds": 5},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
assert any("badgr-job-job_3" in cmd for cmd in kill_calls)
|
|
184
|
+
assert reports, "worker must report a status even on timeout"
|
|
185
|
+
report = reports[0]
|
|
186
|
+
assert report["status"] == "error"
|
|
187
|
+
assert "timed out" in report["error"]
|
|
188
|
+
assert report["result"] == "FAIL"
|
package/src/progress.js
CHANGED
|
@@ -101,7 +101,7 @@ export async function pollJobUntilTerminal(callApi, config, jobId, { chalk, maxM
|
|
|
101
101
|
|
|
102
102
|
// One-time announcement of the automatic retry-on-a-different-route —
|
|
103
103
|
// printed once per retry (not every poll tick) so it reads as an event,
|
|
104
|
-
// not a repeated status line. See
|
|
104
|
+
// not a repeated status line. See the backend's own retrying_different_route
|
|
105
105
|
// stage — this is what "one safe retry" looks like from the CLI.
|
|
106
106
|
if (detail.stage === 'retrying_different_route' && !announcedRetry) {
|
|
107
107
|
announcedRetry = true;
|
|
@@ -148,7 +148,7 @@ function _formatElapsed(seconds) {
|
|
|
148
148
|
* Prints the `Class:`/`Next:` lines for a failed GpuDeployment (the raw
|
|
149
149
|
* /deployments/{id} path used by `badgr run`/`badgr serve`/`badgr comfyui run`)
|
|
150
150
|
* using the exact same failure_class/next_action fields — computed by the
|
|
151
|
-
* same backend
|
|
151
|
+
* same backend classify_failure()/next_step_for() functions —
|
|
152
152
|
* that renderJobClosingBlock below already prints for the productized
|
|
153
153
|
* /v1/jobs path (comfy.batch, train.lora, custom.run, model.serve). One
|
|
154
154
|
* failure-class vocabulary shown the same way regardless of which API path
|
package/src/spec.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deployment spec: the single document that describes what `gpu up` should provision.
|
|
3
|
-
* Mirrors the
|
|
3
|
+
* Mirrors the backend's own deployment-dispatch concepts (gpu_type, region, workload type).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const WORKLOAD_TYPES = ['endpoint', 'job'];
|
|
@@ -26,7 +26,7 @@ export function vmClassForWorkload(workloadName) {
|
|
|
26
26
|
return workloadName === 'playwright' ? 'browser' : 'small';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// Match canonical GPU IDs used in
|
|
29
|
+
// Match canonical GPU IDs used in the backend's own GPU_ALIASES
|
|
30
30
|
export const GPU_TYPE_MAP = {
|
|
31
31
|
'rtx-4090': 'RTX_4090', 'rtx4090': 'RTX_4090', '4090': 'RTX_4090',
|
|
32
32
|
'rtx-3090': 'RTX_3090', 'rtx3090': 'RTX_3090', '3090': 'RTX_3090',
|