blitzwing 0.1.5 → 0.1.9
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/package.json +2 -2
- package/src/config.js +6 -3
- package/src/install.js +108 -21
- package/src/net.js +53 -0
- package/src/wizard.js +109 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzwing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Interactive setup wizard to join a Blitzwing Petals mother swarm as a compute contributor",
|
|
5
5
|
"bin": {
|
|
6
6
|
"blitzwing": "bin/blitzwing.js"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"config": {
|
|
25
|
-
"discoveryUrl": "
|
|
25
|
+
"discoveryUrl": "http://35.238.86.1:9000"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@clack/prompts": "^0.9.1",
|
package/src/config.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
export const DEFAULT_DISCOVERY_URL =
|
|
3
3
|
process.env.BLITZWING_DISCOVERY_URL ||
|
|
4
4
|
process.env.npm_package_config_discoveryUrl ||
|
|
5
|
-
"
|
|
5
|
+
"http://35.238.86.1:9000";
|
|
6
6
|
|
|
7
7
|
export const HOME_DIR = process.env.BLITZWING_HOME || `${process.env.HOME || process.env.USERPROFILE}/.blitzwing`;
|
|
8
8
|
export const STATE_PATH = `${HOME_DIR}/contributor.json`;
|
|
9
9
|
export const VENV_PATH = `${HOME_DIR}/venv`;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Windows + WSL on same PC: avoid 8001/31337 — WSL forwards those on 127.0.0.1.
|
|
11
|
+
const defaultShardPort = process.platform === "win32" ? 8011 : 8001;
|
|
12
|
+
const defaultPetalsPort = process.platform === "win32" ? 31338 : 31337;
|
|
13
|
+
export const SHARD_PORT = Number(process.env.BLITZWING_SHARD_PORT || defaultShardPort);
|
|
14
|
+
export const PETALS_PORT = Number(process.env.BLITZWING_PETALS_PORT || defaultPetalsPort);
|
package/src/install.js
CHANGED
|
@@ -101,13 +101,35 @@ export function ensurePython() {
|
|
|
101
101
|
|
|
102
102
|
function petalsImportable(python) {
|
|
103
103
|
try {
|
|
104
|
-
execFileSync(
|
|
104
|
+
execFileSync(
|
|
105
|
+
python,
|
|
106
|
+
["-c", "import petals.cli.run_server"],
|
|
107
|
+
{ stdio: "ignore" }
|
|
108
|
+
);
|
|
105
109
|
return true;
|
|
106
110
|
} catch {
|
|
107
111
|
return false;
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function uvicornImportable(python) {
|
|
116
|
+
try {
|
|
117
|
+
execFileSync(python, ["-c", "import uvicorn, fastapi"], { stdio: "ignore" });
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureShardManagerDeps(python, pip, env, onLog) {
|
|
125
|
+
if (uvicornImportable(python)) return;
|
|
126
|
+
onLog?.("Installing shard manager deps (uvicorn, fastapi)...");
|
|
127
|
+
execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic"], {
|
|
128
|
+
stdio: "inherit",
|
|
129
|
+
env,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
export function ensureVenvAndPetals({ onLog }) {
|
|
112
134
|
const { py } = ensurePython();
|
|
113
135
|
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
@@ -125,29 +147,43 @@ export function ensureVenvAndPetals({ onLog }) {
|
|
|
125
147
|
};
|
|
126
148
|
|
|
127
149
|
if (petalsImportable(python)) {
|
|
150
|
+
ensureShardManagerDeps(python, pip, env, onLog);
|
|
128
151
|
onLog?.("Petals already installed — skipping dependency install");
|
|
152
|
+
fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
|
|
129
153
|
return { python, pip };
|
|
130
154
|
}
|
|
131
155
|
|
|
156
|
+
if (process.platform === "win32") {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"Petals cannot be installed on native Windows (hivemind requires uvloop/Linux).\n" +
|
|
159
|
+
"Run the contributor inside WSL instead:\n" +
|
|
160
|
+
" wsl bash /mnt/c/Users/MSI/Desktop/blitzwing/scripts/local_wsl_contributor_join.sh\n" +
|
|
161
|
+
"Or run the full E2E script:\n" +
|
|
162
|
+
" powershell -File scripts/run_local_e2e.ps1"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
132
166
|
onLog?.("Installing CPU PyTorch + Petals (may take a few minutes on first run)...");
|
|
133
|
-
execFileSync(
|
|
167
|
+
execFileSync(python, ["-m", "pip", "install", "-U", "pip", "wheel", "setuptools<81"], {
|
|
134
168
|
stdio: "inherit",
|
|
135
169
|
env,
|
|
136
170
|
});
|
|
137
171
|
execFileSync(
|
|
138
|
-
|
|
139
|
-
["install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
|
|
172
|
+
python,
|
|
173
|
+
["-m", "pip", "install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
|
|
140
174
|
{ stdio: "inherit", env }
|
|
141
175
|
);
|
|
142
176
|
// Needed to compile hivemind protobufs during --no-build-isolation install
|
|
143
|
-
execFileSync(
|
|
177
|
+
execFileSync(python, ["-m", "pip", "install", "grpcio", "grpcio-tools", "protobuf"], {
|
|
144
178
|
stdio: "inherit",
|
|
145
179
|
env,
|
|
146
180
|
});
|
|
147
181
|
// Hivemind first with no build isolation (needs pkg_resources from setuptools<81)
|
|
148
182
|
execFileSync(
|
|
149
|
-
|
|
183
|
+
python,
|
|
150
184
|
[
|
|
185
|
+
"-m",
|
|
186
|
+
"pip",
|
|
151
187
|
"install",
|
|
152
188
|
"--no-build-isolation",
|
|
153
189
|
"git+https://github.com/learning-at-home/hivemind.git@213bff98a62accb91f254e2afdccbf1d69ebdea9",
|
|
@@ -155,15 +191,16 @@ export function ensureVenvAndPetals({ onLog }) {
|
|
|
155
191
|
{ stdio: "inherit", env }
|
|
156
192
|
);
|
|
157
193
|
execFileSync(
|
|
158
|
-
|
|
159
|
-
["install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
|
|
194
|
+
python,
|
|
195
|
+
["-m", "pip", "install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
|
|
160
196
|
{ stdio: "inherit", env }
|
|
161
197
|
);
|
|
162
|
-
execFileSync(
|
|
198
|
+
execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
|
|
163
199
|
stdio: "inherit",
|
|
164
200
|
env,
|
|
165
201
|
});
|
|
166
202
|
|
|
203
|
+
fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
|
|
167
204
|
return { python, pip };
|
|
168
205
|
}
|
|
169
206
|
|
|
@@ -183,6 +220,7 @@ from pydantic import BaseModel, Field
|
|
|
183
220
|
|
|
184
221
|
class ReloadRequest(BaseModel):
|
|
185
222
|
block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
|
|
223
|
+
initial_peers: Optional[List[str]] = None
|
|
186
224
|
|
|
187
225
|
class StatusResponse(BaseModel):
|
|
188
226
|
running: bool
|
|
@@ -202,8 +240,12 @@ class Mgr:
|
|
|
202
240
|
self.block_indices = os.environ["BLOCK_INDICES"]
|
|
203
241
|
peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
|
|
204
242
|
self.initial_peers = peers
|
|
243
|
+
announce_raw = os.environ.get("ANNOUNCE_MADDRS", "")
|
|
244
|
+
self.announce_maddrs = [a.strip() for a in announce_raw.split(",") if a.strip()]
|
|
205
245
|
self.python = os.environ.get("PETALS_PYTHON", "python")
|
|
246
|
+
self.petals_log = os.environ.get("PETALS_LOG", os.path.expanduser("~/.blitzwing/petals.log"))
|
|
206
247
|
self._proc = None
|
|
248
|
+
self._log_fp = None
|
|
207
249
|
self._lock = threading.Lock()
|
|
208
250
|
self.last_exit_code = None
|
|
209
251
|
|
|
@@ -212,10 +254,19 @@ class Mgr:
|
|
|
212
254
|
"--device", "cpu", "--quant_type", "none",
|
|
213
255
|
"--block_indices", bi, "--port", str(self.port),
|
|
214
256
|
"--identity_path", self.identity_path, "--num_handlers", "1"]
|
|
215
|
-
if self.
|
|
257
|
+
if self.announce_maddrs:
|
|
258
|
+
c += ["--announce_maddrs", *self.announce_maddrs]
|
|
259
|
+
elif self.public_ip:
|
|
216
260
|
c += ["--public_ip", self.public_ip]
|
|
217
261
|
if self.initial_peers:
|
|
218
262
|
c += ["--initial_peers", *self.initial_peers]
|
|
263
|
+
use_auto_relay = os.environ.get("PETALS_USE_AUTO_RELAY", "1") not in ("0", "false", "False")
|
|
264
|
+
if not self.announce_maddrs and use_auto_relay:
|
|
265
|
+
pass
|
|
266
|
+
elif not use_auto_relay:
|
|
267
|
+
c += ["--no_auto_relay"]
|
|
268
|
+
if os.environ.get("PETALS_SKIP_REACHABILITY_CHECK", "1") in ("1", "true", "True"):
|
|
269
|
+
c += ["--skip_reachability_check"]
|
|
219
270
|
return c
|
|
220
271
|
|
|
221
272
|
def start(self, bi=None):
|
|
@@ -223,7 +274,15 @@ class Mgr:
|
|
|
223
274
|
if bi: self.block_indices = bi
|
|
224
275
|
if self._proc and self._proc.poll() is None:
|
|
225
276
|
return
|
|
226
|
-
self.
|
|
277
|
+
if self._log_fp is None:
|
|
278
|
+
os.makedirs(os.path.dirname(self.petals_log) or ".", exist_ok=True)
|
|
279
|
+
self._log_fp = open(self.petals_log, "a", encoding="utf-8")
|
|
280
|
+
popen_kwargs = {"stdout": self._log_fp, "stderr": subprocess.STDOUT}
|
|
281
|
+
if os.name == "nt":
|
|
282
|
+
popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
283
|
+
else:
|
|
284
|
+
popen_kwargs["preexec_fn"] = os.setsid
|
|
285
|
+
self._proc = subprocess.Popen(self.cmd(self.block_indices), **popen_kwargs)
|
|
227
286
|
self.last_exit_code = None
|
|
228
287
|
|
|
229
288
|
def stop(self):
|
|
@@ -232,17 +291,28 @@ class Mgr:
|
|
|
232
291
|
self._proc = None
|
|
233
292
|
return
|
|
234
293
|
p = self._proc
|
|
235
|
-
|
|
294
|
+
try:
|
|
295
|
+
if os.name == "nt":
|
|
296
|
+
p.send_signal(signal.CTRL_BREAK_EVENT)
|
|
297
|
+
else:
|
|
298
|
+
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
|
|
299
|
+
except Exception:
|
|
300
|
+
p.send_signal(signal.SIGTERM)
|
|
236
301
|
try: p.wait(timeout=30)
|
|
237
302
|
except Exception:
|
|
238
303
|
p.kill(); p.wait(timeout=10)
|
|
239
304
|
self.last_exit_code = p.returncode
|
|
240
305
|
self._proc = None
|
|
241
306
|
|
|
242
|
-
def reload(self, bi):
|
|
307
|
+
def reload(self, bi, initial_peers=None):
|
|
308
|
+
if initial_peers is not None:
|
|
309
|
+
self.initial_peers = initial_peers
|
|
243
310
|
self.stop(); time.sleep(1); self.start(bi)
|
|
244
311
|
|
|
245
312
|
def status(self):
|
|
313
|
+
if self._proc is not None and self._proc.poll() is not None:
|
|
314
|
+
self.last_exit_code = self._proc.returncode
|
|
315
|
+
self._proc = None
|
|
246
316
|
running = self._proc is not None and self._proc.poll() is None
|
|
247
317
|
return StatusResponse(
|
|
248
318
|
running=running,
|
|
@@ -273,7 +343,7 @@ def status():
|
|
|
273
343
|
def reload(body: ReloadRequest):
|
|
274
344
|
a,b = map(int, body.block_indices.split(":"))
|
|
275
345
|
if b <= a: raise HTTPException(400, "bad range")
|
|
276
|
-
mgr.reload(body.block_indices)
|
|
346
|
+
mgr.reload(body.block_indices, initial_peers=body.initial_peers)
|
|
277
347
|
time.sleep(0.5)
|
|
278
348
|
return mgr.status()
|
|
279
349
|
|
|
@@ -292,12 +362,13 @@ export function startShardManagerProcess({
|
|
|
292
362
|
}) {
|
|
293
363
|
const appPath = writeLocalShardManager();
|
|
294
364
|
const out = fs.openSync(logPath, "a");
|
|
365
|
+
const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
|
|
295
366
|
const child = spawn(
|
|
296
|
-
|
|
367
|
+
petalsPy,
|
|
297
368
|
["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
|
|
298
369
|
{
|
|
299
370
|
cwd: path.dirname(appPath),
|
|
300
|
-
env: { ...process.env, ...env, PETALS_PYTHON:
|
|
371
|
+
env: { ...process.env, ...env, PETALS_PYTHON: petalsPy, PETALS_LOG: path.join(HOME_DIR, "petals.log") },
|
|
301
372
|
detached: true,
|
|
302
373
|
stdio: ["ignore", out, out],
|
|
303
374
|
}
|
|
@@ -306,21 +377,37 @@ export function startShardManagerProcess({
|
|
|
306
377
|
return { pid: child.pid, logPath, appPath };
|
|
307
378
|
}
|
|
308
379
|
|
|
309
|
-
export async function waitForShardRunning({ timeoutMs = 300000 } = {}) {
|
|
380
|
+
export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {}) {
|
|
381
|
+
const host = statusHost || process.env.BLITZWING_STATUS_HOST || "127.0.0.1";
|
|
382
|
+
const petalsLog = path.join(HOME_DIR, "petals.log");
|
|
310
383
|
const start = Date.now();
|
|
311
384
|
while (Date.now() - start < timeoutMs) {
|
|
312
385
|
try {
|
|
313
|
-
const res = await fetch(`http
|
|
386
|
+
const res = await fetch(`http://${host}:${SHARD_PORT}/status`);
|
|
314
387
|
if (res.ok) {
|
|
315
388
|
const body = await res.json();
|
|
316
|
-
if (body.running) return body;
|
|
389
|
+
if (body.running && body.pid) return body;
|
|
390
|
+
if (body.last_exit_code != null) {
|
|
391
|
+
const tail = fs.existsSync(petalsLog)
|
|
392
|
+
? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
|
|
393
|
+
: "";
|
|
394
|
+
throw new Error(
|
|
395
|
+
`Petals exited (code ${body.last_exit_code}). ${tail || "See " + petalsLog}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
317
398
|
}
|
|
318
|
-
} catch {
|
|
399
|
+
} catch (err) {
|
|
400
|
+
if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
|
|
319
401
|
/* retry */
|
|
320
402
|
}
|
|
321
403
|
await new Promise((r) => setTimeout(r, 2000));
|
|
322
404
|
}
|
|
323
|
-
|
|
405
|
+
const tail = fs.existsSync(petalsLog)
|
|
406
|
+
? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
|
|
407
|
+
: "";
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Timed out waiting for Petals on http://${host}:${SHARD_PORT}/status. ${tail || "See " + petalsLog}`
|
|
410
|
+
);
|
|
324
411
|
}
|
|
325
412
|
|
|
326
413
|
export { SHARD_PORT, PETALS_PORT, venvPython };
|
package/src/net.js
CHANGED
|
@@ -24,6 +24,59 @@ export async function detectPublicIp() {
|
|
|
24
24
|
return "";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Local LAN/WSL IP for shard_manager_url metadata (not Petals announce). */
|
|
28
|
+
export async function detectLocalIp() {
|
|
29
|
+
if (process.env.BLITZWING_LOCAL_IP) {
|
|
30
|
+
return process.env.BLITZWING_LOCAL_IP.trim();
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const { networkInterfaces } = await import("node:os");
|
|
34
|
+
const nets = networkInterfaces();
|
|
35
|
+
for (const name of Object.keys(nets)) {
|
|
36
|
+
for (const net of nets[name] || []) {
|
|
37
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
38
|
+
return net.address;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
return "127.0.0.1";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Optional ngrok TCP tunnel for dev; production should use VM public IP or auto-relay. */
|
|
49
|
+
export async function discoverNgrokTcpAnnounce(petalsPort) {
|
|
50
|
+
const override = process.env.BLITZWING_ANNOUNCE_MADDRS?.trim();
|
|
51
|
+
if (override) return override;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch("http://127.0.0.1:4040/api/tunnels", {
|
|
55
|
+
signal: AbortSignal.timeout(2000),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) return null;
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
const tunnel = (data.tunnels || []).find((t) => t.proto === "tcp");
|
|
60
|
+
if (!tunnel?.public_url) return null;
|
|
61
|
+
const url = new URL(tunnel.public_url);
|
|
62
|
+
return `/dns4/${url.hostname}/tcp/${url.port || petalsPort}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function extractPeerMultiaddrFromLog(logPath, fs) {
|
|
69
|
+
if (!fs.existsSync(logPath)) return null;
|
|
70
|
+
const text = fs.readFileSync(logPath, "utf8");
|
|
71
|
+
const line = text
|
|
72
|
+
.split("\n")
|
|
73
|
+
.reverse()
|
|
74
|
+
.find((l) => l.includes("Running a server on"));
|
|
75
|
+
if (!line) return null;
|
|
76
|
+
const m = line.match(/Running a server on \['([^']+)'\]/);
|
|
77
|
+
return m ? m[1] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
27
80
|
export function which(cmd) {
|
|
28
81
|
try {
|
|
29
82
|
const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
|
package/src/wizard.js
CHANGED
|
@@ -5,7 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { DEFAULT_DISCOVERY_URL, HOME_DIR, SHARD_PORT, PETALS_PORT } from "./config.js";
|
|
7
7
|
import { listMothers, motherHosts, joinHost, readyHost, leaveHost } from "./api.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
detectPublicIp,
|
|
10
|
+
detectLocalIp,
|
|
11
|
+
discoverNgrokTcpAnnounce,
|
|
12
|
+
extractPeerMultiaddrFromLog,
|
|
13
|
+
} from "./net.js";
|
|
9
14
|
import {
|
|
10
15
|
ensureVenvAndPetals,
|
|
11
16
|
startShardManagerProcess,
|
|
@@ -55,6 +60,9 @@ ${color.bold("blitzwing")} — join a Blitzwing mother swarm as a compute node
|
|
|
55
60
|
|
|
56
61
|
Env:
|
|
57
62
|
BLITZWING_DISCOVERY_URL Override discovery service (default ${DEFAULT_DISCOVERY_URL})
|
|
63
|
+
BLITZWING_ANNOUNCE_MADDRS Explicit Petals announce multiaddr (VM with public IP)
|
|
64
|
+
BLITZWING_LOCAL_IP Local IP for shard manager metadata
|
|
65
|
+
PETALS_USE_AUTO_RELAY=0 Disable libp2p auto-relay (use with public IP / port-forward)
|
|
58
66
|
`);
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -130,20 +138,86 @@ async function wizard(args) {
|
|
|
130
138
|
const layersN = Number(layers);
|
|
131
139
|
|
|
132
140
|
const detected = await detectPublicIp();
|
|
133
|
-
const
|
|
134
|
-
message: "
|
|
135
|
-
|
|
141
|
+
const networkMode = await p.select({
|
|
142
|
+
message: "How will other peers reach your Petals node?",
|
|
143
|
+
options: [
|
|
144
|
+
{
|
|
145
|
+
value: "relay",
|
|
146
|
+
label: "Auto (recommended for home / NAT)",
|
|
147
|
+
hint: "Uses Petals libp2p relay — no port forwarding",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
value: "public",
|
|
151
|
+
label: "Public IP / cloud VM",
|
|
152
|
+
hint: "TCP 31337 (and 8001) open on the internet",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
value: "ngrok",
|
|
156
|
+
label: "Dev tunnel (ngrok TCP on :4040)",
|
|
157
|
+
hint: "Only if ngrok is already running",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
initialValue: "relay",
|
|
161
|
+
});
|
|
162
|
+
if (p.isCancel(networkMode)) {
|
|
163
|
+
p.cancel("Setup cancelled");
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let publicIp = "";
|
|
168
|
+
let announceMaddrs = "";
|
|
169
|
+
let useAutoRelay = "1";
|
|
170
|
+
|
|
171
|
+
if (networkMode === "public") {
|
|
172
|
+
publicIp = await p.text({
|
|
173
|
+
message: "Public IPv4 (must be reachable on TCP " + PETALS_PORT + ")",
|
|
174
|
+
initialValue: detected || "",
|
|
175
|
+
validate(v) {
|
|
176
|
+
if (!v || !/^\d+\.\d+\.\d+\.\d+$/.test(String(v).trim())) {
|
|
177
|
+
return "Enter a valid public IPv4 address";
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
if (p.isCancel(publicIp)) {
|
|
182
|
+
p.cancel("Setup cancelled");
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
publicIp = String(publicIp).trim();
|
|
186
|
+
announceMaddrs = `/ip4/${publicIp}/tcp/${PETALS_PORT}`;
|
|
187
|
+
useAutoRelay = "0";
|
|
188
|
+
} else if (networkMode === "ngrok") {
|
|
189
|
+
const ngrok = await discoverNgrokTcpAnnounce(PETALS_PORT);
|
|
190
|
+
if (!ngrok) {
|
|
191
|
+
p.cancel("No ngrok TCP tunnel on http://127.0.0.1:4040. Start: ngrok tcp " + PETALS_PORT);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
announceMaddrs = ngrok;
|
|
195
|
+
publicIp = ngrok.match(/dns4\/([^/]+)/)?.[1] || detected || "relay";
|
|
196
|
+
useAutoRelay = "0";
|
|
197
|
+
p.log.info(`Using ngrok announce ${announceMaddrs}`);
|
|
198
|
+
} else {
|
|
199
|
+
publicIp = detected || "relay";
|
|
200
|
+
p.log.info("Using Petals libp2p auto-relay (no inbound port forward required).");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const localIp = await detectLocalIp();
|
|
204
|
+
|
|
205
|
+
const hederaAccount = await p.text({
|
|
206
|
+
message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
|
|
207
|
+
initialValue: process.env.BLITZWING_HEDERA_ACCOUNT_ID || "",
|
|
136
208
|
validate(v) {
|
|
137
|
-
|
|
209
|
+
const s = String(v || "").trim();
|
|
210
|
+
if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
|
|
138
211
|
},
|
|
139
212
|
});
|
|
140
|
-
if (p.isCancel(
|
|
213
|
+
if (p.isCancel(hederaAccount)) {
|
|
141
214
|
p.cancel("Setup cancelled");
|
|
142
215
|
process.exit(0);
|
|
143
216
|
}
|
|
217
|
+
const hederaAccountId = String(hederaAccount).trim();
|
|
144
218
|
|
|
145
219
|
const confirm = await p.confirm({
|
|
146
|
-
message: `Join ${selected.model} hosting ${layersN} layers
|
|
220
|
+
message: `Join ${selected.model} hosting ${layersN} layers (${networkMode}) paying to ${hederaAccountId}?`,
|
|
147
221
|
initialValue: true,
|
|
148
222
|
});
|
|
149
223
|
if (p.isCancel(confirm) || !confirm) {
|
|
@@ -166,7 +240,8 @@ async function wizard(args) {
|
|
|
166
240
|
const python = venvPython();
|
|
167
241
|
spin.stop("Environment ready");
|
|
168
242
|
|
|
169
|
-
const shardManagerUrl = `http://${
|
|
243
|
+
const shardManagerUrl = `http://${localIp}:${SHARD_PORT}`;
|
|
244
|
+
const petalsLogPath = path.join(HOME_DIR, "petals.log");
|
|
170
245
|
|
|
171
246
|
spin.start("Requesting layer assignment from mother…");
|
|
172
247
|
let assignment;
|
|
@@ -176,6 +251,7 @@ async function wizard(args) {
|
|
|
176
251
|
layers: layersN,
|
|
177
252
|
public_ip: String(publicIp).trim(),
|
|
178
253
|
shard_manager_url: shardManagerUrl,
|
|
254
|
+
hedera_account_id: hederaAccountId,
|
|
179
255
|
});
|
|
180
256
|
} catch (err) {
|
|
181
257
|
spin.stop("Join rejected");
|
|
@@ -187,22 +263,28 @@ async function wizard(args) {
|
|
|
187
263
|
|
|
188
264
|
const logPath = path.join(HOME_DIR, "shard_manager.log");
|
|
189
265
|
spin.start("Starting local Petals server…");
|
|
190
|
-
const
|
|
191
|
-
python,
|
|
192
|
-
logPath,
|
|
193
|
-
env: {
|
|
266
|
+
const shardEnv = {
|
|
194
267
|
MODEL_NAME: selected.model,
|
|
195
|
-
PUBLIC_IP: String(publicIp).trim(),
|
|
268
|
+
PUBLIC_IP: announceMaddrs ? "" : String(publicIp).trim(),
|
|
196
269
|
BLOCK_INDICES: assignment.block_indices,
|
|
197
270
|
INITIAL_PEERS: (assignment.initial_peers || []).join(","),
|
|
198
271
|
PETALS_PORT: String(PETALS_PORT),
|
|
199
272
|
IDENTITY_PATH: path.join(HOME_DIR, "petals-identity"),
|
|
200
273
|
SHARD_AUTO_START: "1",
|
|
201
|
-
|
|
274
|
+
PETALS_USE_AUTO_RELAY: useAutoRelay,
|
|
275
|
+
PETALS_SKIP_REACHABILITY_CHECK: "1",
|
|
276
|
+
};
|
|
277
|
+
if (announceMaddrs) {
|
|
278
|
+
shardEnv.ANNOUNCE_MADDRS = announceMaddrs;
|
|
279
|
+
}
|
|
280
|
+
const { pid } = startShardManagerProcess({
|
|
281
|
+
python,
|
|
282
|
+
logPath,
|
|
283
|
+
env: shardEnv,
|
|
202
284
|
});
|
|
203
285
|
|
|
204
286
|
try {
|
|
205
|
-
await waitForShardRunning({ timeoutMs: 600000 });
|
|
287
|
+
await waitForShardRunning({ timeoutMs: 600000, statusHost: "127.0.0.1" });
|
|
206
288
|
} catch (err) {
|
|
207
289
|
spin.stop("Petals did not become ready");
|
|
208
290
|
p.cancel(`${err.message}. See ${logPath}`);
|
|
@@ -211,8 +293,12 @@ async function wizard(args) {
|
|
|
211
293
|
spin.stop("Petals is serving your layers");
|
|
212
294
|
|
|
213
295
|
spin.start("Finalizing handoff with mother…");
|
|
296
|
+
const peerMultiaddr = extractPeerMultiaddrFromLog(petalsLogPath, fs);
|
|
214
297
|
try {
|
|
215
|
-
await readyHost(selected.mother_url, {
|
|
298
|
+
await readyHost(selected.mother_url, {
|
|
299
|
+
host_id: assignment.host_id,
|
|
300
|
+
peer_multiaddr: peerMultiaddr || undefined,
|
|
301
|
+
});
|
|
216
302
|
} catch (err) {
|
|
217
303
|
spin.stop("Handoff failed");
|
|
218
304
|
p.cancel(err.message);
|
|
@@ -227,9 +313,12 @@ async function wizard(args) {
|
|
|
227
313
|
block_indices: assignment.block_indices,
|
|
228
314
|
layers_hosted: assignment.layers_hosted,
|
|
229
315
|
public_ip: String(publicIp).trim(),
|
|
316
|
+
network_mode: networkMode,
|
|
317
|
+
announce_maddrs: announceMaddrs || null,
|
|
230
318
|
shard_manager_url: shardManagerUrl,
|
|
231
319
|
shard_pid: pid,
|
|
232
320
|
discovery_url: args.discoveryUrl,
|
|
321
|
+
hedera_account_id: hederaAccountId,
|
|
233
322
|
joined_at: new Date().toISOString(),
|
|
234
323
|
};
|
|
235
324
|
saveState(state);
|
|
@@ -281,7 +370,8 @@ async function showStatus() {
|
|
|
281
370
|
console.log(` mother: ${state.mother_url}`);
|
|
282
371
|
console.log(` public_ip: ${state.public_ip}`);
|
|
283
372
|
try {
|
|
284
|
-
const
|
|
373
|
+
const statusHost = "127.0.0.1";
|
|
374
|
+
const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
|
|
285
375
|
if (res.ok) {
|
|
286
376
|
const st = await res.json();
|
|
287
377
|
console.log(` petals_running: ${st.running}`);
|
|
@@ -308,7 +398,8 @@ async function doLeave() {
|
|
|
308
398
|
p.log.warn(`Mother leave call failed: ${err.message}`);
|
|
309
399
|
}
|
|
310
400
|
try {
|
|
311
|
-
|
|
401
|
+
const statusHost = "127.0.0.1";
|
|
402
|
+
await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
|
|
312
403
|
} catch {
|
|
313
404
|
/* ignore */
|
|
314
405
|
}
|