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.
- package/README.md +14 -5
- package/package.json +2 -1
- package/runtime/app.py +398 -0
- package/runtime/http_chain.py +189 -0
- package/runtime/inference.py +13 -0
- package/runtime/local_runner.py +148 -0
- package/runtime/tensor_codec.py +26 -0
- package/src/heartbeat.js +4 -1
- package/src/install.js +51 -180
- package/src/tunnel.js +231 -0
- package/src/wizard.js +68 -105
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# blitzwing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Join a Blitzwing mother swarm as a compute contributor. One command — no repo clone, no ngrok, no port forwarding.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,22 +8,31 @@ Interactive setup wizard to contribute layers to a Blitzwing Petals mother swarm
|
|
|
8
8
|
npm i -g blitzwing
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
**Windows:** run inside **WSL** (Petals needs Linux). Install Node in WSL, then `npm i -g blitzwing`.
|
|
12
|
+
|
|
11
13
|
## Use
|
|
12
14
|
|
|
13
15
|
```bash
|
|
14
16
|
blitzwing
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
The wizard
|
|
19
|
+
The wizard:
|
|
20
|
+
|
|
21
|
+
1. Discovers mothers (no mother URL to type)
|
|
22
|
+
2. Asks which model and how many layers
|
|
23
|
+
3. Asks for your **Hedera account ID** (`0.0.N`) for payouts
|
|
24
|
+
4. Installs Petals if needed
|
|
25
|
+
5. Opens a free **Cloudflare Quick Tunnel** (no account / token)
|
|
26
|
+
6. Joins the swarm and starts heartbeats
|
|
18
27
|
|
|
19
28
|
```bash
|
|
20
29
|
blitzwing status
|
|
21
30
|
blitzwing leave
|
|
22
31
|
```
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
## Env (optional)
|
|
25
34
|
|
|
26
35
|
```bash
|
|
27
|
-
export BLITZWING_DISCOVERY_URL=http://127.0.0.1:9000
|
|
28
|
-
|
|
36
|
+
export BLITZWING_DISCOVERY_URL=http://127.0.0.1:9000 # override discovery
|
|
37
|
+
export BLITZWING_HEDERA_ACCOUNT_ID=0.0.123456 # prefill payout account
|
|
29
38
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blitzwing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"bin",
|
|
14
14
|
"src",
|
|
15
|
+
"runtime",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"keywords": [
|
package/runtime/app.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Contributor shard manager — Petals supervisor + HTTP-chain inference endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import signal
|
|
9
|
+
import subprocess
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from fastapi import FastAPI, HTTPException
|
|
17
|
+
from pydantic import BaseModel, Field
|
|
18
|
+
|
|
19
|
+
from http_chain import HttpChainInference
|
|
20
|
+
from local_runner import LocalShardRunner
|
|
21
|
+
from tensor_codec import tensor_to_payload
|
|
22
|
+
|
|
23
|
+
logging.basicConfig(level=logging.INFO)
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ReloadRequest(BaseModel):
|
|
28
|
+
block_indices: str = Field(..., pattern=r"^\d+:\d+$")
|
|
29
|
+
initial_peers: Optional[List[str]] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StatusResponse(BaseModel):
|
|
33
|
+
running: bool
|
|
34
|
+
pid: Optional[int] = None
|
|
35
|
+
block_indices: Optional[str] = None
|
|
36
|
+
model: str
|
|
37
|
+
public_ip: Optional[str] = None
|
|
38
|
+
port: int
|
|
39
|
+
last_exit_code: Optional[int] = None
|
|
40
|
+
identity_path: str
|
|
41
|
+
initial_peers: List[str] = []
|
|
42
|
+
new_swarm: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ChatInferenceRequest(BaseModel):
|
|
46
|
+
messages: List[dict]
|
|
47
|
+
max_tokens: int = 64
|
|
48
|
+
temperature: float = 0.7
|
|
49
|
+
top_p: float = 0.9
|
|
50
|
+
swarm_manifest: Optional[dict] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ChatInferenceResponse(BaseModel):
|
|
54
|
+
text: str
|
|
55
|
+
prompt_tokens: int
|
|
56
|
+
completion_tokens: int
|
|
57
|
+
finish_reason: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PrefixRequest(BaseModel):
|
|
61
|
+
input_ids: List[int]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PrefixResponse(BaseModel):
|
|
65
|
+
hidden: dict
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ShardManager:
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
self.model = os.getenv("MODEL_NAME", "bigscience/bloom-560m")
|
|
71
|
+
self.public_ip = os.getenv("PUBLIC_IP")
|
|
72
|
+
self.port = int(os.getenv("PETALS_PORT", "31337"))
|
|
73
|
+
self.identity_path = os.getenv(
|
|
74
|
+
"IDENTITY_PATH", str(Path.home() / ".blitzwing" / "petals-identity")
|
|
75
|
+
)
|
|
76
|
+
self.device = os.getenv("PETALS_DEVICE", "cpu")
|
|
77
|
+
self.quant_type = os.getenv("PETALS_QUANT_TYPE", "none")
|
|
78
|
+
self.num_handlers = int(os.getenv("PETALS_NUM_HANDLERS", "1"))
|
|
79
|
+
self.python = os.getenv("PETALS_PYTHON", "python")
|
|
80
|
+
peers_raw = os.getenv("INITIAL_PEERS", "")
|
|
81
|
+
self.initial_peers = [p.strip() for p in peers_raw.split(",") if p.strip()]
|
|
82
|
+
announce_raw = os.getenv("ANNOUNCE_MADDRS", "")
|
|
83
|
+
self.announce_maddrs = [a.strip() for a in announce_raw.split(",") if a.strip()]
|
|
84
|
+
self.new_swarm = os.getenv("NEW_SWARM", "0") in ("1", "true", "True")
|
|
85
|
+
self.use_auto_relay = self._resolve_use_auto_relay()
|
|
86
|
+
self.skip_reachability_check = os.getenv(
|
|
87
|
+
"PETALS_SKIP_REACHABILITY_CHECK", "1"
|
|
88
|
+
) in ("1", "true", "True")
|
|
89
|
+
self.block_indices = os.getenv("BLOCK_INDICES", "0:22")
|
|
90
|
+
self._proc: Optional[subprocess.Popen] = None
|
|
91
|
+
self._lock = threading.Lock()
|
|
92
|
+
self.last_exit_code: Optional[int] = None
|
|
93
|
+
self._auto_start = os.getenv("SHARD_AUTO_START", "1") not in ("0", "false", "False")
|
|
94
|
+
self._bootstrapped = False
|
|
95
|
+
self._log_fp = None
|
|
96
|
+
self.petals_log = os.getenv(
|
|
97
|
+
"PETALS_LOG", str(Path.home() / ".blitzwing" / "petals.log")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _resolve_use_auto_relay(self) -> bool:
|
|
101
|
+
raw = os.getenv("PETALS_USE_AUTO_RELAY")
|
|
102
|
+
if raw is not None:
|
|
103
|
+
return raw not in ("0", "false", "False", "no")
|
|
104
|
+
if self.announce_maddrs:
|
|
105
|
+
return False
|
|
106
|
+
return not self.new_swarm
|
|
107
|
+
|
|
108
|
+
def build_cmd(self, block_indices: str, *, bootstrap: bool = False) -> List[str]:
|
|
109
|
+
cmd = [
|
|
110
|
+
self.python,
|
|
111
|
+
"-m",
|
|
112
|
+
"petals.cli.run_server",
|
|
113
|
+
self.model,
|
|
114
|
+
"--device",
|
|
115
|
+
self.device,
|
|
116
|
+
"--quant_type",
|
|
117
|
+
self.quant_type,
|
|
118
|
+
"--block_indices",
|
|
119
|
+
block_indices,
|
|
120
|
+
"--port",
|
|
121
|
+
str(self.port),
|
|
122
|
+
"--identity_path",
|
|
123
|
+
self.identity_path,
|
|
124
|
+
"--num_handlers",
|
|
125
|
+
str(self.num_handlers),
|
|
126
|
+
]
|
|
127
|
+
if self.announce_maddrs:
|
|
128
|
+
cmd.append("--announce_maddrs")
|
|
129
|
+
cmd.extend(self.announce_maddrs)
|
|
130
|
+
elif self.public_ip:
|
|
131
|
+
cmd.extend(["--public_ip", self.public_ip])
|
|
132
|
+
if bootstrap and self.new_swarm:
|
|
133
|
+
cmd.append("--new_swarm")
|
|
134
|
+
elif self.initial_peers and not os.getenv("BLITZWING_HTTP_ONLY"):
|
|
135
|
+
cmd.append("--initial_peers")
|
|
136
|
+
cmd.extend(self.initial_peers)
|
|
137
|
+
if not self.use_auto_relay:
|
|
138
|
+
cmd.append("--no_auto_relay")
|
|
139
|
+
if self.skip_reachability_check:
|
|
140
|
+
cmd.append("--skip_reachability_check")
|
|
141
|
+
return cmd
|
|
142
|
+
|
|
143
|
+
def start(self, block_indices: Optional[str] = None, *, bootstrap: bool = False) -> None:
|
|
144
|
+
with self._lock:
|
|
145
|
+
if block_indices:
|
|
146
|
+
self.block_indices = block_indices
|
|
147
|
+
if self._proc and self._proc.poll() is None:
|
|
148
|
+
raise RuntimeError("Petals server already running; call /reload instead")
|
|
149
|
+
use_bootstrap = bootstrap or (self.new_swarm and not self._bootstrapped)
|
|
150
|
+
cmd = self.build_cmd(self.block_indices, bootstrap=use_bootstrap)
|
|
151
|
+
logger.info("Starting Petals: %s", " ".join(cmd))
|
|
152
|
+
if self._log_fp is None:
|
|
153
|
+
Path(self.petals_log).parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
self._log_fp = open(self.petals_log, "a", encoding="utf-8")
|
|
155
|
+
popen_kwargs: dict = {
|
|
156
|
+
"stdout": self._log_fp,
|
|
157
|
+
"stderr": subprocess.STDOUT,
|
|
158
|
+
}
|
|
159
|
+
if os.name != "nt":
|
|
160
|
+
popen_kwargs["preexec_fn"] = os.setsid
|
|
161
|
+
self._proc = subprocess.Popen(cmd, **popen_kwargs)
|
|
162
|
+
self.last_exit_code = None
|
|
163
|
+
self._bootstrapped = True
|
|
164
|
+
|
|
165
|
+
def stop(self, timeout: float = 30.0) -> None:
|
|
166
|
+
with self._lock:
|
|
167
|
+
if not self._proc or self._proc.poll() is not None:
|
|
168
|
+
self._proc = None
|
|
169
|
+
return
|
|
170
|
+
proc = self._proc
|
|
171
|
+
logger.info("Stopping Petals pid=%s", proc.pid)
|
|
172
|
+
try:
|
|
173
|
+
if os.name == "nt":
|
|
174
|
+
proc.terminate()
|
|
175
|
+
else:
|
|
176
|
+
try:
|
|
177
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
|
178
|
+
except ProcessLookupError:
|
|
179
|
+
proc.send_signal(signal.SIGTERM)
|
|
180
|
+
try:
|
|
181
|
+
proc.wait(timeout=timeout)
|
|
182
|
+
except subprocess.TimeoutExpired:
|
|
183
|
+
if os.name != "nt":
|
|
184
|
+
try:
|
|
185
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
186
|
+
except ProcessLookupError:
|
|
187
|
+
proc.kill()
|
|
188
|
+
else:
|
|
189
|
+
proc.kill()
|
|
190
|
+
proc.wait(timeout=10)
|
|
191
|
+
finally:
|
|
192
|
+
self.last_exit_code = proc.returncode
|
|
193
|
+
self._proc = None
|
|
194
|
+
|
|
195
|
+
def reload(self, block_indices: str, initial_peers: Optional[List[str]] = None) -> None:
|
|
196
|
+
if initial_peers is not None:
|
|
197
|
+
self.initial_peers = initial_peers
|
|
198
|
+
rebootstrap = self.new_swarm
|
|
199
|
+
logger.info(
|
|
200
|
+
"Reloading Petals with block_indices=%s initial_peers=%s rebootstrap=%s",
|
|
201
|
+
block_indices,
|
|
202
|
+
self.initial_peers,
|
|
203
|
+
rebootstrap,
|
|
204
|
+
)
|
|
205
|
+
self.stop()
|
|
206
|
+
time.sleep(1.0)
|
|
207
|
+
if rebootstrap:
|
|
208
|
+
self._bootstrapped = False
|
|
209
|
+
self.start(block_indices, bootstrap=rebootstrap)
|
|
210
|
+
|
|
211
|
+
def status(self) -> StatusResponse:
|
|
212
|
+
running = self._proc is not None and self._proc.poll() is None
|
|
213
|
+
pid = self._proc.pid if running and self._proc else None
|
|
214
|
+
if self._proc and not running:
|
|
215
|
+
self.last_exit_code = self._proc.returncode
|
|
216
|
+
return StatusResponse(
|
|
217
|
+
running=running,
|
|
218
|
+
pid=pid,
|
|
219
|
+
block_indices=self.block_indices,
|
|
220
|
+
model=self.model,
|
|
221
|
+
public_ip=self.public_ip,
|
|
222
|
+
port=self.port,
|
|
223
|
+
last_exit_code=self.last_exit_code,
|
|
224
|
+
identity_path=self.identity_path,
|
|
225
|
+
initial_peers=list(self.initial_peers),
|
|
226
|
+
new_swarm=self.new_swarm,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
manager = ShardManager()
|
|
231
|
+
app = FastAPI(title="Blitzwing Contributor Shard Manager", version="0.2.0")
|
|
232
|
+
|
|
233
|
+
_local_runner: Optional[LocalShardRunner] = None
|
|
234
|
+
_http_chain: Optional[HttpChainInference] = None
|
|
235
|
+
_inference_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix="shard-infer")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _petals_log_path() -> str:
|
|
239
|
+
return manager.petals_log
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _reset_inference_caches() -> None:
|
|
243
|
+
global _local_runner, _http_chain
|
|
244
|
+
if _local_runner is not None or _http_chain is not None:
|
|
245
|
+
logger.info("Resetting inference caches (blocks=%s)", manager.block_indices)
|
|
246
|
+
_local_runner = None
|
|
247
|
+
_http_chain = None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_local_runner() -> LocalShardRunner:
|
|
251
|
+
global _local_runner
|
|
252
|
+
blocks = manager.block_indices
|
|
253
|
+
if _local_runner is not None:
|
|
254
|
+
current = f"{_local_runner.block_start}:{_local_runner.block_end}"
|
|
255
|
+
if current != blocks:
|
|
256
|
+
logger.warning(
|
|
257
|
+
"LocalShardRunner stale (%s != %s); recreating",
|
|
258
|
+
current,
|
|
259
|
+
blocks,
|
|
260
|
+
)
|
|
261
|
+
_reset_inference_caches()
|
|
262
|
+
if _local_runner is None:
|
|
263
|
+
_local_runner = LocalShardRunner(
|
|
264
|
+
manager.model,
|
|
265
|
+
blocks,
|
|
266
|
+
local_port=manager.port,
|
|
267
|
+
log_path=_petals_log_path(),
|
|
268
|
+
)
|
|
269
|
+
return _local_runner
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _get_http_chain() -> HttpChainInference:
|
|
273
|
+
global _http_chain
|
|
274
|
+
if _http_chain is None:
|
|
275
|
+
_http_chain = HttpChainInference(
|
|
276
|
+
runner=_get_local_runner(),
|
|
277
|
+
local_block_indices=manager.block_indices,
|
|
278
|
+
)
|
|
279
|
+
return _http_chain
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _prewarm_local_runner() -> None:
|
|
283
|
+
for _ in range(120):
|
|
284
|
+
if manager.status().running:
|
|
285
|
+
break
|
|
286
|
+
time.sleep(2)
|
|
287
|
+
try:
|
|
288
|
+
_get_local_runner()
|
|
289
|
+
logger.info("LocalShardRunner pre-warmed for HTTP chain")
|
|
290
|
+
except Exception: # noqa: BLE001
|
|
291
|
+
logger.exception("LocalShardRunner pre-warm failed")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@app.on_event("startup")
|
|
295
|
+
def on_startup() -> None:
|
|
296
|
+
if manager._auto_start:
|
|
297
|
+
try:
|
|
298
|
+
manager.start(bootstrap=manager.new_swarm)
|
|
299
|
+
except Exception: # noqa: BLE001
|
|
300
|
+
logger.exception("Failed to auto-start Petals server")
|
|
301
|
+
threading.Thread(
|
|
302
|
+
target=_prewarm_local_runner, name="prefix-prewarm", daemon=True
|
|
303
|
+
).start()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@app.on_event("shutdown")
|
|
307
|
+
def on_shutdown() -> None:
|
|
308
|
+
manager.stop()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@app.get("/health")
|
|
312
|
+
def health() -> dict:
|
|
313
|
+
st = manager.status()
|
|
314
|
+
return {"status": "ok" if st.running else "degraded", "running": st.running}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@app.get("/status", response_model=StatusResponse)
|
|
318
|
+
def status() -> StatusResponse:
|
|
319
|
+
return manager.status()
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.post("/reload", response_model=StatusResponse)
|
|
323
|
+
def reload(body: ReloadRequest) -> StatusResponse:
|
|
324
|
+
start, end = map(int, body.block_indices.split(":"))
|
|
325
|
+
if end <= start:
|
|
326
|
+
raise HTTPException(status_code=400, detail="block_indices end must be > start")
|
|
327
|
+
try:
|
|
328
|
+
manager.reload(body.block_indices, initial_peers=body.initial_peers)
|
|
329
|
+
except Exception as exc: # noqa: BLE001
|
|
330
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
331
|
+
_reset_inference_caches()
|
|
332
|
+
threading.Thread(
|
|
333
|
+
target=_prewarm_local_runner, name="prefix-prewarm", daemon=True
|
|
334
|
+
).start()
|
|
335
|
+
time.sleep(0.5)
|
|
336
|
+
return manager.status()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@app.post("/stop", response_model=StatusResponse)
|
|
340
|
+
def stop() -> StatusResponse:
|
|
341
|
+
manager.stop()
|
|
342
|
+
return manager.status()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@app.post("/v1/chain/prefix", response_model=PrefixResponse)
|
|
346
|
+
async def chain_prefix(body: PrefixRequest) -> PrefixResponse:
|
|
347
|
+
st = manager.status()
|
|
348
|
+
if not st.running:
|
|
349
|
+
raise HTTPException(status_code=503, detail="Petals server not running")
|
|
350
|
+
|
|
351
|
+
def _run() -> PrefixResponse:
|
|
352
|
+
import torch
|
|
353
|
+
|
|
354
|
+
runner = _get_local_runner()
|
|
355
|
+
input_ids = torch.tensor([body.input_ids], dtype=torch.long)
|
|
356
|
+
hidden = runner.forward_prefix(input_ids)
|
|
357
|
+
return PrefixResponse(hidden=tensor_to_payload(hidden))
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
loop = asyncio.get_running_loop()
|
|
361
|
+
return await loop.run_in_executor(_inference_executor, _run)
|
|
362
|
+
except Exception as exc: # noqa: BLE001
|
|
363
|
+
logger.exception("HTTP prefix forward failed")
|
|
364
|
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@app.post("/v1/chat/completions", response_model=ChatInferenceResponse)
|
|
368
|
+
async def chat_completions(body: ChatInferenceRequest) -> ChatInferenceResponse:
|
|
369
|
+
st = manager.status()
|
|
370
|
+
if not st.running:
|
|
371
|
+
raise HTTPException(status_code=503, detail="Petals server not running")
|
|
372
|
+
if not body.swarm_manifest:
|
|
373
|
+
raise HTTPException(
|
|
374
|
+
status_code=400,
|
|
375
|
+
detail="swarm_manifest required for distributed HTTP inference",
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def _run() -> ChatInferenceResponse:
|
|
379
|
+
result = _get_http_chain().generate(
|
|
380
|
+
body.messages,
|
|
381
|
+
max_tokens=body.max_tokens,
|
|
382
|
+
temperature=body.temperature,
|
|
383
|
+
top_p=body.top_p,
|
|
384
|
+
swarm_manifest=body.swarm_manifest,
|
|
385
|
+
)
|
|
386
|
+
return ChatInferenceResponse(
|
|
387
|
+
text=result.text,
|
|
388
|
+
prompt_tokens=result.prompt_tokens,
|
|
389
|
+
completion_tokens=result.completion_tokens,
|
|
390
|
+
finish_reason=result.finish_reason,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
loop = asyncio.get_running_loop()
|
|
395
|
+
return await loop.run_in_executor(_inference_executor, _run)
|
|
396
|
+
except Exception as exc: # noqa: BLE001
|
|
397
|
+
logger.exception("Contributor HTTP-chained inference failed")
|
|
398
|
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""HTTP-chained distributed inference — no libp2p between nodes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
from inference import InferenceResult
|
|
14
|
+
from local_runner import LocalShardRunner, parse_range
|
|
15
|
+
from tensor_codec import tensor_from_payload, tensor_to_payload
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_public_http_url(url: str) -> bool:
|
|
21
|
+
if not url:
|
|
22
|
+
return False
|
|
23
|
+
lower = url.lower()
|
|
24
|
+
if lower.startswith("http://127.") or lower.startswith("http://localhost"):
|
|
25
|
+
return False
|
|
26
|
+
if url.startswith("http://172.") or url.startswith("http://192.168."):
|
|
27
|
+
return False
|
|
28
|
+
if url.startswith("http://10."):
|
|
29
|
+
return False
|
|
30
|
+
return url.startswith("http://") or url.startswith("https://")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_reachable_mother_url(url: str) -> bool:
|
|
34
|
+
if not _is_public_http_url(url):
|
|
35
|
+
return False
|
|
36
|
+
if url.rstrip("/").endswith(":8001"):
|
|
37
|
+
return False
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_mother_shard_url(manifest: dict) -> str:
|
|
42
|
+
for env_key in ("MOTHER_PUBLIC_SHARD_URL",):
|
|
43
|
+
override = (os.getenv(env_key) or "").strip().rstrip("/")
|
|
44
|
+
if override:
|
|
45
|
+
return override
|
|
46
|
+
|
|
47
|
+
gateway = (
|
|
48
|
+
os.getenv("MOTHER_URL")
|
|
49
|
+
or os.getenv("BLITZWING_MOTHER_URL")
|
|
50
|
+
or os.getenv("MOTHER_PUBLIC_GATEWAY_URL")
|
|
51
|
+
or ""
|
|
52
|
+
).strip().rstrip("/")
|
|
53
|
+
if gateway:
|
|
54
|
+
return gateway
|
|
55
|
+
|
|
56
|
+
for host in manifest.get("hosts", []):
|
|
57
|
+
if host.get("role") != "mother":
|
|
58
|
+
continue
|
|
59
|
+
url = (host.get("shard_manager_url") or "").rstrip("/")
|
|
60
|
+
if _is_reachable_mother_url(url):
|
|
61
|
+
return url
|
|
62
|
+
public_ip = host.get("public_ip")
|
|
63
|
+
if public_ip:
|
|
64
|
+
return f"http://{public_ip}:8000"
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Cannot resolve mother prefix URL from swarm manifest "
|
|
67
|
+
"(set BLITZWING_MOTHER_URL or MOTHER_PUBLIC_SHARD_URL)"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def mother_prefix_blocks(manifest: dict) -> str:
|
|
72
|
+
for host in manifest.get("hosts", []):
|
|
73
|
+
if host.get("role") == "mother" and host.get("block_indices"):
|
|
74
|
+
return str(host["block_indices"])
|
|
75
|
+
return "0:18"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class HttpChainInference:
|
|
80
|
+
runner: LocalShardRunner
|
|
81
|
+
local_block_indices: str
|
|
82
|
+
|
|
83
|
+
def generate(
|
|
84
|
+
self,
|
|
85
|
+
messages: Sequence[dict],
|
|
86
|
+
*,
|
|
87
|
+
max_tokens: int = 64,
|
|
88
|
+
temperature: float = 0.7,
|
|
89
|
+
top_p: float = 0.9,
|
|
90
|
+
swarm_manifest: Optional[dict] = None,
|
|
91
|
+
) -> InferenceResult:
|
|
92
|
+
if not swarm_manifest:
|
|
93
|
+
raise RuntimeError("swarm_manifest required for HTTP-chained inference")
|
|
94
|
+
|
|
95
|
+
mother_url = resolve_mother_shard_url(swarm_manifest)
|
|
96
|
+
mother_blocks = mother_prefix_blocks(swarm_manifest)
|
|
97
|
+
_, mother_end = parse_range(mother_blocks)
|
|
98
|
+
local_start, _local_end = parse_range(self.local_block_indices)
|
|
99
|
+
prefix_timeout = float(os.getenv("HTTP_CHAIN_PREFIX_TIMEOUT", "120"))
|
|
100
|
+
|
|
101
|
+
if local_start != mother_end:
|
|
102
|
+
logger.warning(
|
|
103
|
+
"Layer gap between mother end=%s and local start=%s",
|
|
104
|
+
mother_end,
|
|
105
|
+
local_start,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
tokenizer = self.runner.tokenizer
|
|
109
|
+
try:
|
|
110
|
+
prompt = tokenizer.apply_chat_template(
|
|
111
|
+
list(messages),
|
|
112
|
+
tokenize=False,
|
|
113
|
+
add_generation_prompt=True,
|
|
114
|
+
)
|
|
115
|
+
except Exception: # noqa: BLE001
|
|
116
|
+
parts: List[str] = []
|
|
117
|
+
for msg in messages:
|
|
118
|
+
role = msg.get("role", "user")
|
|
119
|
+
content = msg.get("content", "")
|
|
120
|
+
parts.append(f"<|{role}|>\n{content}</s>")
|
|
121
|
+
parts.append("<|assistant|>\n")
|
|
122
|
+
prompt = "".join(parts)
|
|
123
|
+
|
|
124
|
+
input_ids = tokenizer(prompt, return_tensors="pt")["input_ids"]
|
|
125
|
+
prompt_tokens = int(input_ids.shape[-1])
|
|
126
|
+
max_new_tokens = max(1, min(int(max_tokens), 512))
|
|
127
|
+
logger.info(
|
|
128
|
+
"HTTP chain: mother=%s blocks=%s local=%s max_tokens=%s",
|
|
129
|
+
mother_url,
|
|
130
|
+
mother_blocks,
|
|
131
|
+
self.local_block_indices,
|
|
132
|
+
max_new_tokens,
|
|
133
|
+
)
|
|
134
|
+
generated: List[int] = []
|
|
135
|
+
finish_reason = "stop"
|
|
136
|
+
|
|
137
|
+
with httpx.Client(timeout=prefix_timeout) as client:
|
|
138
|
+
for step in range(max_new_tokens):
|
|
139
|
+
full_ids = (
|
|
140
|
+
torch.cat(
|
|
141
|
+
[input_ids, torch.tensor([generated], dtype=input_ids.dtype)],
|
|
142
|
+
dim=-1,
|
|
143
|
+
)
|
|
144
|
+
if generated
|
|
145
|
+
else input_ids
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
prefix_resp = client.post(
|
|
149
|
+
f"{mother_url.rstrip('/')}/v1/chain/prefix",
|
|
150
|
+
json={"input_ids": full_ids[0].tolist()},
|
|
151
|
+
)
|
|
152
|
+
prefix_resp.raise_for_status()
|
|
153
|
+
hidden = tensor_from_payload(prefix_resp.json()["hidden"])
|
|
154
|
+
logger.debug("HTTP chain step %s: mother prefix ok", step)
|
|
155
|
+
|
|
156
|
+
hidden = self.runner.forward_tail(hidden)
|
|
157
|
+
logits = self.runner.logits_from_hidden(hidden)
|
|
158
|
+
next_logits = logits[0, -1, :]
|
|
159
|
+
|
|
160
|
+
if temperature <= 0:
|
|
161
|
+
next_id = int(torch.argmax(next_logits).item())
|
|
162
|
+
else:
|
|
163
|
+
probs = torch.softmax(next_logits / float(temperature), dim=-1)
|
|
164
|
+
if top_p < 1.0:
|
|
165
|
+
sorted_probs, sorted_idx = torch.sort(probs, descending=True)
|
|
166
|
+
cumulative = torch.cumsum(sorted_probs, dim=-1)
|
|
167
|
+
mask = cumulative > float(top_p)
|
|
168
|
+
mask[..., 1:] = mask[..., :-1].clone()
|
|
169
|
+
mask[..., 0] = False
|
|
170
|
+
sorted_probs[mask] = 0
|
|
171
|
+
sorted_probs = sorted_probs / sorted_probs.sum()
|
|
172
|
+
pick = torch.multinomial(sorted_probs, 1).item()
|
|
173
|
+
next_id = int(sorted_idx[pick].item())
|
|
174
|
+
else:
|
|
175
|
+
next_id = int(torch.multinomial(probs, 1).item())
|
|
176
|
+
|
|
177
|
+
generated.append(next_id)
|
|
178
|
+
if next_id == tokenizer.eos_token_id:
|
|
179
|
+
break
|
|
180
|
+
else:
|
|
181
|
+
finish_reason = "length"
|
|
182
|
+
|
|
183
|
+
text = tokenizer.decode(generated, skip_special_tokens=True)
|
|
184
|
+
return InferenceResult(
|
|
185
|
+
text=text,
|
|
186
|
+
prompt_tokens=prompt_tokens,
|
|
187
|
+
completion_tokens=len(generated),
|
|
188
|
+
finish_reason=finish_reason,
|
|
189
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Minimal inference result type for HTTP-chain contributors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class InferenceResult:
|
|
10
|
+
text: str
|
|
11
|
+
prompt_tokens: int
|
|
12
|
+
completion_tokens: int
|
|
13
|
+
finish_reason: str
|