blitzwing 0.2.0 → 0.2.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 +32 -32
- package/runtime/app.py +29 -1
- package/runtime/http_chain.py +95 -30
- package/src/config.js +1 -1
- package/src/wizard.js +62 -33
package/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "blitzwing",
|
|
3
|
-
"version": "0.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
|
-
"runtime",
|
|
16
|
-
"README.md"
|
|
17
|
-
],
|
|
18
|
-
"keywords": [
|
|
19
|
-
"blitzwing",
|
|
20
|
-
"petals",
|
|
21
|
-
"llm",
|
|
22
|
-
"distributed-inference"
|
|
23
|
-
],
|
|
24
|
-
"license": "MIT",
|
|
25
|
-
"config": {
|
|
26
|
-
"discoveryUrl": "http://35.
|
|
27
|
-
},
|
|
28
|
-
"dependencies": {
|
|
29
|
-
"@clack/prompts": "^0.9.1",
|
|
30
|
-
"picocolors": "^1.1.1"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "blitzwing",
|
|
3
|
+
"version": "0.2.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
|
+
"runtime",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"blitzwing",
|
|
20
|
+
"petals",
|
|
21
|
+
"llm",
|
|
22
|
+
"distributed-inference"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"config": {
|
|
26
|
+
"discoveryUrl": "http://35.226.124.189:9000"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@clack/prompts": "^0.9.1",
|
|
30
|
+
"picocolors": "^1.1.1"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/runtime/app.py
CHANGED
|
@@ -18,7 +18,7 @@ from pydantic import BaseModel, Field
|
|
|
18
18
|
|
|
19
19
|
from http_chain import HttpChainInference
|
|
20
20
|
from local_runner import LocalShardRunner
|
|
21
|
-
from tensor_codec import tensor_to_payload
|
|
21
|
+
from tensor_codec import tensor_from_payload, tensor_to_payload
|
|
22
22
|
|
|
23
23
|
logging.basicConfig(level=logging.INFO)
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
@@ -65,6 +65,14 @@ class PrefixResponse(BaseModel):
|
|
|
65
65
|
hidden: dict
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
class ContinueRequest(BaseModel):
|
|
69
|
+
hidden: dict
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ContinueResponse(BaseModel):
|
|
73
|
+
hidden: dict
|
|
74
|
+
|
|
75
|
+
|
|
68
76
|
class ShardManager:
|
|
69
77
|
def __init__(self) -> None:
|
|
70
78
|
self.model = os.getenv("MODEL_NAME", "bigscience/bloom-560m")
|
|
@@ -364,6 +372,26 @@ async def chain_prefix(body: PrefixRequest) -> PrefixResponse:
|
|
|
364
372
|
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
365
373
|
|
|
366
374
|
|
|
375
|
+
@app.post("/v1/chain/continue", response_model=ContinueResponse)
|
|
376
|
+
async def chain_continue(body: ContinueRequest) -> ContinueResponse:
|
|
377
|
+
st = manager.status()
|
|
378
|
+
if not st.running:
|
|
379
|
+
raise HTTPException(status_code=503, detail="Petals server not running")
|
|
380
|
+
|
|
381
|
+
def _run() -> ContinueResponse:
|
|
382
|
+
runner = _get_local_runner()
|
|
383
|
+
hidden = tensor_from_payload(body.hidden)
|
|
384
|
+
hidden = runner.forward_tail(hidden)
|
|
385
|
+
return ContinueResponse(hidden=tensor_to_payload(hidden))
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
loop = asyncio.get_running_loop()
|
|
389
|
+
return await loop.run_in_executor(_inference_executor, _run)
|
|
390
|
+
except Exception as exc: # noqa: BLE001
|
|
391
|
+
logger.exception("HTTP chain continue failed")
|
|
392
|
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
393
|
+
|
|
394
|
+
|
|
367
395
|
@app.post("/v1/chat/completions", response_model=ChatInferenceResponse)
|
|
368
396
|
async def chat_completions(body: ChatInferenceRequest) -> ChatInferenceResponse:
|
|
369
397
|
st = manager.status()
|
package/runtime/http_chain.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""HTTP-chained distributed inference —
|
|
1
|
+
"""HTTP-chained distributed inference — every online host runs its layer slice."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import List, Optional, Sequence
|
|
8
|
+
from typing import List, Optional, Sequence, Tuple
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
import torch
|
|
@@ -68,11 +68,39 @@ def resolve_mother_shard_url(manifest: dict) -> str:
|
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
def
|
|
72
|
-
for
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
def ordered_hosts(manifest: dict) -> List[dict]:
|
|
72
|
+
hosts = [h for h in manifest.get("hosts", []) if h.get("block_indices")]
|
|
73
|
+
hosts.sort(key=lambda h: parse_range(str(h["block_indices"]))[0])
|
|
74
|
+
return hosts
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def validate_chain_coverage(hosts: List[dict], total_layers: int) -> None:
|
|
78
|
+
if not hosts:
|
|
79
|
+
raise RuntimeError("swarm_manifest has no hosts")
|
|
80
|
+
expected = 0
|
|
81
|
+
for host in hosts:
|
|
82
|
+
start, end = parse_range(str(host["block_indices"]))
|
|
83
|
+
if start != expected:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"layer gap before {host.get('host_id')}: expected start={expected}, got {start}"
|
|
86
|
+
)
|
|
87
|
+
expected = end
|
|
88
|
+
if total_layers and expected != int(total_layers):
|
|
89
|
+
raise RuntimeError(
|
|
90
|
+
f"layer map incomplete: covered 0:{expected}, need 0:{total_layers}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def host_http_url(host: dict, manifest: dict) -> str:
|
|
95
|
+
if host.get("role") == "mother":
|
|
96
|
+
return resolve_mother_shard_url(manifest)
|
|
97
|
+
url = (host.get("shard_manager_url") or "").rstrip("/")
|
|
98
|
+
if not _is_public_http_url(url):
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
f"host {host.get('host_id')} has no public shard_manager_url "
|
|
101
|
+
f"(got {url or 'empty'}) — cannot include it in HTTP chain"
|
|
102
|
+
)
|
|
103
|
+
return url
|
|
76
104
|
|
|
77
105
|
|
|
78
106
|
@dataclass
|
|
@@ -80,6 +108,22 @@ class HttpChainInference:
|
|
|
80
108
|
runner: LocalShardRunner
|
|
81
109
|
local_block_indices: str
|
|
82
110
|
|
|
111
|
+
def _remote_prefix(self, client: httpx.Client, url: str, input_ids: List[int]) -> torch.Tensor:
|
|
112
|
+
resp = client.post(
|
|
113
|
+
f"{url.rstrip('/')}/v1/chain/prefix",
|
|
114
|
+
json={"input_ids": input_ids},
|
|
115
|
+
)
|
|
116
|
+
resp.raise_for_status()
|
|
117
|
+
return tensor_from_payload(resp.json()["hidden"])
|
|
118
|
+
|
|
119
|
+
def _remote_continue(self, client: httpx.Client, url: str, hidden: torch.Tensor) -> torch.Tensor:
|
|
120
|
+
resp = client.post(
|
|
121
|
+
f"{url.rstrip('/')}/v1/chain/continue",
|
|
122
|
+
json={"hidden": tensor_to_payload(hidden)},
|
|
123
|
+
)
|
|
124
|
+
resp.raise_for_status()
|
|
125
|
+
return tensor_from_payload(resp.json()["hidden"])
|
|
126
|
+
|
|
83
127
|
def generate(
|
|
84
128
|
self,
|
|
85
129
|
messages: Sequence[dict],
|
|
@@ -92,19 +136,22 @@ class HttpChainInference:
|
|
|
92
136
|
if not swarm_manifest:
|
|
93
137
|
raise RuntimeError("swarm_manifest required for HTTP-chained inference")
|
|
94
138
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
hosts = ordered_hosts(swarm_manifest)
|
|
140
|
+
total_layers = int(swarm_manifest.get("total_layers") or 0)
|
|
141
|
+
validate_chain_coverage(hosts, total_layers)
|
|
142
|
+
|
|
143
|
+
local_start, local_end = parse_range(self.local_block_indices)
|
|
144
|
+
tail = hosts[-1]
|
|
145
|
+
tail_start, tail_end = parse_range(str(tail["block_indices"]))
|
|
146
|
+
if (tail_start, tail_end) != (local_start, local_end):
|
|
147
|
+
raise RuntimeError(
|
|
148
|
+
f"local blocks {self.local_block_indices} are not the tail "
|
|
149
|
+
f"(tail is {tail.get('host_id')} {tail['block_indices']})"
|
|
106
150
|
)
|
|
107
151
|
|
|
152
|
+
upstream = hosts[:-1]
|
|
153
|
+
hop_timeout = float(os.getenv("HTTP_CHAIN_PREFIX_TIMEOUT", "120"))
|
|
154
|
+
|
|
108
155
|
tokenizer = self.runner.tokenizer
|
|
109
156
|
try:
|
|
110
157
|
prompt = tokenizer.apply_chat_template(
|
|
@@ -124,17 +171,19 @@ class HttpChainInference:
|
|
|
124
171
|
input_ids = tokenizer(prompt, return_tensors="pt")["input_ids"]
|
|
125
172
|
prompt_tokens = int(input_ids.shape[-1])
|
|
126
173
|
max_new_tokens = max(1, min(int(max_tokens), 512))
|
|
174
|
+
hop_desc = " -> ".join(
|
|
175
|
+
f"{h.get('host_id')}[{h.get('block_indices')}]" for h in hosts
|
|
176
|
+
)
|
|
127
177
|
logger.info(
|
|
128
|
-
"HTTP chain
|
|
129
|
-
|
|
130
|
-
mother_blocks,
|
|
178
|
+
"HTTP chain hops=%s local=%s max_tokens=%s",
|
|
179
|
+
hop_desc,
|
|
131
180
|
self.local_block_indices,
|
|
132
181
|
max_new_tokens,
|
|
133
182
|
)
|
|
134
183
|
generated: List[int] = []
|
|
135
184
|
finish_reason = "stop"
|
|
136
185
|
|
|
137
|
-
with httpx.Client(timeout=
|
|
186
|
+
with httpx.Client(timeout=hop_timeout) as client:
|
|
138
187
|
for step in range(max_new_tokens):
|
|
139
188
|
full_ids = (
|
|
140
189
|
torch.cat(
|
|
@@ -144,16 +193,32 @@ class HttpChainInference:
|
|
|
144
193
|
if generated
|
|
145
194
|
else input_ids
|
|
146
195
|
)
|
|
196
|
+
token_ids = full_ids[0].tolist()
|
|
147
197
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
if not upstream:
|
|
199
|
+
# Mother-only / single-node: run everything locally.
|
|
200
|
+
hidden = self.runner.forward_prefix(
|
|
201
|
+
torch.tensor([token_ids], dtype=torch.long)
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
first = upstream[0]
|
|
205
|
+
first_url = host_http_url(first, swarm_manifest)
|
|
206
|
+
hidden = self._remote_prefix(client, first_url, token_ids)
|
|
207
|
+
logger.debug(
|
|
208
|
+
"HTTP chain step %s: prefix via %s ok",
|
|
209
|
+
step,
|
|
210
|
+
first.get("host_id"),
|
|
211
|
+
)
|
|
212
|
+
for mid in upstream[1:]:
|
|
213
|
+
mid_url = host_http_url(mid, swarm_manifest)
|
|
214
|
+
hidden = self._remote_continue(client, mid_url, hidden)
|
|
215
|
+
logger.debug(
|
|
216
|
+
"HTTP chain step %s: continue via %s ok",
|
|
217
|
+
step,
|
|
218
|
+
mid.get("host_id"),
|
|
219
|
+
)
|
|
220
|
+
hidden = self.runner.forward_tail(hidden)
|
|
155
221
|
|
|
156
|
-
hidden = self.runner.forward_tail(hidden)
|
|
157
222
|
logits = self.runner.logits_from_hidden(hidden)
|
|
158
223
|
next_logits = logits[0, -1, :]
|
|
159
224
|
|
package/src/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export const DEFAULT_DISCOVERY_URL =
|
|
3
3
|
process.env.BLITZWING_DISCOVERY_URL ||
|
|
4
4
|
process.env.npm_package_config_discoveryUrl ||
|
|
5
|
-
"http://35.
|
|
5
|
+
"http://35.226.124.189: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`;
|
package/src/wizard.js
CHANGED
|
@@ -117,48 +117,77 @@ async function wizard(args) {
|
|
|
117
117
|
process.exit(1);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
120
|
+
const nonInteractive =
|
|
121
|
+
process.env.BLITZWING_NONINTERACTIVE === "1" ||
|
|
122
|
+
process.env.BLITZWING_YES === "1" ||
|
|
123
|
+
!process.stdin.isTTY;
|
|
124
|
+
|
|
125
|
+
let layersN;
|
|
126
|
+
if (nonInteractive && process.env.BLITZWING_LAYERS) {
|
|
127
|
+
layersN = Number(process.env.BLITZWING_LAYERS);
|
|
128
|
+
if (!Number.isInteger(layersN) || layersN < 1 || layersN > maxLayers) {
|
|
129
|
+
p.cancel(`BLITZWING_LAYERS must be an integer between 1 and ${maxLayers}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
p.log.info(`Layers: ${layersN} (non-interactive)`);
|
|
133
|
+
} else {
|
|
134
|
+
const layers = await p.text({
|
|
135
|
+
message: `How many layers can this machine host? (1–${maxLayers})`,
|
|
136
|
+
initialValue: String(Math.min(8, maxLayers)),
|
|
137
|
+
validate(v) {
|
|
138
|
+
const n = Number(v);
|
|
139
|
+
if (!Number.isInteger(n) || n < 1 || n > maxLayers) {
|
|
140
|
+
return `Enter an integer between 1 and ${maxLayers}`;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
if (p.isCancel(layers)) {
|
|
145
|
+
p.cancel("Setup cancelled");
|
|
146
|
+
process.exit(0);
|
|
147
|
+
}
|
|
148
|
+
layersN = Number(layers);
|
|
133
149
|
}
|
|
134
|
-
const layersN = Number(layers);
|
|
135
150
|
|
|
136
151
|
const hederaPrefill =
|
|
137
152
|
process.env.BLITZWING_HEDERA_ACCOUNT_ID ||
|
|
138
153
|
process.env.HEDERA_ACCOUNT_ID ||
|
|
139
154
|
existing?.hedera_account_id ||
|
|
140
155
|
"";
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
p.
|
|
151
|
-
|
|
156
|
+
let hederaAccountId;
|
|
157
|
+
if (nonInteractive && hederaPrefill) {
|
|
158
|
+
hederaAccountId = String(hederaPrefill).trim();
|
|
159
|
+
if (!/^0\.0\.\d+$/.test(hederaAccountId)) {
|
|
160
|
+
p.cancel("BLITZWING_HEDERA_ACCOUNT_ID must look like 0.0.123456");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
p.log.info(`Hedera payouts: ${hederaAccountId} (non-interactive)`);
|
|
164
|
+
} else {
|
|
165
|
+
const hederaAccount = await p.text({
|
|
166
|
+
message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
|
|
167
|
+
initialValue: hederaPrefill,
|
|
168
|
+
validate(v) {
|
|
169
|
+
const s = String(v || "").trim();
|
|
170
|
+
if (!/^0\.0\.\d+$/.test(s)) return "Enter a Hedera account like 0.0.123456";
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
if (p.isCancel(hederaAccount)) {
|
|
174
|
+
p.cancel("Setup cancelled");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
hederaAccountId = String(hederaAccount).trim();
|
|
152
178
|
}
|
|
153
|
-
const hederaAccountId = String(hederaAccount).trim();
|
|
154
179
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
p.
|
|
161
|
-
|
|
180
|
+
if (!nonInteractive) {
|
|
181
|
+
const confirm = await p.confirm({
|
|
182
|
+
message: `Join ${selected.model} hosting ${layersN} layers, payouts to ${hederaAccountId}?`,
|
|
183
|
+
initialValue: true,
|
|
184
|
+
});
|
|
185
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
186
|
+
p.cancel("Setup cancelled");
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
p.log.info(`Joining ${selected.model} with ${layersN} layers…`);
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
spin.start("Preparing Python environment + Petals");
|