blitzwing 0.1.0 → 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.
- package/package.json +31 -31
- package/src/api.js +2 -0
- package/src/config.js +3 -1
- package/src/install.js +277 -218
package/package.json
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "blitzwing",
|
|
3
|
-
"version": "0.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": "
|
|
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/api.js
CHANGED
package/src/config.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Default Discovery Service URL — override with BLITZWING_DISCOVERY_URL or --discovery-url */
|
|
2
2
|
export const DEFAULT_DISCOVERY_URL =
|
|
3
|
-
process.env.BLITZWING_DISCOVERY_URL ||
|
|
3
|
+
process.env.BLITZWING_DISCOVERY_URL ||
|
|
4
|
+
process.env.npm_package_config_discoveryUrl ||
|
|
5
|
+
"https://f44c-103-98-63-33.ngrok-free.app";
|
|
4
6
|
|
|
5
7
|
export const HOME_DIR = process.env.BLITZWING_HOME || `${process.env.HOME || process.env.USERPROFILE}/.blitzwing`;
|
|
6
8
|
export const STATE_PATH = `${HOME_DIR}/contributor.json`;
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 };
|