blitzwing 0.1.4 → 0.1.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blitzwing",
3
- "version": "0.1.4",
3
+ "version": "0.1.8",
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": "https://f44c-103-98-63-33.ngrok-free.app"
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
- "https://f44c-103-98-63-33.ngrok-free.app";
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
- export const SHARD_PORT = Number(process.env.BLITZWING_SHARD_PORT || 8001);
11
- export const PETALS_PORT = Number(process.env.BLITZWING_PETALS_PORT || 31337);
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
@@ -33,11 +33,25 @@ function isPetalsCompatible(info) {
33
33
  return info && info.major === 3 && info.minor >= 10 && info.minor <= 11;
34
34
  }
35
35
 
36
+ function pinnedPythonPath() {
37
+ try {
38
+ const p = path.join(HOME_DIR, "python");
39
+ if (!fs.existsSync(p)) return null;
40
+ const bin = fs.readFileSync(p, "utf8").trim();
41
+ return bin || null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
36
47
  export function ensurePython() {
37
48
  // Petals/hivemind break on 3.12+. Never fall back to a too-new interpreter.
49
+ const home = process.env.HOME || process.env.USERPROFILE || "";
38
50
  const candidates = [];
39
51
  const envBin = process.env.BLITZWING_PYTHON || process.env.PETALS_PYTHON;
40
52
  if (envBin) candidates.push(envBin);
53
+ const pinned = pinnedPythonPath();
54
+ if (pinned) candidates.push(pinned);
41
55
  if (process.env.CONDA_PREFIX) {
42
56
  candidates.push(
43
57
  path.join(process.env.CONDA_PREFIX, "bin", "python3.11"),
@@ -45,43 +59,77 @@ export function ensurePython() {
45
59
  path.join(process.env.CONDA_PREFIX, "bin", "python")
46
60
  );
47
61
  }
48
- candidates.push(
49
- path.join(process.env.HOME || "", "miniconda", "envs", "blitzwing", "bin", "python3.11"),
50
- path.join(process.env.HOME || "", "miniconda", "envs", "blitzwing", "bin", "python"),
51
- path.join(process.env.HOME || "", "miniconda3", "envs", "blitzwing", "bin", "python3.11"),
52
- path.join(process.env.HOME || "", "miniconda3", "envs", "blitzwing", "bin", "python"),
53
- "python3.11",
54
- "python3.10"
55
- );
62
+ for (const root of ["miniforge3", "mambaforge", "miniconda", "miniconda3", "anaconda3"]) {
63
+ candidates.push(
64
+ path.join(home, root, "envs", "blitzwing", "bin", "python3.11"),
65
+ path.join(home, root, "envs", "blitzwing", "bin", "python")
66
+ );
67
+ }
68
+ candidates.push("python3.11", "python3.10");
56
69
 
70
+ const tried = [];
57
71
  const seen = new Set();
58
72
  for (const cmd of candidates) {
59
73
  if (!cmd || seen.has(cmd)) continue;
60
74
  seen.add(cmd);
61
75
  const found = cmd.includes("/") || cmd.includes("\\") ? (fs.existsSync(cmd) ? cmd : null) : which(cmd);
62
- if (!found) continue;
76
+ if (!found) {
77
+ tried.push(`${cmd} (missing)`);
78
+ continue;
79
+ }
63
80
  const info = pythonVersion(found);
64
81
  if (isPetalsCompatible(info)) {
82
+ fs.mkdirSync(HOME_DIR, { recursive: true });
83
+ fs.writeFileSync(path.join(HOME_DIR, "python"), found + "\n");
65
84
  return { py: found, ver: info.ver };
66
85
  }
86
+ tried.push(`${found} (${info ? info.ver : "unreadable"})`);
67
87
  }
68
88
 
69
89
  throw new Error(
70
- "Python 3.10 or 3.11 is required (3.12+ will not work). " +
71
- "Install with: conda create -n blitzwing python=3.11 && conda activate blitzwing " +
72
- "or set BLITZWING_PYTHON=/path/to/python3.11"
90
+ "Python 3.10 or 3.11 is required (3.12+ will not work).\n" +
91
+ "Checked:\n - " +
92
+ tried.slice(0, 12).join("\n - ") +
93
+ "\nFix:\n" +
94
+ " curl -LsSf https://astral.sh/uv/install.sh | sh && source $HOME/.local/bin/env\n" +
95
+ " uv python install 3.11\n" +
96
+ " export BLITZWING_PYTHON=\"$(uv python find 3.11)\"\n" +
97
+ " mkdir -p ~/.blitzwing && echo \"$BLITZWING_PYTHON\" > ~/.blitzwing/python\n" +
98
+ " blitzwing"
73
99
  );
74
100
  }
75
101
 
76
102
  function petalsImportable(python) {
77
103
  try {
78
- execFileSync(python, ["-c", "import petals"], { stdio: "ignore" });
104
+ execFileSync(
105
+ python,
106
+ ["-c", "import petals.cli.run_server"],
107
+ { stdio: "ignore" }
108
+ );
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function uvicornImportable(python) {
116
+ try {
117
+ execFileSync(python, ["-c", "import uvicorn, fastapi"], { stdio: "ignore" });
79
118
  return true;
80
119
  } catch {
81
120
  return false;
82
121
  }
83
122
  }
84
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
+
85
133
  export function ensureVenvAndPetals({ onLog }) {
86
134
  const { py } = ensurePython();
87
135
  fs.mkdirSync(HOME_DIR, { recursive: true });
@@ -99,29 +147,43 @@ export function ensureVenvAndPetals({ onLog }) {
99
147
  };
100
148
 
101
149
  if (petalsImportable(python)) {
150
+ ensureShardManagerDeps(python, pip, env, onLog);
102
151
  onLog?.("Petals already installed — skipping dependency install");
152
+ fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
103
153
  return { python, pip };
104
154
  }
105
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
+
106
166
  onLog?.("Installing CPU PyTorch + Petals (may take a few minutes on first run)...");
107
- execFileSync(pip, ["install", "-U", "pip", "wheel", "setuptools<81"], {
167
+ execFileSync(python, ["-m", "pip", "install", "-U", "pip", "wheel", "setuptools<81"], {
108
168
  stdio: "inherit",
109
169
  env,
110
170
  });
111
171
  execFileSync(
112
- pip,
113
- ["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"],
114
174
  { stdio: "inherit", env }
115
175
  );
116
176
  // Needed to compile hivemind protobufs during --no-build-isolation install
117
- execFileSync(pip, ["install", "grpcio", "grpcio-tools", "protobuf"], {
177
+ execFileSync(python, ["-m", "pip", "install", "grpcio", "grpcio-tools", "protobuf"], {
118
178
  stdio: "inherit",
119
179
  env,
120
180
  });
121
181
  // Hivemind first with no build isolation (needs pkg_resources from setuptools<81)
122
182
  execFileSync(
123
- pip,
183
+ python,
124
184
  [
185
+ "-m",
186
+ "pip",
125
187
  "install",
126
188
  "--no-build-isolation",
127
189
  "git+https://github.com/learning-at-home/hivemind.git@213bff98a62accb91f254e2afdccbf1d69ebdea9",
@@ -129,15 +191,16 @@ export function ensureVenvAndPetals({ onLog }) {
129
191
  { stdio: "inherit", env }
130
192
  );
131
193
  execFileSync(
132
- pip,
133
- ["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"],
134
196
  { stdio: "inherit", env }
135
197
  );
136
- execFileSync(pip, ["install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
198
+ execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
137
199
  stdio: "inherit",
138
200
  env,
139
201
  });
140
202
 
203
+ fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
141
204
  return { python, pip };
142
205
  }
143
206
 
@@ -176,8 +239,12 @@ class Mgr:
176
239
  self.block_indices = os.environ["BLOCK_INDICES"]
177
240
  peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
178
241
  self.initial_peers = peers
242
+ announce_raw = os.environ.get("ANNOUNCE_MADDRS", "")
243
+ self.announce_maddrs = [a.strip() for a in announce_raw.split(",") if a.strip()]
179
244
  self.python = os.environ.get("PETALS_PYTHON", "python")
245
+ self.petals_log = os.environ.get("PETALS_LOG", os.path.expanduser("~/.blitzwing/petals.log"))
180
246
  self._proc = None
247
+ self._log_fp = None
181
248
  self._lock = threading.Lock()
182
249
  self.last_exit_code = None
183
250
 
@@ -186,7 +253,9 @@ class Mgr:
186
253
  "--device", "cpu", "--quant_type", "none",
187
254
  "--block_indices", bi, "--port", str(self.port),
188
255
  "--identity_path", self.identity_path, "--num_handlers", "1"]
189
- if self.public_ip:
256
+ if self.announce_maddrs:
257
+ c += ["--announce_maddrs", *self.announce_maddrs]
258
+ elif self.public_ip:
190
259
  c += ["--public_ip", self.public_ip]
191
260
  if self.initial_peers:
192
261
  c += ["--initial_peers", *self.initial_peers]
@@ -197,7 +266,15 @@ class Mgr:
197
266
  if bi: self.block_indices = bi
198
267
  if self._proc and self._proc.poll() is None:
199
268
  return
200
- self._proc = subprocess.Popen(self.cmd(self.block_indices))
269
+ if self._log_fp is None:
270
+ os.makedirs(os.path.dirname(self.petals_log) or ".", exist_ok=True)
271
+ self._log_fp = open(self.petals_log, "a", encoding="utf-8")
272
+ popen_kwargs = {"stdout": self._log_fp, "stderr": subprocess.STDOUT}
273
+ if os.name == "nt":
274
+ popen_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
275
+ else:
276
+ popen_kwargs["preexec_fn"] = os.setsid
277
+ self._proc = subprocess.Popen(self.cmd(self.block_indices), **popen_kwargs)
201
278
  self.last_exit_code = None
202
279
 
203
280
  def stop(self):
@@ -206,7 +283,13 @@ class Mgr:
206
283
  self._proc = None
207
284
  return
208
285
  p = self._proc
209
- p.send_signal(signal.SIGTERM)
286
+ try:
287
+ if os.name == "nt":
288
+ p.send_signal(signal.CTRL_BREAK_EVENT)
289
+ else:
290
+ os.killpg(os.getpgid(p.pid), signal.SIGTERM)
291
+ except Exception:
292
+ p.send_signal(signal.SIGTERM)
210
293
  try: p.wait(timeout=30)
211
294
  except Exception:
212
295
  p.kill(); p.wait(timeout=10)
@@ -217,6 +300,9 @@ class Mgr:
217
300
  self.stop(); time.sleep(1); self.start(bi)
218
301
 
219
302
  def status(self):
303
+ if self._proc is not None and self._proc.poll() is not None:
304
+ self.last_exit_code = self._proc.returncode
305
+ self._proc = None
220
306
  running = self._proc is not None and self._proc.poll() is None
221
307
  return StatusResponse(
222
308
  running=running,
@@ -266,12 +352,13 @@ export function startShardManagerProcess({
266
352
  }) {
267
353
  const appPath = writeLocalShardManager();
268
354
  const out = fs.openSync(logPath, "a");
355
+ const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
269
356
  const child = spawn(
270
- python,
357
+ petalsPy,
271
358
  ["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
272
359
  {
273
360
  cwd: path.dirname(appPath),
274
- env: { ...process.env, ...env, PETALS_PYTHON: python },
361
+ env: { ...process.env, ...env, PETALS_PYTHON: petalsPy, PETALS_LOG: path.join(HOME_DIR, "petals.log") },
275
362
  detached: true,
276
363
  stdio: ["ignore", out, out],
277
364
  }
@@ -280,21 +367,37 @@ export function startShardManagerProcess({
280
367
  return { pid: child.pid, logPath, appPath };
281
368
  }
282
369
 
283
- export async function waitForShardRunning({ timeoutMs = 300000 } = {}) {
370
+ export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {}) {
371
+ const host = statusHost || process.env.BLITZWING_STATUS_HOST || "127.0.0.1";
372
+ const petalsLog = path.join(HOME_DIR, "petals.log");
284
373
  const start = Date.now();
285
374
  while (Date.now() - start < timeoutMs) {
286
375
  try {
287
- const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
376
+ const res = await fetch(`http://${host}:${SHARD_PORT}/status`);
288
377
  if (res.ok) {
289
378
  const body = await res.json();
290
- if (body.running) return body;
379
+ if (body.running && body.pid) return body;
380
+ if (body.last_exit_code != null) {
381
+ const tail = fs.existsSync(petalsLog)
382
+ ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
383
+ : "";
384
+ throw new Error(
385
+ `Petals exited (code ${body.last_exit_code}). ${tail || "See " + petalsLog}`
386
+ );
387
+ }
291
388
  }
292
- } catch {
389
+ } catch (err) {
390
+ if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
293
391
  /* retry */
294
392
  }
295
393
  await new Promise((r) => setTimeout(r, 2000));
296
394
  }
297
- throw new Error("Timed out waiting for local Petals shard manager to become ready");
395
+ const tail = fs.existsSync(petalsLog)
396
+ ? fs.readFileSync(petalsLog, "utf8").trim().split("\n").slice(-5).join("\n")
397
+ : "";
398
+ throw new Error(
399
+ `Timed out waiting for Petals on http://${host}:${SHARD_PORT}/status. ${tail || "See " + petalsLog}`
400
+ );
298
401
  }
299
402
 
300
403
  export { SHARD_PORT, PETALS_PORT, venvPython };
package/src/wizard.js CHANGED
@@ -142,8 +142,22 @@ async function wizard(args) {
142
142
  process.exit(0);
143
143
  }
144
144
 
145
+ const hederaAccount = await p.text({
146
+ message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
147
+ initialValue: process.env.BLITZWING_HEDERA_ACCOUNT_ID || "",
148
+ validate(v) {
149
+ const s = String(v || "").trim();
150
+ if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
151
+ },
152
+ });
153
+ if (p.isCancel(hederaAccount)) {
154
+ p.cancel("Setup cancelled");
155
+ process.exit(0);
156
+ }
157
+ const hederaAccountId = String(hederaAccount).trim();
158
+
145
159
  const confirm = await p.confirm({
146
- message: `Join ${selected.model} hosting ${layersN} layers from ${String(publicIp).trim()}?`,
160
+ message: `Join ${selected.model} hosting ${layersN} layers from ${String(publicIp).trim()} paying to ${hederaAccountId}?`,
147
161
  initialValue: true,
148
162
  });
149
163
  if (p.isCancel(confirm) || !confirm) {
@@ -176,6 +190,7 @@ async function wizard(args) {
176
190
  layers: layersN,
177
191
  public_ip: String(publicIp).trim(),
178
192
  shard_manager_url: shardManagerUrl,
193
+ hedera_account_id: hederaAccountId,
179
194
  });
180
195
  } catch (err) {
181
196
  spin.stop("Join rejected");
@@ -202,7 +217,7 @@ async function wizard(args) {
202
217
  });
203
218
 
204
219
  try {
205
- await waitForShardRunning({ timeoutMs: 600000 });
220
+ await waitForShardRunning({ timeoutMs: 600000, statusHost: String(publicIp).trim() });
206
221
  } catch (err) {
207
222
  spin.stop("Petals did not become ready");
208
223
  p.cancel(`${err.message}. See ${logPath}`);
@@ -230,6 +245,7 @@ async function wizard(args) {
230
245
  shard_manager_url: shardManagerUrl,
231
246
  shard_pid: pid,
232
247
  discovery_url: args.discoveryUrl,
248
+ hedera_account_id: hederaAccountId,
233
249
  joined_at: new Date().toISOString(),
234
250
  };
235
251
  saveState(state);
@@ -281,7 +297,8 @@ async function showStatus() {
281
297
  console.log(` mother: ${state.mother_url}`);
282
298
  console.log(` public_ip: ${state.public_ip}`);
283
299
  try {
284
- const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
300
+ const statusHost = state.public_ip || "127.0.0.1";
301
+ const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
285
302
  if (res.ok) {
286
303
  const st = await res.json();
287
304
  console.log(` petals_running: ${st.running}`);
@@ -308,7 +325,8 @@ async function doLeave() {
308
325
  p.log.warn(`Mother leave call failed: ${err.message}`);
309
326
  }
310
327
  try {
311
- await fetch(`http://127.0.0.1:${SHARD_PORT}/stop`, { method: "POST" });
328
+ const statusHost = state.public_ip || "127.0.0.1";
329
+ await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
312
330
  } catch {
313
331
  /* ignore */
314
332
  }