blitzwing 0.1.5 → 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.5",
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
@@ -101,13 +101,35 @@ export function ensurePython() {
101
101
 
102
102
  function petalsImportable(python) {
103
103
  try {
104
- execFileSync(python, ["-c", "import petals"], { stdio: "ignore" });
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(pip, ["install", "-U", "pip", "wheel", "setuptools<81"], {
167
+ execFileSync(python, ["-m", "pip", "install", "-U", "pip", "wheel", "setuptools<81"], {
134
168
  stdio: "inherit",
135
169
  env,
136
170
  });
137
171
  execFileSync(
138
- pip,
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(pip, ["install", "grpcio", "grpcio-tools", "protobuf"], {
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
- pip,
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
- pip,
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(pip, ["install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
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
 
@@ -202,8 +239,12 @@ class Mgr:
202
239
  self.block_indices = os.environ["BLOCK_INDICES"]
203
240
  peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
204
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()]
205
244
  self.python = os.environ.get("PETALS_PYTHON", "python")
245
+ self.petals_log = os.environ.get("PETALS_LOG", os.path.expanduser("~/.blitzwing/petals.log"))
206
246
  self._proc = None
247
+ self._log_fp = None
207
248
  self._lock = threading.Lock()
208
249
  self.last_exit_code = None
209
250
 
@@ -212,7 +253,9 @@ class Mgr:
212
253
  "--device", "cpu", "--quant_type", "none",
213
254
  "--block_indices", bi, "--port", str(self.port),
214
255
  "--identity_path", self.identity_path, "--num_handlers", "1"]
215
- if self.public_ip:
256
+ if self.announce_maddrs:
257
+ c += ["--announce_maddrs", *self.announce_maddrs]
258
+ elif self.public_ip:
216
259
  c += ["--public_ip", self.public_ip]
217
260
  if self.initial_peers:
218
261
  c += ["--initial_peers", *self.initial_peers]
@@ -223,7 +266,15 @@ class Mgr:
223
266
  if bi: self.block_indices = bi
224
267
  if self._proc and self._proc.poll() is None:
225
268
  return
226
- 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)
227
278
  self.last_exit_code = None
228
279
 
229
280
  def stop(self):
@@ -232,7 +283,13 @@ class Mgr:
232
283
  self._proc = None
233
284
  return
234
285
  p = self._proc
235
- 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)
236
293
  try: p.wait(timeout=30)
237
294
  except Exception:
238
295
  p.kill(); p.wait(timeout=10)
@@ -243,6 +300,9 @@ class Mgr:
243
300
  self.stop(); time.sleep(1); self.start(bi)
244
301
 
245
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
246
306
  running = self._proc is not None and self._proc.poll() is None
247
307
  return StatusResponse(
248
308
  running=running,
@@ -292,12 +352,13 @@ export function startShardManagerProcess({
292
352
  }) {
293
353
  const appPath = writeLocalShardManager();
294
354
  const out = fs.openSync(logPath, "a");
355
+ const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
295
356
  const child = spawn(
296
- python,
357
+ petalsPy,
297
358
  ["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
298
359
  {
299
360
  cwd: path.dirname(appPath),
300
- env: { ...process.env, ...env, PETALS_PYTHON: python },
361
+ env: { ...process.env, ...env, PETALS_PYTHON: petalsPy, PETALS_LOG: path.join(HOME_DIR, "petals.log") },
301
362
  detached: true,
302
363
  stdio: ["ignore", out, out],
303
364
  }
@@ -306,21 +367,37 @@ export function startShardManagerProcess({
306
367
  return { pid: child.pid, logPath, appPath };
307
368
  }
308
369
 
309
- 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");
310
373
  const start = Date.now();
311
374
  while (Date.now() - start < timeoutMs) {
312
375
  try {
313
- const res = await fetch(`http://127.0.0.1:${SHARD_PORT}/status`);
376
+ const res = await fetch(`http://${host}:${SHARD_PORT}/status`);
314
377
  if (res.ok) {
315
378
  const body = await res.json();
316
- 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
+ }
317
388
  }
318
- } catch {
389
+ } catch (err) {
390
+ if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
319
391
  /* retry */
320
392
  }
321
393
  await new Promise((r) => setTimeout(r, 2000));
322
394
  }
323
- 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
+ );
324
401
  }
325
402
 
326
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
  }