blitzwing 0.1.10 → 0.2.0

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.
@@ -0,0 +1,148 @@
1
+ """Petals client that only talks to the local server (no remote DHT)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ import threading
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Optional, Tuple
12
+
13
+ import torch
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def parse_range(block_indices: str) -> Tuple[int, int]:
19
+ start_s, end_s = block_indices.split(":")
20
+ return int(start_s), int(end_s)
21
+
22
+
23
+ def _peer_from_log(log_path: str, port: int) -> Optional[str]:
24
+ try:
25
+ text = Path(log_path).read_text(encoding="utf-8", errors="ignore")
26
+ except OSError:
27
+ return None
28
+ matches = re.findall(
29
+ r"Running a server on \['[^']+/p2p/([A-Za-z0-9]+)'\]",
30
+ text,
31
+ )
32
+ if not matches:
33
+ return None
34
+ return f"/ip4/127.0.0.1/tcp/{port}/p2p/{matches[-1]}"
35
+
36
+
37
+ class LocalShardRunner:
38
+ """Forward pass through this node's Petals server blocks only."""
39
+
40
+ def __init__(
41
+ self,
42
+ model_name: str,
43
+ block_indices: str,
44
+ *,
45
+ local_port: int,
46
+ log_path: Optional[str] = None,
47
+ ) -> None:
48
+ self.model_name = model_name
49
+ self.block_start, self.block_end = parse_range(block_indices)
50
+ self.local_port = local_port
51
+ if log_path:
52
+ self.log_path = log_path
53
+ else:
54
+ explicit = os.getenv("PETALS_SERVER_LOG") or os.getenv("CONTRIB_SHARD_LOG")
55
+ if explicit:
56
+ self.log_path = explicit
57
+ else:
58
+ self.log_path = str(Path.home() / ".blitzwing" / "petals.log")
59
+ self._model = None
60
+ self._tokenizer = None
61
+ self._lock = threading.Lock()
62
+
63
+ def _resolve_local_peer(self, *, timeout_seconds: float = 90.0) -> str:
64
+ deadline = time.time() + timeout_seconds
65
+ while time.time() < deadline:
66
+ peer = _peer_from_log(self.log_path, self.local_port)
67
+ if peer:
68
+ return peer
69
+ time.sleep(1.0)
70
+ raise RuntimeError(f"Local Petals peer not found in {self.log_path}")
71
+
72
+ def _ensure_loaded(self) -> None:
73
+ with self._lock:
74
+ if self._model is not None:
75
+ return
76
+ local_peer = self._resolve_local_peer()
77
+ from transformers import AutoTokenizer
78
+
79
+ from petals import AutoDistributedModelForCausalLM
80
+
81
+ logger.info(
82
+ "Loading local-only Petals client blocks=%s:%s peer=%s",
83
+ self.block_start,
84
+ self.block_end,
85
+ local_peer,
86
+ )
87
+ self._tokenizer = AutoTokenizer.from_pretrained(self.model_name, use_fast=True)
88
+ if self._tokenizer.pad_token is None:
89
+ self._tokenizer.pad_token = self._tokenizer.eos_token
90
+ self._model = AutoDistributedModelForCausalLM.from_pretrained(
91
+ self.model_name,
92
+ initial_peers=[local_peer],
93
+ torch_dtype=torch.float32,
94
+ update_period=30,
95
+ )
96
+ logger.info("Local-only Petals client ready")
97
+
98
+ @property
99
+ def model(self):
100
+ self._ensure_loaded()
101
+ assert self._model is not None
102
+ return self._model
103
+
104
+ @property
105
+ def tokenizer(self):
106
+ self._ensure_loaded()
107
+ assert self._tokenizer is not None
108
+ return self._tokenizer
109
+
110
+ def _layers(self):
111
+ m = self.model
112
+ if hasattr(m, "model") and hasattr(m.model, "layers"):
113
+ return m.model.layers
114
+ if hasattr(m, "transformer") and hasattr(m.transformer, "h"):
115
+ return m.transformer.h
116
+ raise RuntimeError("Could not locate transformer layers")
117
+
118
+ def _embed(self, input_ids: torch.Tensor) -> torch.Tensor:
119
+ m = self.model
120
+ if hasattr(m, "model") and hasattr(m.model, "embed_tokens"):
121
+ return m.model.embed_tokens(input_ids)
122
+ if hasattr(m, "transformer") and hasattr(m.transformer, "word_embeddings"):
123
+ return m.transformer.word_embeddings(input_ids)
124
+ raise RuntimeError("Could not locate embedding layer")
125
+
126
+ def _lm_head(self, hidden: torch.Tensor) -> torch.Tensor:
127
+ m = self.model
128
+ if hasattr(m, "lm_head"):
129
+ return m.lm_head(hidden)
130
+ if hasattr(m, "model") and hasattr(m.model, "lm_head"):
131
+ return m.model.lm_head(hidden)
132
+ raise RuntimeError("Could not locate lm_head")
133
+
134
+ def forward_prefix(self, input_ids: torch.Tensor) -> torch.Tensor:
135
+ hidden = self._embed(input_ids)
136
+ layers = self._layers()
137
+ for idx in range(self.block_start, self.block_end):
138
+ hidden = layers[idx](hidden)
139
+ return hidden
140
+
141
+ def forward_tail(self, hidden: torch.Tensor) -> torch.Tensor:
142
+ layers = self._layers()
143
+ for idx in range(self.block_start, self.block_end):
144
+ hidden = layers[idx](hidden)
145
+ return hidden
146
+
147
+ def logits_from_hidden(self, hidden: torch.Tensor) -> torch.Tensor:
148
+ return self._lm_head(hidden)
@@ -0,0 +1,26 @@
1
+ """Serialize torch tensors for HTTP transfer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from typing import Any, Dict
7
+
8
+ import numpy as np
9
+ import torch
10
+
11
+
12
+ def tensor_to_payload(tensor: torch.Tensor) -> Dict[str, Any]:
13
+ arr = tensor.detach().cpu().contiguous().numpy()
14
+ return {
15
+ "b64": base64.b64encode(arr.tobytes()).decode("ascii"),
16
+ "shape": list(arr.shape),
17
+ "dtype": str(arr.dtype),
18
+ }
19
+
20
+
21
+ def tensor_from_payload(payload: Dict[str, Any]) -> torch.Tensor:
22
+ arr = np.frombuffer(
23
+ base64.b64decode(payload["b64"]),
24
+ dtype=np.dtype(payload["dtype"]),
25
+ ).reshape(payload["shape"])
26
+ return torch.from_numpy(arr.copy())
package/src/heartbeat.js CHANGED
@@ -31,7 +31,10 @@ async function beat() {
31
31
  "Content-Type": "application/json",
32
32
  "ngrok-skip-browser-warning": "true",
33
33
  },
34
- body: JSON.stringify({ host_id: hostId }),
34
+ body: JSON.stringify({
35
+ host_id: hostId,
36
+ block_indices: process.env.BLOCK_INDICES || undefined,
37
+ }),
35
38
  });
36
39
  } catch {}
37
40
  }
package/src/install.js CHANGED
@@ -1,9 +1,15 @@
1
1
  import { spawn, execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { HOME_DIR, VENV_PATH, SHARD_PORT, PETALS_PORT } from "./config.js";
5
6
  import { which } from "./net.js";
6
7
 
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const PACKAGE_ROOT = path.join(__dirname, "..");
10
+ const RUNTIME_SRC = path.join(PACKAGE_ROOT, "runtime");
11
+ const RUNTIME_DEST = path.join(HOME_DIR, "runtime");
12
+
7
13
  function venvPython() {
8
14
  if (process.platform === "win32") {
9
15
  return path.join(VENV_PATH, "Scripts", "python.exe");
@@ -45,7 +51,6 @@ function pinnedPythonPath() {
45
51
  }
46
52
 
47
53
  export function ensurePython() {
48
- // Petals/hivemind break on 3.12+. Never fall back to a too-new interpreter.
49
54
  const home = process.env.HOME || process.env.USERPROFILE || "";
50
55
  const candidates = [];
51
56
  const envBin = process.env.BLITZWING_PYTHON || process.env.PETALS_PYTHON;
@@ -101,11 +106,7 @@ export function ensurePython() {
101
106
 
102
107
  function petalsImportable(python) {
103
108
  try {
104
- execFileSync(
105
- python,
106
- ["-c", "import petals.cli.run_server"],
107
- { stdio: "ignore" }
108
- );
109
+ execFileSync(python, ["-c", "import petals.cli.run_server"], { stdio: "ignore" });
109
110
  return true;
110
111
  } catch {
111
112
  return false;
@@ -114,20 +115,21 @@ function petalsImportable(python) {
114
115
 
115
116
  function uvicornImportable(python) {
116
117
  try {
117
- execFileSync(python, ["-c", "import uvicorn, fastapi"], { stdio: "ignore" });
118
+ execFileSync(python, ["-c", "import uvicorn, fastapi, httpx, numpy"], { stdio: "ignore" });
118
119
  return true;
119
120
  } catch {
120
121
  return false;
121
122
  }
122
123
  }
123
124
 
124
- function ensureShardManagerDeps(python, pip, env, onLog) {
125
+ function ensureShardManagerDeps(python, _pip, env, onLog) {
125
126
  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
- });
127
+ onLog?.("Installing shard manager deps (uvicorn, fastapi, httpx, numpy)...");
128
+ execFileSync(
129
+ python,
130
+ ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx", "numpy"],
131
+ { stdio: "inherit", env }
132
+ );
131
133
  }
132
134
 
133
135
  export function ensureVenvAndPetals({ onLog }) {
@@ -155,11 +157,10 @@ export function ensureVenvAndPetals({ onLog }) {
155
157
 
156
158
  if (process.platform === "win32") {
157
159
  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"
160
+ "Petals cannot run on native Windows (needs Linux).\n" +
161
+ "Install Node in WSL, then:\n" +
162
+ " npm i -g blitzwing\n" +
163
+ " blitzwing"
163
164
  );
164
165
  }
165
166
 
@@ -173,12 +174,10 @@ export function ensureVenvAndPetals({ onLog }) {
173
174
  ["-m", "pip", "install", "torch", "--index-url", "https://download.pytorch.org/whl/cpu"],
174
175
  { stdio: "inherit", env }
175
176
  );
176
- // Needed to compile hivemind protobufs during --no-build-isolation install
177
177
  execFileSync(python, ["-m", "pip", "install", "grpcio", "grpcio-tools", "protobuf"], {
178
178
  stdio: "inherit",
179
179
  env,
180
180
  });
181
- // Hivemind first with no build isolation (needs pkg_resources from setuptools<81)
182
181
  execFileSync(
183
182
  python,
184
183
  [
@@ -195,180 +194,53 @@ export function ensureVenvAndPetals({ onLog }) {
195
194
  ["-m", "pip", "install", "--no-build-isolation", "git+https://github.com/bigscience-workshop/petals.git"],
196
195
  { stdio: "inherit", env }
197
196
  );
198
- execFileSync(python, ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx"], {
199
- stdio: "inherit",
200
- env,
201
- });
197
+ execFileSync(
198
+ python,
199
+ ["-m", "pip", "install", "fastapi", "uvicorn[standard]", "pydantic", "httpx", "numpy"],
200
+ { stdio: "inherit", env }
201
+ );
202
202
 
203
203
  fs.writeFileSync(path.join(HOME_DIR, "python"), python + "\n");
204
204
  return { python, pip };
205
205
  }
206
206
 
207
207
  /**
208
- * Write a tiny local shard-manager runner script so contributors don't need the monorepo.
209
- * Embeds a minimal supervisor compatible with mother /reload API.
208
+ * Copy packaged runtime Python modules into ~/.blitzwing/runtime.
209
+ * Contributors never need the monorepo everything ships in the npm package.
210
210
  */
211
- export function writeLocalShardManager() {
212
- const dir = path.join(HOME_DIR, "runtime");
213
- fs.mkdirSync(dir, { recursive: true });
214
- const appPath = path.join(dir, "shard_manager_app.py");
215
- const code = `
216
- import os, signal, subprocess, threading, time
217
- from typing import Optional, List
218
- from fastapi import FastAPI, HTTPException
219
- from pydantic import BaseModel, Field
220
-
221
- class ReloadRequest(BaseModel):
222
- block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
223
- initial_peers: Optional[List[str]] = None
224
-
225
- class StatusResponse(BaseModel):
226
- running: bool
227
- pid: Optional[int] = None
228
- block_indices: Optional[str] = None
229
- model: str
230
- public_ip: Optional[str] = None
231
- port: int
232
- last_exit_code: Optional[int] = None
233
-
234
- class Mgr:
235
- def __init__(self):
236
- self.model = os.environ["MODEL_NAME"]
237
- self.public_ip = os.environ.get("PUBLIC_IP")
238
- self.port = int(os.environ.get("PETALS_PORT", "31337"))
239
- self.identity_path = os.environ.get("IDENTITY_PATH", os.path.expanduser("~/.blitzwing/petals-identity"))
240
- self.block_indices = os.environ["BLOCK_INDICES"]
241
- peers = [p.strip() for p in os.environ.get("INITIAL_PEERS", "").split(",") if p.strip()]
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()]
245
- self.python = os.environ.get("PETALS_PYTHON", "python")
246
- self.petals_log = os.environ.get("PETALS_LOG", os.path.expanduser("~/.blitzwing/petals.log"))
247
- self._proc = None
248
- self._log_fp = None
249
- self._lock = threading.Lock()
250
- self.last_exit_code = None
251
-
252
- def cmd(self, bi):
253
- c = [self.python, "-m", "petals.cli.run_server", self.model,
254
- "--device", "cpu", "--quant_type", "none",
255
- "--block_indices", bi, "--port", str(self.port),
256
- "--identity_path", self.identity_path, "--num_handlers", "1"]
257
- if self.announce_maddrs:
258
- c += ["--announce_maddrs", *self.announce_maddrs]
259
- elif self.public_ip:
260
- c += ["--public_ip", self.public_ip]
261
- if self.initial_peers:
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"]
270
- return c
271
-
272
- def start(self, bi=None):
273
- with self._lock:
274
- if bi: self.block_indices = bi
275
- if self._proc and self._proc.poll() is None:
276
- return
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)
286
- self.last_exit_code = None
287
-
288
- def stop(self):
289
- with self._lock:
290
- if not self._proc or self._proc.poll() is not None:
291
- self._proc = None
292
- return
293
- p = self._proc
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)
301
- try: p.wait(timeout=30)
302
- except Exception:
303
- p.kill(); p.wait(timeout=10)
304
- self.last_exit_code = p.returncode
305
- self._proc = None
306
-
307
- def reload(self, bi, initial_peers=None):
308
- if initial_peers is not None:
309
- self.initial_peers = initial_peers
310
- self.stop(); time.sleep(1); self.start(bi)
311
-
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
316
- running = self._proc is not None and self._proc.poll() is None
317
- return StatusResponse(
318
- running=running,
319
- pid=(self._proc.pid if running else None),
320
- block_indices=self.block_indices,
321
- model=self.model,
322
- public_ip=self.public_ip,
323
- port=self.port,
324
- last_exit_code=self.last_exit_code,
325
- )
326
-
327
- mgr = Mgr()
328
- app = FastAPI()
329
-
330
- @app.on_event("startup")
331
- def _up():
332
- mgr.start()
333
-
334
- @app.on_event("shutdown")
335
- def _down():
336
- mgr.stop()
337
-
338
- @app.get("/status")
339
- def status():
340
- return mgr.status()
341
-
342
- @app.post("/reload")
343
- def reload(body: ReloadRequest):
344
- a,b = map(int, body.block_indices.split(":"))
345
- if b <= a: raise HTTPException(400, "bad range")
346
- mgr.reload(body.block_indices, initial_peers=body.initial_peers)
347
- time.sleep(0.5)
348
- return mgr.status()
211
+ export function syncRuntimeFiles() {
212
+ if (!fs.existsSync(RUNTIME_SRC)) {
213
+ throw new Error(`CLI runtime missing at ${RUNTIME_SRC} reinstall the blitzwing package`);
214
+ }
215
+ fs.mkdirSync(RUNTIME_DEST, { recursive: true });
216
+ for (const name of fs.readdirSync(RUNTIME_SRC)) {
217
+ if (!name.endsWith(".py")) continue;
218
+ fs.copyFileSync(path.join(RUNTIME_SRC, name), path.join(RUNTIME_DEST, name));
219
+ }
220
+ return path.join(RUNTIME_DEST, "app.py");
221
+ }
349
222
 
350
- @app.post("/stop")
351
- def stop():
352
- mgr.stop(); return mgr.status()
353
- `;
354
- fs.writeFileSync(appPath, code);
355
- return appPath;
223
+ /** @deprecated use syncRuntimeFiles */
224
+ export function writeLocalShardManager() {
225
+ return syncRuntimeFiles();
356
226
  }
357
227
 
358
- export function startShardManagerProcess({
359
- python,
360
- env,
361
- logPath,
362
- }) {
363
- const appPath = writeLocalShardManager();
228
+ export function startShardManagerProcess({ python, env, logPath }) {
229
+ const appPath = syncRuntimeFiles();
364
230
  const out = fs.openSync(logPath, "a");
365
231
  const petalsPy = petalsImportable(venvPython()) ? venvPython() : python;
366
232
  const child = spawn(
367
233
  petalsPy,
368
- ["-m", "uvicorn", `shard_manager_app:app`, "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
234
+ ["-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", String(SHARD_PORT)],
369
235
  {
370
236
  cwd: path.dirname(appPath),
371
- env: { ...process.env, ...env, PETALS_PYTHON: petalsPy, PETALS_LOG: path.join(HOME_DIR, "petals.log") },
237
+ env: {
238
+ ...process.env,
239
+ ...env,
240
+ PETALS_PYTHON: petalsPy,
241
+ PETALS_LOG: path.join(HOME_DIR, "petals.log"),
242
+ PYTHONPATH: path.dirname(appPath),
243
+ },
372
244
  detached: true,
373
245
  stdio: ["ignore", out, out],
374
246
  }
@@ -398,7 +270,6 @@ export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {
398
270
  }
399
271
  } catch (err) {
400
272
  if (err instanceof Error && err.message.startsWith("Petals exited")) throw err;
401
- /* retry */
402
273
  }
403
274
  await new Promise((r) => setTimeout(r, 2000));
404
275
  }
@@ -410,4 +281,4 @@ export async function waitForShardRunning({ timeoutMs = 300000, statusHost } = {
410
281
  );
411
282
  }
412
283
 
413
- export { SHARD_PORT, PETALS_PORT, venvPython };
284
+ export { SHARD_PORT, PETALS_PORT, venvPython, RUNTIME_DEST };