blitzwing 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/package.json +31 -31
  2. package/src/install.js +277 -218
package/package.json CHANGED
@@ -1,31 +1,31 @@
1
- {
2
- "name": "blitzwing",
3
- "version": "0.1.1",
4
- "description": "Interactive setup wizard to join a Blitzwing Petals mother swarm as a compute contributor",
5
- "bin": {
6
- "blitzwing": "bin/blitzwing.js"
7
- },
8
- "type": "module",
9
- "engines": {
10
- "node": ">=18"
11
- },
12
- "files": [
13
- "bin",
14
- "src",
15
- "README.md"
16
- ],
17
- "keywords": [
18
- "blitzwing",
19
- "petals",
20
- "llm",
21
- "distributed-inference"
22
- ],
23
- "license": "MIT",
24
- "config": {
25
- "discoveryUrl": "https://f44c-103-98-63-33.ngrok-free.app"
26
- },
27
- "dependencies": {
28
- "@clack/prompts": "^0.9.1",
29
- "picocolors": "^1.1.1"
30
- }
31
- }
1
+ {
2
+ "name": "blitzwing",
3
+ "version": "0.1.2",
4
+ "description": "Interactive setup wizard to join a Blitzwing Petals mother swarm as a compute contributor",
5
+ "bin": {
6
+ "blitzwing": "bin/blitzwing.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "blitzwing",
19
+ "petals",
20
+ "llm",
21
+ "distributed-inference"
22
+ ],
23
+ "license": "MIT",
24
+ "config": {
25
+ "discoveryUrl": "https://f44c-103-98-63-33.ngrok-free.app"
26
+ },
27
+ "dependencies": {
28
+ "@clack/prompts": "^0.9.1",
29
+ "picocolors": "^1.1.1"
30
+ }
31
+ }
package/src/install.js CHANGED
@@ -1,218 +1,277 @@
1
- import { spawn, execFileSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { HOME_DIR, VENV_PATH, SHARD_PORT, PETALS_PORT } from "./config.js";
5
- import { which } from "./net.js";
6
-
7
- function venvPython() {
8
- if (process.platform === "win32") {
9
- return path.join(VENV_PATH, "Scripts", "python.exe");
10
- }
11
- return path.join(VENV_PATH, "bin", "python");
12
- }
13
-
14
- function venvPip() {
15
- if (process.platform === "win32") {
16
- return path.join(VENV_PATH, "Scripts", "pip.exe");
17
- }
18
- return path.join(VENV_PATH, "bin", "pip");
19
- }
20
-
21
- export function ensurePython() {
22
- const py = which("python3") || which("python");
23
- if (!py) {
24
- throw new Error("Python 3 is required. Install Python 3.10+ and re-run blitzwing.");
25
- }
26
- const ver = execFileSync(py, ["--version"], { encoding: "utf8" });
27
- return { py, ver: ver.trim() };
28
- }
29
-
30
- export function ensureVenvAndPetals({ onLog }) {
31
- const { py } = ensurePython();
32
- fs.mkdirSync(HOME_DIR, { recursive: true });
33
-
34
- if (!fs.existsSync(venvPython())) {
35
- onLog?.(`Creating venv at ${VENV_PATH}`);
36
- execFileSync(py, ["-m", "venv", VENV_PATH], { stdio: "inherit" });
37
- }
38
-
39
- const pip = venvPip();
40
- const python = venvPython();
41
-
42
- onLog?.("Installing CPU PyTorch + Petals (may take a few minutes on first run)...");
43
- execFileSync(pip, ["install", "-U", "pip"], { stdio: "inherit" });
44
- execFileSync(
45
- pip,
46
- ["install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
47
- { stdio: "inherit" }
48
- );
49
- // Prefer git install for contributors without the monorepo
50
- execFileSync(
51
- pip,
52
- ["install", "git+https://github.com/bigscience-workshop/petals.git"],
53
- { stdio: "inherit" }
54
- );
55
- execFileSync(pip, ["install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
56
- stdio: "inherit",
57
- });
58
-
59
- return { python, pip };
60
- }
61
-
62
- /**
63
- * Write a tiny local shard-manager runner script so contributors don't need the monorepo.
64
- * Embeds a minimal supervisor compatible with mother /reload API.
65
- */
66
- export function writeLocalShardManager() {
67
- const dir = path.join(HOME_DIR, "runtime");
68
- fs.mkdirSync(dir, { recursive: true });
69
- const appPath = path.join(dir, "shard_manager_app.py");
70
- const code = `
71
- import os, signal, subprocess, threading, time
72
- from typing import Optional, List
73
- from fastapi import FastAPI, HTTPException
74
- from pydantic import BaseModel, Field
75
-
76
- class ReloadRequest(BaseModel):
77
- block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
78
-
79
- class StatusResponse(BaseModel):
80
- running: bool
81
- pid: Optional[int] = None
82
- block_indices: Optional[str] = None
83
- model: str
84
- public_ip: Optional[str] = None
85
- port: int
86
- last_exit_code: Optional[int] = None
87
-
88
- class Mgr:
89
- def __init__(self):
90
- self.model = os.environ["MODEL_NAME"]
91
- self.public_ip = os.environ.get("PUBLIC_IP")
92
- self.port = int(os.environ.get("PETALS_PORT", "31337"))
93
- self.identity_path = os.environ.get("IDENTITY_PATH", os.path.expanduser("~/.blitzwing/petals-identity"))
94
- self.block_indices = os.environ["BLOCK_INDICES"]
95
- peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
96
- self.initial_peers = peers
97
- self.python = os.environ.get("PETALS_PYTHON", "python")
98
- self._proc = None
99
- self._lock = threading.Lock()
100
- self.last_exit_code = None
101
-
102
- def cmd(self, bi):
103
- c = [self.python, "-m", "petals.cli.run_server", self.model,
104
- "--device", "cpu", "--quant_type", "none",
105
- "--block_indices", bi, "--port", str(self.port),
106
- "--identity_path", self.identity_path, "--num_handlers", "1"]
107
- if self.public_ip:
108
- c += ["--public_ip", self.public_ip]
109
- if self.initial_peers:
110
- c += ["--initial_peers", *self.initial_peers]
111
- return c
112
-
113
- def start(self, bi=None):
114
- with self._lock:
115
- if bi: self.block_indices = bi
116
- if self._proc and self._proc.poll() is None:
117
- return
118
- self._proc = subprocess.Popen(self.cmd(self.block_indices))
119
- self.last_exit_code = None
120
-
121
- def stop(self):
122
- with self._lock:
123
- if not self._proc or self._proc.poll() is not None:
124
- self._proc = None
125
- return
126
- p = self._proc
127
- p.send_signal(signal.SIGTERM)
128
- try: p.wait(timeout=30)
129
- except Exception:
130
- p.kill(); p.wait(timeout=10)
131
- self.last_exit_code = p.returncode
132
- self._proc = None
133
-
134
- def reload(self, bi):
135
- self.stop(); time.sleep(1); self.start(bi)
136
-
137
- def status(self):
138
- running = self._proc is not None and self._proc.poll() is None
139
- return StatusResponse(
140
- running=running,
141
- pid=(self._proc.pid if running else None),
142
- block_indices=self.block_indices,
143
- model=self.model,
144
- public_ip=self.public_ip,
145
- port=self.port,
146
- last_exit_code=self.last_exit_code,
147
- )
148
-
149
- mgr = Mgr()
150
- app = FastAPI()
151
-
152
- @app.on_event("startup")
153
- def _up():
154
- mgr.start()
155
-
156
- @app.on_event("shutdown")
157
- def _down():
158
- mgr.stop()
159
-
160
- @app.get("/status")
161
- def status():
162
- return mgr.status()
163
-
164
- @app.post("/reload")
165
- def reload(body: ReloadRequest):
166
- a,b = map(int, body.block_indices.split(":"))
167
- if b <= a: raise HTTPException(400, "bad range")
168
- mgr.reload(body.block_indices)
169
- time.sleep(0.5)
170
- return mgr.status()
171
-
172
- @app.post("/stop")
173
- def stop():
174
- mgr.stop(); return mgr.status()
175
- `;
176
- fs.writeFileSync(appPath, code);
177
- return appPath;
178
- }
179
-
180
- export function startShardManagerProcess({
181
- python,
182
- env,
183
- logPath,
184
- }) {
185
- const appPath = writeLocalShardManager();
186
- const out = fs.openSync(logPath, "a");
187
- const child = spawn(
188
- python,
189
- ["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
190
- {
191
- cwd: path.dirname(appPath),
192
- env: { ...process.env, ...env, PETALS_PYTHON: python },
193
- detached: true,
194
- stdio: ["ignore", out, out],
195
- }
196
- );
197
- child.unref();
198
- return { pid: child.pid, logPath, appPath };
199
- }
200
-
201
- export async function waitForShardRunning({ timeoutMs = 300000 } = {}) {
202
- const start = Date.now();
203
- while (Date.now() - start < timeoutMs) {
204
- try {
205
- const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
206
- if (res.ok) {
207
- const body = await res.json();
208
- if (body.running) return body;
209
- }
210
- } catch {
211
- /* retry */
212
- }
213
- await new Promise((r) => setTimeout(r, 2000));
214
- }
215
- throw new Error("Timed out waiting for local Petals shard manager to become ready");
216
- }
217
-
218
- export { SHARD_PORT, PETALS_PORT, venvPython };
1
+ import { spawn, execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { HOME_DIR, VENV_PATH, SHARD_PORT, PETALS_PORT } from "./config.js";
5
+ import { which } from "./net.js";
6
+
7
+ function venvPython() {
8
+ if (process.platform === "win32") {
9
+ return path.join(VENV_PATH, "Scripts", "python.exe");
10
+ }
11
+ return path.join(VENV_PATH, "bin", "python");
12
+ }
13
+
14
+ function venvPip() {
15
+ if (process.platform === "win32") {
16
+ return path.join(VENV_PATH, "Scripts", "pip.exe");
17
+ }
18
+ return path.join(VENV_PATH, "bin", "pip");
19
+ }
20
+
21
+ export function ensurePython() {
22
+ // Prefer 3.10/3.11 Petals/hivemind break on 3.12+ / 3.13+
23
+ const candidates = ["python3.11", "python3.10", "python3", "python"];
24
+ let py = null;
25
+ let ver = "";
26
+ for (const cmd of candidates) {
27
+ const found = which(cmd);
28
+ if (!found) continue;
29
+ try {
30
+ ver = execFileSync(found, ["--version"], { encoding: "utf8" }).trim();
31
+ const m = ver.match(/Python (\d+)\.(\d+)/);
32
+ if (!m) continue;
33
+ const major = Number(m[1]);
34
+ const minor = Number(m[2]);
35
+ if (major === 3 && minor >= 10 && minor <= 11) {
36
+ py = found;
37
+ break;
38
+ }
39
+ if (!py) {
40
+ py = found; // fallback, may fail later
41
+ }
42
+ } catch {
43
+ /* try next */
44
+ }
45
+ }
46
+ if (!py) {
47
+ throw new Error("Python 3.10 or 3.11 is required. Install via conda/miniconda and re-run.");
48
+ }
49
+ const m = ver.match(/Python (\d+)\.(\d+)/);
50
+ if (m && (Number(m[1]) !== 3 || Number(m[2]) > 11)) {
51
+ throw new Error(
52
+ `${ver} is too new for Petals. Use Python 3.11 (e.g. conda create -n blitzwing python=3.11).`
53
+ );
54
+ }
55
+ return { py, ver };
56
+ }
57
+
58
+ function petalsImportable(python) {
59
+ try {
60
+ execFileSync(python, ["-c", "import petals"], { stdio: "ignore" });
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ export function ensureVenvAndPetals({ onLog }) {
68
+ const { py } = ensurePython();
69
+ fs.mkdirSync(HOME_DIR, { recursive: true });
70
+
71
+ if (!fs.existsSync(venvPython())) {
72
+ onLog?.(`Creating venv at ${VENV_PATH}`);
73
+ execFileSync(py, ["-m", "venv", VENV_PATH], { stdio: "inherit" });
74
+ }
75
+
76
+ const pip = venvPip();
77
+ const python = venvPython();
78
+ const env = {
79
+ ...process.env,
80
+ PIP_NO_BUILD_ISOLATION: "1",
81
+ };
82
+
83
+ if (petalsImportable(python)) {
84
+ onLog?.("Petals already installed — skipping dependency install");
85
+ return { python, pip };
86
+ }
87
+
88
+ onLog?.("Installing CPU PyTorch + Petals (may take a few minutes on first run)...");
89
+ execFileSync(pip, ["install", "-U", "pip", "wheel", "setuptools<81"], {
90
+ stdio: "inherit",
91
+ env,
92
+ });
93
+ execFileSync(
94
+ pip,
95
+ ["install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
96
+ { stdio: "inherit", env }
97
+ );
98
+ // Hivemind first with no build isolation (needs pkg_resources from setuptools<81)
99
+ execFileSync(
100
+ pip,
101
+ [
102
+ "install",
103
+ "--no-build-isolation",
104
+ "git+https://github.com/learning-at-home/hivemind.git@213bff98a62accb91f254e2afdccbf1d69ebdea9",
105
+ ],
106
+ { stdio: "inherit", env }
107
+ );
108
+ execFileSync(
109
+ pip,
110
+ ["install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
111
+ { stdio: "inherit", env }
112
+ );
113
+ execFileSync(pip, ["install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
114
+ stdio: "inherit",
115
+ env,
116
+ });
117
+
118
+ return { python, pip };
119
+ }
120
+
121
+ /**
122
+ * Write a tiny local shard-manager runner script so contributors don't need the monorepo.
123
+ * Embeds a minimal supervisor compatible with mother /reload API.
124
+ */
125
+ export function writeLocalShardManager() {
126
+ const dir = path.join(HOME_DIR, "runtime");
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ const appPath = path.join(dir, "shard_manager_app.py");
129
+ const code = `
130
+ import os, signal, subprocess, threading, time
131
+ from typing import Optional, List
132
+ from fastapi import FastAPI, HTTPException
133
+ from pydantic import BaseModel, Field
134
+
135
+ class ReloadRequest(BaseModel):
136
+ block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
137
+
138
+ class StatusResponse(BaseModel):
139
+ running: bool
140
+ pid: Optional[int] = None
141
+ block_indices: Optional[str] = None
142
+ model: str
143
+ public_ip: Optional[str] = None
144
+ port: int
145
+ last_exit_code: Optional[int] = None
146
+
147
+ class Mgr:
148
+ def __init__(self):
149
+ self.model = os.environ["MODEL_NAME"]
150
+ self.public_ip = os.environ.get("PUBLIC_IP")
151
+ self.port = int(os.environ.get("PETALS_PORT", "31337"))
152
+ self.identity_path = os.environ.get("IDENTITY_PATH", os.path.expanduser("~/.blitzwing/petals-identity"))
153
+ self.block_indices = os.environ["BLOCK_INDICES"]
154
+ peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
155
+ self.initial_peers = peers
156
+ self.python = os.environ.get("PETALS_PYTHON", "python")
157
+ self._proc = None
158
+ self._lock = threading.Lock()
159
+ self.last_exit_code = None
160
+
161
+ def cmd(self, bi):
162
+ c = [self.python, "-m", "petals.cli.run_server", self.model,
163
+ "--device", "cpu", "--quant_type", "none",
164
+ "--block_indices", bi, "--port", str(self.port),
165
+ "--identity_path", self.identity_path, "--num_handlers", "1"]
166
+ if self.public_ip:
167
+ c += ["--public_ip", self.public_ip]
168
+ if self.initial_peers:
169
+ c += ["--initial_peers", *self.initial_peers]
170
+ return c
171
+
172
+ def start(self, bi=None):
173
+ with self._lock:
174
+ if bi: self.block_indices = bi
175
+ if self._proc and self._proc.poll() is None:
176
+ return
177
+ self._proc = subprocess.Popen(self.cmd(self.block_indices))
178
+ self.last_exit_code = None
179
+
180
+ def stop(self):
181
+ with self._lock:
182
+ if not self._proc or self._proc.poll() is not None:
183
+ self._proc = None
184
+ return
185
+ p = self._proc
186
+ p.send_signal(signal.SIGTERM)
187
+ try: p.wait(timeout=30)
188
+ except Exception:
189
+ p.kill(); p.wait(timeout=10)
190
+ self.last_exit_code = p.returncode
191
+ self._proc = None
192
+
193
+ def reload(self, bi):
194
+ self.stop(); time.sleep(1); self.start(bi)
195
+
196
+ def status(self):
197
+ running = self._proc is not None and self._proc.poll() is None
198
+ return StatusResponse(
199
+ running=running,
200
+ pid=(self._proc.pid if running else None),
201
+ block_indices=self.block_indices,
202
+ model=self.model,
203
+ public_ip=self.public_ip,
204
+ port=self.port,
205
+ last_exit_code=self.last_exit_code,
206
+ )
207
+
208
+ mgr = Mgr()
209
+ app = FastAPI()
210
+
211
+ @app.on_event("startup")
212
+ def _up():
213
+ mgr.start()
214
+
215
+ @app.on_event("shutdown")
216
+ def _down():
217
+ mgr.stop()
218
+
219
+ @app.get("/status")
220
+ def status():
221
+ return mgr.status()
222
+
223
+ @app.post("/reload")
224
+ def reload(body: ReloadRequest):
225
+ a,b = map(int, body.block_indices.split(":"))
226
+ if b <= a: raise HTTPException(400, "bad range")
227
+ mgr.reload(body.block_indices)
228
+ time.sleep(0.5)
229
+ return mgr.status()
230
+
231
+ @app.post("/stop")
232
+ def stop():
233
+ mgr.stop(); return mgr.status()
234
+ `;
235
+ fs.writeFileSync(appPath, code);
236
+ return appPath;
237
+ }
238
+
239
+ export function startShardManagerProcess({
240
+ python,
241
+ env,
242
+ logPath,
243
+ }) {
244
+ const appPath = writeLocalShardManager();
245
+ const out = fs.openSync(logPath, "a");
246
+ const child = spawn(
247
+ python,
248
+ ["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
249
+ {
250
+ cwd: path.dirname(appPath),
251
+ env: { ...process.env, ...env, PETALS_PYTHON: python },
252
+ detached: true,
253
+ stdio: ["ignore", out, out],
254
+ }
255
+ );
256
+ child.unref();
257
+ return { pid: child.pid, logPath, appPath };
258
+ }
259
+
260
+ export async function waitForShardRunning({ timeoutMs = 300000 } = {}) {
261
+ const start = Date.now();
262
+ while (Date.now() - start < timeoutMs) {
263
+ try {
264
+ const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
265
+ if (res.ok) {
266
+ const body = await res.json();
267
+ if (body.running) return body;
268
+ }
269
+ } catch {
270
+ /* retry */
271
+ }
272
+ await new Promise((r) => setTimeout(r, 2000));
273
+ }
274
+ throw new Error("Timed out waiting for local Petals shard manager to become ready");
275
+ }
276
+
277
+ export { SHARD_PORT, PETALS_PORT, venvPython };