blitzwing 0.1.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # blitzwing
2
2
 
3
- Interactive setup wizard to contribute layers to a Blitzwing Petals mother swarm.
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 talks to the **Discovery Service** (no mother URL to type), asks which model and how many layers to host, installs Petals if needed, and joins the swarm.
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
- Override discovery URL (dev/staging only):
33
+ ## Env (optional)
25
34
 
26
35
  ```bash
27
- export BLITZWING_DISCOVERY_URL=http://127.0.0.1:9000
28
- blitzwing
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.1.9",
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