2026-05-16 05:49:43 +00:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""Pipeline-parallel tinygrad worker — stdin/stdout JSON protocol.
|
|
|
|
|
|
|
|
|
|
|
|
Each worker hosts one *stage* of a pipeline-parallel inference run. The
|
|
|
|
|
|
stage index, total stage count, and the model name are read from
|
|
|
|
|
|
environment variables at startup:
|
|
|
|
|
|
|
|
|
|
|
|
STAGE=0 NUM_STAGES=2 MODEL=llama3.2:1b
|
|
|
|
|
|
|
|
|
|
|
|
The worker runs in one of two modes:
|
|
|
|
|
|
|
|
|
|
|
|
* **stub** (``--stub`` flag or ``PP_WORKER_STUB=1`` env): skips loading
|
|
|
|
|
|
any real model. Stage-0 ops return deterministic pseudo-bf16 hidden
|
|
|
|
|
|
bytes derived from the inputs; the final-stage op returns a
|
|
|
|
|
|
deterministic ``token_id``. ``tokenize`` whitespace-splits and hashes;
|
|
|
|
|
|
``detokenize`` formats the token ids as text. This mode is used by
|
|
|
|
|
|
the Rust actor and integration tests so they never need a GPU.
|
|
|
|
|
|
|
|
|
|
|
|
* **real** (default): loads ``$MODEL`` via ``Transformer.from_gguf`` and
|
|
|
|
|
|
``SimpleTokenizer.from_gguf_kv`` and runs the per-stage forward pass
|
|
|
|
|
|
directly against ``model.blk[start:end]`` (bypassing
|
|
|
|
|
|
``Transformer.forward`` and ``forward_jit``). Hidden state is cast to
|
|
|
|
|
|
float16 on the wire (2 bytes per element); decoded back to float16
|
|
|
|
|
|
on the receiving stage. Stage-1's ``forward_and_sample`` follows the
|
|
|
|
|
|
block range with ``output_norm`` + ``output`` and argmax-samples the
|
|
|
|
|
|
last position's logits.
|
|
|
|
|
|
|
|
|
|
|
|
Protocol — one JSON line in, one JSON line out.
|
|
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
Each op is restricted to the role that owns it (First = stage 0,
|
|
|
|
|
|
Last = stage N-1, Middle = anything in between). Ops invoked on the
|
|
|
|
|
|
wrong role return ``{"error": ...}`` and the worker keeps serving.
|
|
|
|
|
|
|
|
|
|
|
|
First-stage ops (``STAGE == 0``)::
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
-> {"op": "embed_and_forward", "request_id": <int>,
|
|
|
|
|
|
"tokens": [<int>, ...], "position": <int>}
|
|
|
|
|
|
<- {"request_id": <int>, "hidden_b64": "<base64>", "seq_len": <int>}
|
|
|
|
|
|
|
|
|
|
|
|
-> {"op": "decode_step", "request_id": <int>,
|
|
|
|
|
|
"token_id": <int>, "position": <int>}
|
|
|
|
|
|
<- {"request_id": <int>, "hidden_b64": "<base64>", "seq_len": 1}
|
|
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
-> {"op": "tokenize", "request_id": <int>, "prompt": "<text>"}
|
|
|
|
|
|
<- {"request_id": <int>, "tokens": [<int>, ...]}
|
|
|
|
|
|
|
|
|
|
|
|
Middle-stage op (``0 < STAGE < NUM_STAGES - 1``)::
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
-> {"op": "forward_range", "request_id": <int>,
|
2026-05-16 05:49:43 +00:00
|
|
|
|
"hidden_b64": "<base64>", "position": <int>, "seq_len": <int>}
|
2026-05-20 07:41:30 +00:00
|
|
|
|
<- {"request_id": <int>, "hidden_b64": "<base64>", "seq_len": <int>}
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
Last-stage ops (``STAGE == NUM_STAGES - 1``)::
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
-> {"op": "forward_and_sample", "request_id": <int>,
|
|
|
|
|
|
"hidden_b64": "<base64>", "position": <int>, "seq_len": <int>}
|
|
|
|
|
|
<- {"request_id": <int>, "token_id": <int>}
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
-> {"op": "detokenize", "request_id": <int>, "tokens": [<int>, ...]}
|
|
|
|
|
|
<- {"request_id": <int>, "text": "<text>"}
|
|
|
|
|
|
|
|
|
|
|
|
Errors of any kind reply with::
|
|
|
|
|
|
|
|
|
|
|
|
<- {"request_id": <int>?, "error": "<message>"}
|
|
|
|
|
|
|
|
|
|
|
|
and the worker keeps serving. EOF on stdin causes a clean exit 0.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import base64
|
2026-05-28 07:05:41 +00:00
|
|
|
|
import functools
|
2026-05-16 05:49:43 +00:00
|
|
|
|
import hashlib
|
2026-05-28 07:05:41 +00:00
|
|
|
|
import io
|
2026-05-16 05:49:43 +00:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import re
|
2026-05-25 18:19:06 +00:00
|
|
|
|
import signal
|
2026-05-28 07:05:41 +00:00
|
|
|
|
import struct
|
2026-05-16 05:49:43 +00:00
|
|
|
|
import sys
|
2026-05-25 18:19:06 +00:00
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
2026-05-28 07:05:41 +00:00
|
|
|
|
import urllib.parse
|
|
|
|
|
|
import urllib.request
|
|
|
|
|
|
from pathlib import Path
|
2026-05-16 05:49:43 +00:00
|
|
|
|
from typing import Sequence
|
|
|
|
|
|
|
|
|
|
|
|
# Stub-mode constants — small so test payloads stay tiny. Both values
|
|
|
|
|
|
# must match what the Rust stub-mode actor tests assume.
|
|
|
|
|
|
STUB_HIDDEN_DIM = 16
|
|
|
|
|
|
STUB_VOCAB_SIZE = 32
|
|
|
|
|
|
BYTES_PER_ELEM = 2 # bf16
|
|
|
|
|
|
|
|
|
|
|
|
# ASCII-only integer; rejects unicode digits and surrounding whitespace.
|
|
|
|
|
|
_INT_RE = re.compile(r"-?[0-9]+")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_layer_range(stage: int, num_stages: int, total_blocks: int) -> tuple[int, int]:
|
|
|
|
|
|
"""Return the half-open ``[start, end)`` block range owned by ``stage``.
|
|
|
|
|
|
|
|
|
|
|
|
The split is ``total_blocks // num_stages`` per stage; the final stage
|
2026-05-20 07:41:30 +00:00
|
|
|
|
absorbs any remainder. ``num_stages`` must be ``>= 2`` — the example
|
|
|
|
|
|
does not serve single-node configurations (see
|
|
|
|
|
|
``examples/single-gpu-inference`` for that). Raises ``ValueError``
|
|
|
|
|
|
on out-of-range inputs.
|
2026-05-16 05:49:43 +00:00
|
|
|
|
"""
|
2026-05-20 07:41:30 +00:00
|
|
|
|
if num_stages < 2:
|
|
|
|
|
|
raise ValueError(f"num_stages must be >= 2, got {num_stages}")
|
2026-05-16 05:49:43 +00:00
|
|
|
|
if not (0 <= stage < num_stages):
|
|
|
|
|
|
raise ValueError(f"stage {stage} out of range [0, {num_stages})")
|
|
|
|
|
|
if total_blocks < num_stages:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"total_blocks {total_blocks} cannot be split into {num_stages} stages"
|
|
|
|
|
|
)
|
|
|
|
|
|
k = total_blocks // num_stages
|
|
|
|
|
|
start = stage * k
|
|
|
|
|
|
end = (stage + 1) * k if stage < num_stages - 1 else total_blocks
|
|
|
|
|
|
return start, end
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# ggml type tables for the sharded loader: quantized types map to
|
|
|
|
|
|
# (elements_per_block, bytes_per_block); native types map to byte width. These
|
|
|
|
|
|
# mirror tinygrad 0.12.0's ggml_data_to_tensor and let us size each tensor's raw
|
|
|
|
|
|
# byte slice so only the kept weights are copied off disk.
|
|
|
|
|
|
_GGML_QUANT_BLOCK = {2: (32, 18), 3: (32, 20), 8: (32, 34), 12: (256, 144), 14: (256, 210), 39: (32, 17)}
|
|
|
|
|
|
_GGML_NATIVE_ITEMSIZE = {0: 4, 1: 2, 16: 1, 17: 2, 18: 4}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ggml_tensor_nbytes(n_elements: int, ggml_type: int) -> int:
|
|
|
|
|
|
"""Raw byte size of an ``n_elements`` ggml tensor of ``ggml_type``."""
|
|
|
|
|
|
if ggml_type in _GGML_NATIVE_ITEMSIZE:
|
|
|
|
|
|
return _GGML_NATIVE_ITEMSIZE[ggml_type] * n_elements
|
|
|
|
|
|
if ggml_type in _GGML_QUANT_BLOCK:
|
|
|
|
|
|
elems_per_block, bytes_per_block = _GGML_QUANT_BLOCK[ggml_type]
|
|
|
|
|
|
return (n_elements // elems_per_block) * bytes_per_block
|
|
|
|
|
|
raise ValueError(f"unsupported ggml type {ggml_type}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─── Sharded download (spec §4.1 / §4.2 / §4.7) ───────────────────────────
|
|
|
|
|
|
#
|
|
|
|
|
|
# The worker fetches only the byte ranges its stage actually needs
|
|
|
|
|
|
# (§4.1), caches the partial GGUF idempotently (§4.2), and emits
|
|
|
|
|
|
# `pp_download_progress` events with bounded latency while downloading
|
|
|
|
|
|
# (§4.7). The cached file is a SPARSE file with the same apparent size
|
|
|
|
|
|
# as the source — kept tensors live at their original byte offsets, so
|
|
|
|
|
|
# `_load_sharded_transformer` opens it unchanged. Filesystem holes
|
|
|
|
|
|
# absorb the non-kept regions so the actual disk usage is
|
|
|
|
|
|
# O(per-stage shard size), not O(full model size).
|
|
|
|
|
|
|
|
|
|
|
|
_PP_DOWNLOAD_READ_CHUNK = 256 * 1024
|
|
|
|
|
|
_PP_HEADER_INITIAL = 1024 * 1024
|
|
|
|
|
|
_PP_HEADER_MAX = 64 * 1024 * 1024
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_round_up(n: int, align: int) -> int:
|
|
|
|
|
|
if align <= 0:
|
|
|
|
|
|
return n
|
|
|
|
|
|
return ((n + align - 1) // align) * align
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _PpHeaderTooShort(Exception):
|
|
|
|
|
|
"""Raised mid-parse when the header buffer ran out — caller grows it."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_parse_gguf_header(buf: bytes) -> "tuple[list[tuple[str, tuple, int, int]], int, dict]":
|
|
|
|
|
|
"""Parse a GGUF header from `buf`. Returns (t_infos, data_start, kv).
|
|
|
|
|
|
|
|
|
|
|
|
`t_infos` is a list of ``(name, dims, ggml_type, offset)`` tuples
|
|
|
|
|
|
matching what ``_load_sharded_transformer``'s in-file parser
|
|
|
|
|
|
produces. `data_start` is the absolute byte offset where tensor
|
|
|
|
|
|
data begins. Raises `_PpHeaderTooShort` if the header is larger
|
|
|
|
|
|
than `buf` — caller should re-fetch with a larger buffer.
|
|
|
|
|
|
|
|
|
|
|
|
Format reference: tinygrad 0.12.0's gguf reader. GGUF versions 2
|
|
|
|
|
|
and 3 share the parse shape; the file's u32 version is checked.
|
|
|
|
|
|
"""
|
|
|
|
|
|
bio = io.BytesIO(buf)
|
|
|
|
|
|
|
|
|
|
|
|
def _need(n: int) -> bytes:
|
|
|
|
|
|
start = bio.tell()
|
|
|
|
|
|
out = bio.read(n)
|
|
|
|
|
|
if len(out) != n:
|
|
|
|
|
|
raise _PpHeaderTooShort(f"need {n} bytes at {start}, got {len(out)}")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
def _unpack(fmt: str, nbytes: int):
|
|
|
|
|
|
return struct.unpack(fmt, _need(nbytes))[0]
|
|
|
|
|
|
|
|
|
|
|
|
def _read_u32() -> int:
|
|
|
|
|
|
return _unpack("<I", 4)
|
|
|
|
|
|
|
|
|
|
|
|
def _read_i32() -> int:
|
|
|
|
|
|
return _unpack("<i", 4)
|
|
|
|
|
|
|
|
|
|
|
|
def _read_u64() -> int:
|
|
|
|
|
|
return _unpack("<Q", 8)
|
|
|
|
|
|
|
|
|
|
|
|
def _read_str() -> str:
|
|
|
|
|
|
length = _read_u64()
|
|
|
|
|
|
return _need(length).decode("utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
def _read_arr():
|
|
|
|
|
|
elem_type = _read_i32()
|
|
|
|
|
|
count = _read_u64()
|
|
|
|
|
|
return [_readers[elem_type]() for _ in range(count)]
|
|
|
|
|
|
|
|
|
|
|
|
_readers = {
|
|
|
|
|
|
0: lambda: _unpack("<b", 1),
|
|
|
|
|
|
1: lambda: _unpack("<B", 1),
|
|
|
|
|
|
2: lambda: _unpack("<h", 2),
|
|
|
|
|
|
3: lambda: _unpack("<H", 2),
|
|
|
|
|
|
4: _read_u32,
|
|
|
|
|
|
5: _read_i32,
|
|
|
|
|
|
6: lambda: _unpack("<f", 4),
|
|
|
|
|
|
7: lambda: _unpack("<?", 1),
|
|
|
|
|
|
8: _read_str,
|
|
|
|
|
|
9: _read_arr,
|
|
|
|
|
|
10: _read_u64,
|
|
|
|
|
|
11: lambda: _unpack("<q", 8),
|
|
|
|
|
|
12: lambda: _unpack("<d", 8),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
magic = _need(4)
|
|
|
|
|
|
if magic != b"GGUF":
|
|
|
|
|
|
raise ValueError(f"not a GGUF artifact (magic={magic!r})")
|
|
|
|
|
|
version = _read_i32()
|
|
|
|
|
|
if version not in (2, 3):
|
|
|
|
|
|
raise ValueError(f"unsupported GGUF version {version}")
|
|
|
|
|
|
n_tensors = _read_u64()
|
|
|
|
|
|
n_kv = _read_u64()
|
|
|
|
|
|
|
|
|
|
|
|
kv: "dict[str, object]" = {}
|
|
|
|
|
|
for _ in range(n_kv):
|
|
|
|
|
|
key = _read_str()
|
|
|
|
|
|
typ = _read_i32()
|
|
|
|
|
|
kv[key] = _readers[typ]()
|
|
|
|
|
|
|
|
|
|
|
|
t_infos: "list[tuple[str, tuple, int, int]]" = []
|
|
|
|
|
|
for _ in range(n_tensors):
|
|
|
|
|
|
name = _read_str()
|
|
|
|
|
|
n_dims = _read_u32()
|
|
|
|
|
|
dims = tuple(_read_u64() for _ in range(n_dims))
|
|
|
|
|
|
ggml_type = _read_i32()
|
|
|
|
|
|
offset = _read_u64()
|
|
|
|
|
|
t_infos.append((name, dims, ggml_type, offset))
|
|
|
|
|
|
|
|
|
|
|
|
alignment = int(kv.get("general.alignment", 32))
|
|
|
|
|
|
data_start = _pp_round_up(bio.tell(), alignment)
|
|
|
|
|
|
return t_infos, data_start, kv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_kept_names(t_infos, stage: int, num_stages: int, kv: dict) -> "set[str]":
|
|
|
|
|
|
"""Return the set of tensor names this stage requires. Matches the
|
|
|
|
|
|
`_kept` predicate inside `_load_sharded_transformer` — both code
|
|
|
|
|
|
paths must agree on the kept set or the loader would try to realize
|
|
|
|
|
|
a tensor whose bytes were not fetched."""
|
|
|
|
|
|
arch = str(kv["general.architecture"])
|
|
|
|
|
|
total_blocks = int(kv[f"{arch}.block_count"])
|
|
|
|
|
|
start, end = compute_layer_range(stage, num_stages, total_blocks)
|
|
|
|
|
|
is_last = stage == num_stages - 1
|
|
|
|
|
|
names = {info[0] for info in t_infos}
|
|
|
|
|
|
tied_output = "output.weight" not in names
|
|
|
|
|
|
kept: "set[str]" = set()
|
|
|
|
|
|
for info in t_infos:
|
|
|
|
|
|
name = info[0]
|
|
|
|
|
|
keep = False
|
|
|
|
|
|
for i in range(start, end):
|
|
|
|
|
|
if name.startswith(f"blk.{i}."):
|
|
|
|
|
|
keep = True
|
|
|
|
|
|
break
|
|
|
|
|
|
if not keep:
|
|
|
|
|
|
if name == "token_embd.weight" and (stage == 0 or (is_last and tied_output)):
|
|
|
|
|
|
keep = True
|
|
|
|
|
|
elif name == "output_norm.weight" and is_last:
|
|
|
|
|
|
keep = True
|
|
|
|
|
|
elif name == "output.weight" and is_last and not tied_output:
|
|
|
|
|
|
keep = True
|
|
|
|
|
|
if keep:
|
|
|
|
|
|
kept.add(name)
|
|
|
|
|
|
return kept
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_kept_byte_ranges(t_infos, data_start: int, kept_names: "set[str]") -> "list[tuple[int, int]]":
|
|
|
|
|
|
"""Return the ``[(absolute_offset, nbytes)]`` byte ranges this stage
|
|
|
|
|
|
keeps, ordered by offset (so a streamed download writes ascending
|
|
|
|
|
|
offsets and the filesystem allocates fewer fragmented holes)."""
|
|
|
|
|
|
out: list[tuple[int, int]] = []
|
|
|
|
|
|
for name, dims, ggml_type, offset in t_infos:
|
|
|
|
|
|
if name not in kept_names:
|
|
|
|
|
|
continue
|
|
|
|
|
|
n_elements = 1
|
|
|
|
|
|
for d in dims:
|
|
|
|
|
|
n_elements *= int(d)
|
|
|
|
|
|
nbytes = _ggml_tensor_nbytes(n_elements, ggml_type)
|
|
|
|
|
|
out.append((data_start + int(offset), nbytes))
|
|
|
|
|
|
out.sort()
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_cache_paths(url: str) -> "tuple[Path, Path, Path]":
|
|
|
|
|
|
"""Return (cache_path, meta_path, partial_path) for ``url``.
|
|
|
|
|
|
|
|
|
|
|
|
Cache root defaults to ``$PP_MODEL_CACHE_DIR`` then ``~/.cache/pp-pipeline``.
|
|
|
|
|
|
The filename is ``<short-url-hash>-<basename>`` so two URLs that share a
|
|
|
|
|
|
basename cannot collide.
|
|
|
|
|
|
"""
|
|
|
|
|
|
raw = os.environ.get("PP_MODEL_CACHE_DIR", "").strip()
|
|
|
|
|
|
cache_root = Path(raw).expanduser() if raw else Path.home() / ".cache" / "pp-pipeline"
|
|
|
|
|
|
cache_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
url_hash = hashlib.sha256(url.encode("utf-8")).hexdigest()[:16]
|
|
|
|
|
|
basename = os.path.basename(urllib.parse.urlparse(url).path) or "model.gguf"
|
|
|
|
|
|
cache_path = cache_root / f"{url_hash}-{basename}"
|
|
|
|
|
|
meta_path = cache_path.with_name(cache_path.name + ".pp_meta")
|
|
|
|
|
|
partial_path = cache_path.with_name(cache_path.name + ".partial")
|
|
|
|
|
|
return cache_path, meta_path, partial_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_head(url: str) -> "tuple[int, str]":
|
|
|
|
|
|
"""HEAD request; return (Content-Length, Accept-Ranges header lowercased)."""
|
|
|
|
|
|
req = urllib.request.Request(url, method="HEAD")
|
|
|
|
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
|
|
|
|
total = int(resp.headers.get("Content-Length", "0"))
|
|
|
|
|
|
accept = (resp.headers.get("Accept-Ranges") or "").lower()
|
|
|
|
|
|
return total, accept
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_range_get(url: str, start: int, end_inclusive: int) -> bytes:
|
|
|
|
|
|
"""Issue a Range GET; return the body bytes."""
|
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
|
url, headers={"Range": f"bytes={start}-{end_inclusive}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
|
|
|
|
return resp.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_emit_progress(stage: int, bytes_done: int, bytes_total: int, started_at_mono: float) -> None:
|
|
|
|
|
|
"""Emit one `pp_download_progress` event with the spec's field set
|
|
|
|
|
|
(§4.7). `started_at_mono` is `time.monotonic()` captured before the
|
|
|
|
|
|
first event so `elapsed_ms` is monotonic across the fetch."""
|
|
|
|
|
|
elapsed_ms = int((time.monotonic() - started_at_mono) * 1000)
|
|
|
|
|
|
mbps = round(((bytes_done * 8) / 1_000_000) / max(elapsed_ms / 1000, 1e-3), 1)
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_download_progress",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
bytes_done=bytes_done,
|
|
|
|
|
|
bytes_total=bytes_total,
|
|
|
|
|
|
elapsed_ms=elapsed_ms,
|
|
|
|
|
|
mbps=mbps,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_fingerprint(f, offset: int, nbytes: int) -> str:
|
|
|
|
|
|
"""Hash the first 4KB + last 4KB of a kept tensor (or the whole
|
|
|
|
|
|
tensor if shorter). The §4.2 content-derived check: matches the
|
|
|
|
|
|
fingerprint recorded in the sidecar at download time."""
|
|
|
|
|
|
sample = 4096
|
|
|
|
|
|
f.seek(offset)
|
|
|
|
|
|
head = f.read(min(sample, nbytes))
|
|
|
|
|
|
if nbytes > sample:
|
|
|
|
|
|
f.seek(offset + nbytes - sample)
|
|
|
|
|
|
tail = f.read(sample)
|
|
|
|
|
|
else:
|
|
|
|
|
|
tail = b""
|
|
|
|
|
|
h = hashlib.sha256()
|
|
|
|
|
|
h.update(head)
|
|
|
|
|
|
h.update(tail)
|
|
|
|
|
|
h.update(nbytes.to_bytes(8, "little"))
|
|
|
|
|
|
return h.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_write_meta(
|
|
|
|
|
|
meta_path: Path,
|
|
|
|
|
|
cache_path: Path,
|
|
|
|
|
|
stage: int,
|
|
|
|
|
|
num_stages: int,
|
|
|
|
|
|
url: str,
|
|
|
|
|
|
total_size: int,
|
|
|
|
|
|
kept_ranges: "list[tuple[int, int]]",
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
fingerprints = []
|
|
|
|
|
|
with open(cache_path, "rb") as f:
|
|
|
|
|
|
for offset, nbytes in kept_ranges:
|
|
|
|
|
|
fingerprints.append({
|
|
|
|
|
|
"offset": offset,
|
|
|
|
|
|
"nbytes": nbytes,
|
|
|
|
|
|
"fingerprint": _pp_fingerprint(f, offset, nbytes),
|
|
|
|
|
|
})
|
|
|
|
|
|
meta_path.write_text(json.dumps({
|
|
|
|
|
|
"schema": 1,
|
|
|
|
|
|
"url": url,
|
|
|
|
|
|
"stage": stage,
|
|
|
|
|
|
"num_stages": num_stages,
|
|
|
|
|
|
"total_size": total_size,
|
|
|
|
|
|
"kept": fingerprints,
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_verify_cache(
|
|
|
|
|
|
cache_path: Path,
|
|
|
|
|
|
meta_path: Path,
|
|
|
|
|
|
stage: int,
|
|
|
|
|
|
num_stages: int,
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""Spec §4.2 integrity check: file present at the expected apparent
|
|
|
|
|
|
size AND every recorded fingerprint re-matches the cached bytes.
|
|
|
|
|
|
Returns False on any discrepancy (including missing files, missing
|
|
|
|
|
|
sidecar, mismatched stage / num_stages, size mismatch, or any
|
|
|
|
|
|
fingerprint mismatch). A passing cache is used as-is — no refetch."""
|
|
|
|
|
|
if not cache_path.exists() or not meta_path.exists():
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
meta = json.loads(meta_path.read_text())
|
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
|
return False
|
|
|
|
|
|
if meta.get("schema") != 1:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if meta.get("stage") != stage or meta.get("num_stages") != num_stages:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if cache_path.stat().st_size != meta.get("total_size"):
|
|
|
|
|
|
return False
|
|
|
|
|
|
kept = meta.get("kept") or []
|
|
|
|
|
|
if not kept:
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(cache_path, "rb") as f:
|
|
|
|
|
|
for entry in kept:
|
|
|
|
|
|
offset = int(entry["offset"])
|
|
|
|
|
|
nbytes = int(entry["nbytes"])
|
|
|
|
|
|
expected = entry["fingerprint"]
|
|
|
|
|
|
if _pp_fingerprint(f, offset, nbytes) != expected:
|
|
|
|
|
|
return False
|
|
|
|
|
|
except (OSError, KeyError, ValueError):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _pp_download_sharded(url: str, stage: int, num_stages: int) -> str:
|
|
|
|
|
|
"""Spec §4.1 + §4.2 + §4.7: fetch only this stage's tensor bytes,
|
|
|
|
|
|
cache idempotently, emit progress events.
|
|
|
|
|
|
|
|
|
|
|
|
Returns the path to the on-disk file (sparse — apparent size matches
|
|
|
|
|
|
the source; only the kept ranges occupy disk blocks). On cache hit,
|
|
|
|
|
|
NO `pp_download_progress` events are emitted (spec §4.7).
|
|
|
|
|
|
"""
|
|
|
|
|
|
cache_path, meta_path, partial_path = _pp_cache_paths(url)
|
|
|
|
|
|
|
|
|
|
|
|
# §4.2: orphan-cleanup any leftover .partial from a previous killed
|
|
|
|
|
|
# fetch BEFORE any new fetch is initiated. Emit an event so the
|
|
|
|
|
|
# bundle reader can see that a stale temp was reaped.
|
|
|
|
|
|
if partial_path.exists():
|
|
|
|
|
|
try:
|
|
|
|
|
|
partial_path.unlink()
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_cache_orphan_cleaned",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
path=str(partial_path),
|
|
|
|
|
|
)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# §4.2 cache hit — return the cached file as-is.
|
|
|
|
|
|
if _pp_verify_cache(cache_path, meta_path, stage, num_stages):
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_cache_hit",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
path=str(cache_path),
|
|
|
|
|
|
)
|
|
|
|
|
|
return str(cache_path)
|
|
|
|
|
|
|
|
|
|
|
|
# §4.1 fail-fast on no-range support.
|
|
|
|
|
|
total_size, accept_ranges = _pp_head(url)
|
|
|
|
|
|
if "bytes" not in accept_ranges:
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_download_failed",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
reason="no_byte_range_support",
|
|
|
|
|
|
url=url,
|
|
|
|
|
|
accept_ranges=accept_ranges,
|
|
|
|
|
|
)
|
|
|
|
|
|
_die(f"source does not support byte-range requests: {url}")
|
|
|
|
|
|
if total_size <= 0:
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_download_failed",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
reason="no_content_length",
|
|
|
|
|
|
url=url,
|
|
|
|
|
|
)
|
|
|
|
|
|
_die(f"source did not advertise Content-Length: {url}")
|
|
|
|
|
|
|
|
|
|
|
|
# Fetch the header in growing increments until we can parse it.
|
|
|
|
|
|
header_size = min(_PP_HEADER_INITIAL, total_size)
|
|
|
|
|
|
while True:
|
|
|
|
|
|
try:
|
|
|
|
|
|
header_bytes = _pp_range_get(url, 0, header_size - 1)
|
|
|
|
|
|
t_infos, data_start, kv = _pp_parse_gguf_header(header_bytes)
|
|
|
|
|
|
if data_start <= len(header_bytes):
|
|
|
|
|
|
break
|
|
|
|
|
|
# Parser succeeded structurally but data_start sits past our
|
|
|
|
|
|
# buffer — re-fetch enough to include the tensor data start.
|
|
|
|
|
|
header_size = min(data_start + 1024, total_size)
|
|
|
|
|
|
except _PpHeaderTooShort:
|
|
|
|
|
|
new_size = min(header_size * 2, total_size)
|
|
|
|
|
|
if new_size == header_size or new_size > _PP_HEADER_MAX:
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_download_failed",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
reason="header_too_large",
|
|
|
|
|
|
header_size=header_size,
|
|
|
|
|
|
)
|
|
|
|
|
|
_die(f"GGUF header exceeded {_PP_HEADER_MAX} bytes")
|
|
|
|
|
|
header_size = new_size
|
|
|
|
|
|
|
|
|
|
|
|
kept_names = _pp_kept_names(t_infos, stage, num_stages, kv)
|
|
|
|
|
|
kept_ranges = _pp_kept_byte_ranges(t_infos, data_start, kept_names)
|
|
|
|
|
|
|
|
|
|
|
|
# bytes_total is the bytes this stage will pull from the network:
|
|
|
|
|
|
# header + kept-tensor regions. Not the full file (spec §4.1 means
|
|
|
|
|
|
# we never fetch the rest).
|
|
|
|
|
|
header_keep_bytes = data_start
|
|
|
|
|
|
kept_bytes_total = sum(nb for _, nb in kept_ranges)
|
|
|
|
|
|
bytes_total = header_keep_bytes + kept_bytes_total
|
|
|
|
|
|
|
|
|
|
|
|
interval_s_raw = os.environ.get("PP_DOWNLOAD_PROGRESS_INTERVAL_SECS", "").strip()
|
|
|
|
|
|
try:
|
|
|
|
|
|
interval_s = float(interval_s_raw) if interval_s_raw else 10.0
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
interval_s = 10.0
|
|
|
|
|
|
if interval_s <= 0:
|
|
|
|
|
|
interval_s = 10.0
|
|
|
|
|
|
|
|
|
|
|
|
# Write the sparse output to `.partial`; rename on success. Opening
|
|
|
|
|
|
# with "wb" then truncate(total_size) creates a sparse file on
|
|
|
|
|
|
# Linux: only blocks we actually `write()` allocate disk.
|
|
|
|
|
|
started_at = time.monotonic()
|
|
|
|
|
|
with open(partial_path, "wb") as f:
|
|
|
|
|
|
f.truncate(total_size)
|
|
|
|
|
|
f.seek(0)
|
|
|
|
|
|
f.write(header_bytes[:data_start])
|
|
|
|
|
|
bytes_done = data_start
|
|
|
|
|
|
|
|
|
|
|
|
# Spec §4.7: first event MUST be at start of fetch, AFTER we
|
|
|
|
|
|
# know bytes_total. We have that now.
|
|
|
|
|
|
_pp_emit_progress(stage, bytes_done, bytes_total, started_at)
|
|
|
|
|
|
last_emit = time.monotonic()
|
|
|
|
|
|
|
|
|
|
|
|
for offset, nbytes in kept_ranges:
|
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
|
url,
|
|
|
|
|
|
headers={"Range": f"bytes={offset}-{offset + nbytes - 1}"},
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
resp = urllib.request.urlopen(req, timeout=300)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"pp_download_failed",
|
|
|
|
|
|
stage_index=stage,
|
|
|
|
|
|
reason="range_get_failed",
|
|
|
|
|
|
offset=offset,
|
|
|
|
|
|
nbytes=nbytes,
|
|
|
|
|
|
error=str(e),
|
|
|
|
|
|
)
|
|
|
|
|
|
# Spec §4.7: final event MUST be emitted on fetch failure.
|
|
|
|
|
|
_pp_emit_progress(stage, bytes_done, bytes_total, started_at)
|
|
|
|
|
|
_die(f"range GET failed at offset {offset}: {e}")
|
|
|
|
|
|
try:
|
|
|
|
|
|
f.seek(offset)
|
|
|
|
|
|
while True:
|
|
|
|
|
|
chunk = resp.read(_PP_DOWNLOAD_READ_CHUNK)
|
|
|
|
|
|
if not chunk:
|
|
|
|
|
|
break
|
|
|
|
|
|
f.write(chunk)
|
|
|
|
|
|
bytes_done += len(chunk)
|
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
|
if now - last_emit >= interval_s:
|
|
|
|
|
|
_pp_emit_progress(stage, bytes_done, bytes_total, started_at)
|
|
|
|
|
|
last_emit = now
|
|
|
|
|
|
finally:
|
|
|
|
|
|
resp.close()
|
|
|
|
|
|
|
|
|
|
|
|
# Spec §4.7: final event at completion.
|
|
|
|
|
|
_pp_emit_progress(stage, bytes_done, bytes_total, started_at)
|
|
|
|
|
|
|
|
|
|
|
|
# Sidecar before rename so a crash between rename + meta-write does
|
|
|
|
|
|
# not leave a "valid file, no sidecar" → would fail _pp_verify and
|
|
|
|
|
|
# refetch. Writing the sidecar first means a crash here leaves
|
|
|
|
|
|
# cache_path absent and partial_path present (which orphan-cleanup
|
|
|
|
|
|
# reaps on next boot).
|
|
|
|
|
|
_pp_write_meta(
|
|
|
|
|
|
meta_path, partial_path, stage, num_stages, url, total_size, kept_ranges
|
|
|
|
|
|
)
|
|
|
|
|
|
os.replace(partial_path, cache_path)
|
|
|
|
|
|
return str(cache_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_sharded_transformer(gguf_path, stage: int, num_stages: int, max_context: int = 512):
|
|
|
|
|
|
"""Load only this stage's slice of the model onto the compute device.
|
|
|
|
|
|
|
|
|
|
|
|
Stock ``Transformer.from_gguf`` copies the *entire* GGUF onto the compute
|
|
|
|
|
|
device before any layer runs (it does ``gguf.to(None)``), so an 18 GB model
|
|
|
|
|
|
OOMs a 12 GB GPU no matter how the layers are split. Instead we parse the
|
|
|
|
|
|
GGUF header on the DISK device and copy only the tensors this stage needs —
|
|
|
|
|
|
``blk[start:end]`` plus ``token_embd`` (stage 0) and ``output_norm`` /
|
|
|
|
|
|
``output`` (last stage) — dequantizing each on the compute device. Returns
|
|
|
|
|
|
``(model, kv, start, end)``. Vendored against tinygrad 0.12.0's gguf format.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import io
|
|
|
|
|
|
import struct
|
|
|
|
|
|
import functools
|
|
|
|
|
|
|
|
|
|
|
|
from tinygrad import Tensor, Device, nn
|
|
|
|
|
|
from tinygrad.helpers import prod, round_up, getenv
|
|
|
|
|
|
from tinygrad.nn.state import TensorIO, ggml_data_to_tensor
|
|
|
|
|
|
from tinygrad.apps.llm import Transformer
|
|
|
|
|
|
|
|
|
|
|
|
_t0 = time.monotonic()
|
2026-05-29 08:55:10 +00:00
|
|
|
|
gguf = Tensor(Path(gguf_path)) # device is DISK:<path> — nothing is copied to the GPU yet
|
2026-05-28 07:05:41 +00:00
|
|
|
|
|
|
|
|
|
|
# --- parse the GGUF header (kv metadata + tensor directory) off disk ---
|
|
|
|
|
|
reader = io.BufferedReader(TensorIO(gguf), 1_000_000)
|
|
|
|
|
|
|
|
|
|
|
|
def _unpack(fmt, nbytes):
|
|
|
|
|
|
return struct.unpack(fmt, reader.read(nbytes))[0]
|
|
|
|
|
|
|
|
|
|
|
|
def _read_str():
|
|
|
|
|
|
return str(reader.read(_read_u64()), "utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
def _read_arr():
|
|
|
|
|
|
elem_reader, count = _readers[_read_i32()], _read_u64()
|
|
|
|
|
|
return [elem_reader() for _ in range(count)]
|
|
|
|
|
|
|
|
|
|
|
|
_readers = {8: _read_str, 9: _read_arr, **{t: functools.partial(_unpack, "<" + f, nb) for t, f, nb in
|
|
|
|
|
|
[(0, "c", 1), (1, "b", 1), (2, "H", 2), (3, "h", 2), (4, "I", 4), (5, "i", 4),
|
|
|
|
|
|
(6, "f", 4), (7, "?", 1), (10, "Q", 8), (11, "q", 8), (12, "d", 8)]}}
|
|
|
|
|
|
_read_u32, _read_i32, _read_u64 = _readers[4], _readers[5], _readers[10]
|
|
|
|
|
|
|
|
|
|
|
|
magic, version = reader.read(4), _read_i32()
|
|
|
|
|
|
n_tensors, n_kv = _read_u64(), _read_u64()
|
|
|
|
|
|
if magic != b"GGUF" or version not in (2, 3):
|
|
|
|
|
|
raise ValueError(f"invalid GGUF (magic={magic!r} version={version})")
|
|
|
|
|
|
kv = {}
|
|
|
|
|
|
for _ in range(n_kv):
|
|
|
|
|
|
key, typ = _read_str(), _read_i32()
|
|
|
|
|
|
kv[key] = _readers[typ]()
|
|
|
|
|
|
t_infos = [(_read_str(), tuple(_read_u64() for _ in range(_read_u32())), _read_i32(), _read_u64())
|
|
|
|
|
|
for _ in range(n_tensors)]
|
|
|
|
|
|
data_start = round_up(reader.tell(), kv.get("general.alignment", 32))
|
|
|
|
|
|
_t_header = time.monotonic()
|
|
|
|
|
|
|
|
|
|
|
|
arch = kv["general.architecture"]
|
|
|
|
|
|
total_blocks = int(kv[f"{arch}.block_count"])
|
|
|
|
|
|
start, end = compute_layer_range(stage, num_stages, total_blocks)
|
|
|
|
|
|
is_last = stage == num_stages - 1
|
|
|
|
|
|
names = {info[0] for info in t_infos}
|
|
|
|
|
|
tied_output = "output.weight" not in names # small models tie output to token_embd
|
|
|
|
|
|
|
|
|
|
|
|
def _kept(name: str) -> bool:
|
|
|
|
|
|
for i in range(start, end):
|
|
|
|
|
|
if name.startswith(f"blk.{i}."):
|
|
|
|
|
|
return True
|
|
|
|
|
|
if name == "token_embd.weight" and (stage == 0 or (is_last and tied_output)):
|
|
|
|
|
|
return True
|
|
|
|
|
|
if name == "output_norm.weight" and is_last:
|
|
|
|
|
|
return True
|
|
|
|
|
|
if name == "output.weight" and is_last and not tied_output:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
half, device = getenv("HALF", 1), Device.DEFAULT
|
|
|
|
|
|
state_dict = {}
|
|
|
|
|
|
bytes_copied = 0
|
|
|
|
|
|
kept_count = 0
|
|
|
|
|
|
for name, dims, ggml_type, offset in t_infos:
|
|
|
|
|
|
n_elements = prod(dims)
|
|
|
|
|
|
if _kept(name):
|
|
|
|
|
|
nbytes = _ggml_tensor_nbytes(n_elements, ggml_type)
|
|
|
|
|
|
bytes_copied += nbytes
|
|
|
|
|
|
kept_count += 1
|
|
|
|
|
|
raw = gguf[data_start + offset: data_start + offset + nbytes].to(device)
|
|
|
|
|
|
tensor = ggml_data_to_tensor(raw, n_elements, ggml_type).reshape(*reversed(dims))
|
|
|
|
|
|
if arch == "llama": # interleaved -> half-split RoPE layout (llama-style only)
|
|
|
|
|
|
n_heads, n_kv_heads = kv[f"{arch}.attention.head_count"], kv[f"{arch}.attention.head_count_kv"]
|
|
|
|
|
|
if "attn_q.weight" in name:
|
|
|
|
|
|
tensor = tensor.rearrange("(n h two) d -> (n two h) d", n=n_heads, two=2)
|
|
|
|
|
|
if "attn_k.weight" in name:
|
|
|
|
|
|
tensor = tensor.rearrange("(n h two) d -> (n two h) d", n=n_kv_heads, two=2)
|
|
|
|
|
|
state_dict[name] = tensor.cast("float16") if half else tensor
|
|
|
|
|
|
else:
|
|
|
|
|
|
# DISK-rooted lazy tensor: only its .shape is read (model construction); never realized.
|
|
|
|
|
|
state_dict[name] = ggml_data_to_tensor(gguf[data_start + offset:], n_elements, ggml_type).reshape(*reversed(dims))
|
|
|
|
|
|
if tied_output and is_last:
|
|
|
|
|
|
state_dict["output.weight"] = state_dict["token_embd.weight"]
|
|
|
|
|
|
_t_statedict = time.monotonic()
|
|
|
|
|
|
|
|
|
|
|
|
n_heads = kv[f"{arch}.attention.head_count"]
|
|
|
|
|
|
model = Transformer(
|
|
|
|
|
|
num_blocks=total_blocks, dim=kv[f"{arch}.embedding_length"],
|
|
|
|
|
|
hidden_dim=kv.get(f"{arch}.expert_feed_forward_length", kv[f"{arch}.feed_forward_length"]),
|
|
|
|
|
|
n_heads=n_heads, n_kv_heads=kv[f"{arch}.attention.head_count_kv"],
|
|
|
|
|
|
norm_eps=kv[f"{arch}.attention.layer_norm_rms_epsilon"], vocab_size=len(kv["tokenizer.ggml.tokens"]),
|
|
|
|
|
|
head_dim=kv.get(f"{arch}.attention.key_length", kv[f"{arch}.embedding_length"] // n_heads),
|
|
|
|
|
|
rope_theta=kv[f"{arch}.rope.freq_base"], max_context=max_context,
|
|
|
|
|
|
qk_norm=int(state_dict["blk.0.attn_q_norm.weight"].shape[0]) if "blk.0.attn_q_norm.weight" in state_dict else 0,
|
|
|
|
|
|
num_experts=kv.get(f"{arch}.expert_count", 0), num_experts_per_tok=kv.get(f"{arch}.expert_used_count", 0))
|
|
|
|
|
|
|
|
|
|
|
|
_t_construct = time.monotonic()
|
|
|
|
|
|
|
|
|
|
|
|
# Provide only the kept weights; strict=False leaves the other blocks at their
|
|
|
|
|
|
# (lazy, never-run) init so they never touch the compute device.
|
|
|
|
|
|
kept = {name: tensor for name, tensor in state_dict.items()
|
|
|
|
|
|
if _kept(name) or (tied_output and is_last and name == "output.weight")}
|
|
|
|
|
|
# This is where the kept tensors are actually copied off disk and
|
|
|
|
|
|
# dequantized on the compute device — the dominant load cost.
|
|
|
|
|
|
nn.state.load_state_dict(model, kept, strict=False, verbose=False, consume=True, realize=True)
|
|
|
|
|
|
_t_realize = time.monotonic()
|
|
|
|
|
|
|
|
|
|
|
|
realize_ms = (_t_realize - _t_construct) * 1000
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"model_load_breakdown",
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
resident_blocks=end - start,
|
|
|
|
|
|
total_blocks=total_blocks,
|
|
|
|
|
|
kept_tensors=kept_count,
|
|
|
|
|
|
bytes_copied=bytes_copied,
|
|
|
|
|
|
mb_copied=round(bytes_copied / 1_000_000, 1),
|
|
|
|
|
|
header_ms=round((_t_header - _t0) * 1000, 1),
|
|
|
|
|
|
statedict_build_ms=round((_t_statedict - _t_header) * 1000, 1),
|
|
|
|
|
|
construct_ms=round((_t_construct - _t_statedict) * 1000, 1),
|
|
|
|
|
|
realize_ms=round(realize_ms, 1),
|
|
|
|
|
|
realize_mb_per_s=round((bytes_copied / 1_000_000) / max(realize_ms / 1000, 1e-3), 1),
|
|
|
|
|
|
rss_mb=_rss_mb(),
|
|
|
|
|
|
)
|
|
|
|
|
|
return model, kv, start, end
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
def argmax_sample(logits: Sequence[float]) -> int:
|
|
|
|
|
|
"""Return the index of the maximum element in ``logits``.
|
|
|
|
|
|
|
|
|
|
|
|
On ties, the lowest index wins. Raises ``ValueError`` on empty input.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if len(logits) == 0:
|
|
|
|
|
|
raise ValueError("argmax over empty logits")
|
|
|
|
|
|
best_idx = 0
|
|
|
|
|
|
best_val = logits[0]
|
|
|
|
|
|
for i in range(1, len(logits)):
|
|
|
|
|
|
if logits[i] > best_val:
|
|
|
|
|
|
best_val = logits[i]
|
|
|
|
|
|
best_idx = i
|
|
|
|
|
|
return best_idx
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stub_hidden_bytes(tokens: Sequence[int], position: int) -> bytes:
|
|
|
|
|
|
"""Deterministic pseudo-bf16 bytes of length ``len(tokens) * hidden_dim * 2``.
|
|
|
|
|
|
|
|
|
|
|
|
Same ``(tokens, position)`` always produce the same bytes; different
|
|
|
|
|
|
inputs almost always differ. Bytes are derived from SHA-256 so the
|
|
|
|
|
|
payload is non-trivial (not all zeros, not monotonic).
|
|
|
|
|
|
"""
|
|
|
|
|
|
seed = hashlib.sha256(
|
|
|
|
|
|
b"pp-stub:"
|
|
|
|
|
|
+ str(position).encode()
|
|
|
|
|
|
+ b"|"
|
|
|
|
|
|
+ b",".join(str(int(t)).encode() for t in tokens)
|
|
|
|
|
|
).digest()
|
|
|
|
|
|
n_bytes = len(tokens) * STUB_HIDDEN_DIM * BYTES_PER_ELEM
|
|
|
|
|
|
out = bytearray()
|
|
|
|
|
|
counter = 0
|
|
|
|
|
|
while len(out) < n_bytes:
|
|
|
|
|
|
out.extend(hashlib.sha256(seed + counter.to_bytes(8, "little")).digest())
|
|
|
|
|
|
counter += 1
|
|
|
|
|
|
return bytes(out[:n_bytes])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
def _stub_forward_range_bytes(hidden: bytes, position: int, seq_len: int) -> bytes:
|
|
|
|
|
|
"""Deterministic pseudo-bf16 bytes for a middle stage's ``forward_range``.
|
|
|
|
|
|
|
|
|
|
|
|
Output length is ``seq_len * STUB_HIDDEN_DIM * 2`` (matches the input
|
|
|
|
|
|
hidden's expected length). Same ``(hidden, position, seq_len)`` always
|
|
|
|
|
|
produce the same bytes; differing inputs almost always differ. Used
|
|
|
|
|
|
only in stub mode.
|
|
|
|
|
|
"""
|
|
|
|
|
|
n_bytes = seq_len * STUB_HIDDEN_DIM * BYTES_PER_ELEM
|
|
|
|
|
|
seed = hashlib.sha256(
|
|
|
|
|
|
b"pp-stub-fr:" + position.to_bytes(8, "little", signed=False) + hidden
|
|
|
|
|
|
).digest()
|
|
|
|
|
|
out = bytearray()
|
|
|
|
|
|
counter = 0
|
|
|
|
|
|
while len(out) < n_bytes:
|
|
|
|
|
|
out.extend(hashlib.sha256(seed + counter.to_bytes(8, "little")).digest())
|
|
|
|
|
|
counter += 1
|
|
|
|
|
|
return bytes(out[:n_bytes])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
def _stub_token_id(hidden: bytes, position: int) -> int:
|
|
|
|
|
|
"""Deterministic stub token id in ``[0, STUB_VOCAB_SIZE)``."""
|
|
|
|
|
|
digest = hashlib.sha256(
|
|
|
|
|
|
b"pp-stub-sample:" + position.to_bytes(8, "little", signed=False) + hidden
|
|
|
|
|
|
).digest()
|
|
|
|
|
|
return int.from_bytes(digest[:4], "little") % STUB_VOCAB_SIZE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_env_int(name: str) -> int:
|
|
|
|
|
|
raw = os.environ.get(name, "")
|
|
|
|
|
|
stripped = raw.strip()
|
|
|
|
|
|
if not stripped:
|
|
|
|
|
|
_die(f"env {name} is required")
|
|
|
|
|
|
if not _INT_RE.fullmatch(stripped):
|
|
|
|
|
|
_die(f"env {name}={raw!r} is not an ASCII integer")
|
|
|
|
|
|
return int(stripped)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _die(msg: str) -> "None":
|
|
|
|
|
|
print(f"pp_tinygrad_worker: {msg}", file=sys.stderr, flush=True)
|
|
|
|
|
|
raise SystemExit(2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _write(obj) -> None:
|
|
|
|
|
|
sys.stdout.write(json.dumps(obj) + "\n")
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 18:19:06 +00:00
|
|
|
|
# ─── Lifecycle event emission ─────────────────────────────────────────────
|
|
|
|
|
|
#
|
|
|
|
|
|
# The Rust StageActor parses every stdout line as JSON; any line carrying
|
|
|
|
|
|
# `"event": "<kind>"` is re-emitted as `Custom("worker_<kind>")` into the
|
|
|
|
|
|
# diagnostic bundle. The worker subprocess is otherwise opaque to the
|
|
|
|
|
|
# Rust side, so these are the only diagnostic signal the bundle ever sees
|
|
|
|
|
|
# from the Python layer (apart from exit code + stderr tail). We do NOT
|
|
|
|
|
|
# emit a `request_id` on event lines so the actor never confuses an event
|
|
|
|
|
|
# with an op reply.
|
|
|
|
|
|
|
|
|
|
|
|
_WORKER_START_MONOTONIC = time.monotonic()
|
|
|
|
|
|
_REQUESTS_SERVED = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _emit_event(kind: str, **fields) -> None:
|
|
|
|
|
|
"""Emit a structured lifecycle event on stdout. The Rust actor folds
|
|
|
|
|
|
these into the diag bundle as `Custom("worker_<kind>")`."""
|
|
|
|
|
|
payload = {"event": kind, **fields}
|
|
|
|
|
|
try:
|
|
|
|
|
|
sys.stdout.write(json.dumps(payload) + "\n")
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# Best-effort: never let a logging failure crash the worker.
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _uptime_ms() -> int:
|
|
|
|
|
|
return int((time.monotonic() - _WORKER_START_MONOTONIC) * 1000)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 07:05:41 +00:00
|
|
|
|
def _wall_ms() -> int:
|
|
|
|
|
|
"""Epoch milliseconds. Lets the bundle align worker events across nodes
|
|
|
|
|
|
and against the orchestrator's vast.ai create/lease timestamps — e.g.
|
|
|
|
|
|
(worker `starting`.wall_ms − instance create_ms) is the image-pull +
|
|
|
|
|
|
container-boot + worker-spawn cost the node can't see itself."""
|
|
|
|
|
|
return int(time.time() * 1000)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 18:19:06 +00:00
|
|
|
|
def _rss_mb() -> "int | None":
|
|
|
|
|
|
"""Resident-set size in MB, read from /proc/self/status (Linux).
|
|
|
|
|
|
Returns None on non-Linux or when the read fails — the field is
|
|
|
|
|
|
informational, never required."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open("/proc/self/status", "r") as f:
|
|
|
|
|
|
for line in f:
|
|
|
|
|
|
if line.startswith("VmRSS:"):
|
|
|
|
|
|
parts = line.split()
|
|
|
|
|
|
# VmRSS: 12345 kB
|
|
|
|
|
|
return int(parts[1]) // 1024
|
|
|
|
|
|
except (OSError, ValueError, IndexError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _install_excepthook() -> None:
|
|
|
|
|
|
"""Catch every uncaught exception and emit a structured event before
|
|
|
|
|
|
the interpreter prints the traceback to stderr (which the actor's
|
|
|
|
|
|
ring buffer will also capture)."""
|
|
|
|
|
|
|
|
|
|
|
|
def _hook(exc_type, exc_value, exc_tb):
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
tb_text = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"uncaught_exception",
|
|
|
|
|
|
type=exc_type.__name__,
|
|
|
|
|
|
value=str(exc_value),
|
|
|
|
|
|
traceback=tb_text,
|
|
|
|
|
|
uptime_ms=_uptime_ms(),
|
|
|
|
|
|
)
|
|
|
|
|
|
# Preserve the default behaviour so stderr still shows the trace
|
|
|
|
|
|
# (the actor's stderr ring buffer is a belt-and-braces backup).
|
|
|
|
|
|
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
|
|
|
|
|
|
|
|
|
|
sys.excepthook = _hook
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _install_signal_handlers() -> None:
|
|
|
|
|
|
"""Emit `signal_received` and exit cleanly on SIGTERM/SIGINT.
|
|
|
|
|
|
SIGKILL and SIGSEGV cannot be caught — the Rust side relies on the
|
|
|
|
|
|
exit code / signal field of the eventual `worker_exited` Custom
|
|
|
|
|
|
event for those."""
|
|
|
|
|
|
|
|
|
|
|
|
def _on_signal(signum, _frame):
|
|
|
|
|
|
try:
|
|
|
|
|
|
name = signal.Signals(signum).name
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
name = f"signal-{signum}"
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"signal_received",
|
|
|
|
|
|
signum=signum,
|
|
|
|
|
|
name=name,
|
|
|
|
|
|
uptime_ms=_uptime_ms(),
|
|
|
|
|
|
)
|
|
|
|
|
|
# 128 + signum is the conventional exit code for signal-driven
|
|
|
|
|
|
# termination; matches what /bin/sh reports.
|
|
|
|
|
|
sys.exit(128 + signum)
|
|
|
|
|
|
|
|
|
|
|
|
for s in (signal.SIGTERM, signal.SIGINT):
|
|
|
|
|
|
try:
|
|
|
|
|
|
signal.signal(s, _on_signal)
|
|
|
|
|
|
except (ValueError, OSError):
|
|
|
|
|
|
# Some environments (e.g. non-main thread) don't allow
|
|
|
|
|
|
# signal install — silently skip rather than crash here.
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _start_heartbeat(interval_s: float = 30.0) -> None:
|
|
|
|
|
|
"""Daemon thread emitting `heartbeat` events. Lets the post-processor
|
|
|
|
|
|
distinguish *hung* (heartbeats stop but process alive — no exit event)
|
|
|
|
|
|
from *dead* (no heartbeat AND no exit — likely SIGKILL/SIGSEGV)."""
|
|
|
|
|
|
|
|
|
|
|
|
def _loop():
|
|
|
|
|
|
while True:
|
|
|
|
|
|
time.sleep(interval_s)
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"heartbeat",
|
|
|
|
|
|
uptime_ms=_uptime_ms(),
|
|
|
|
|
|
rss_mb=_rss_mb(),
|
|
|
|
|
|
requests_served=_REQUESTS_SERVED,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
t = threading.Thread(target=_loop, name="pp-worker-heartbeat", daemon=True)
|
|
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
def _is_nonneg_int(x) -> bool:
|
|
|
|
|
|
# ``bool`` is a subclass of ``int`` in Python; reject it explicitly so
|
|
|
|
|
|
# ``{"position": true}`` doesn't sneak through.
|
|
|
|
|
|
return isinstance(x, int) and not isinstance(x, bool) and x >= 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _RealModelState:
|
|
|
|
|
|
"""Holds the loaded tinygrad model + tokenizer for real mode.
|
|
|
|
|
|
|
|
|
|
|
|
All tinygrad imports live inside the loader so stub-mode never pays
|
|
|
|
|
|
the import cost. Once loaded, the state is reused for every request.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, model_name: str, stage: int, num_stages: int):
|
2026-05-25 18:19:06 +00:00
|
|
|
|
# Import tinygrad lazily so stub-mode never touches it. This is
|
|
|
|
|
|
# the most likely crash site in real mode — emit lifecycle
|
|
|
|
|
|
# events around the import so the bundle records exactly when
|
|
|
|
|
|
# the worker started loading and how long it took.
|
2026-05-28 07:05:41 +00:00
|
|
|
|
_emit_event("importing_tinygrad", stage=stage, wall_ms=_wall_ms())
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_import_start = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
import numpy as np
|
2026-05-28 07:05:41 +00:00
|
|
|
|
from tinygrad import Tensor, Device
|
|
|
|
|
|
from tinygrad.helpers import fetch, getenv
|
|
|
|
|
|
from tinygrad.apps.llm import SimpleTokenizer, models
|
|
|
|
|
|
|
|
|
|
|
|
import_ms = int((time.monotonic() - _import_start) * 1000)
|
|
|
|
|
|
_emit_event("tinygrad_imported", stage=stage, elapsed_ms=import_ms)
|
|
|
|
|
|
# Device + precision context: which backend the shard lands on and the
|
|
|
|
|
|
# toggles (HALF/JIT/BEAM) that dominate load + inference cost. Correlate
|
|
|
|
|
|
# with model_load_breakdown / op events to attribute time to dequant vs
|
|
|
|
|
|
# kernel compile vs steady-state matmul.
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_emit_event(
|
2026-05-28 07:05:41 +00:00
|
|
|
|
"device",
|
2026-05-25 18:19:06 +00:00
|
|
|
|
stage=stage,
|
2026-05-28 07:05:41 +00:00
|
|
|
|
default_device=str(Device.DEFAULT),
|
|
|
|
|
|
half=getenv("HALF", 1),
|
|
|
|
|
|
jit=getenv("JIT", 1),
|
|
|
|
|
|
beam=getenv("BEAM", 0),
|
|
|
|
|
|
cuda_visible=os.environ.get("CUDA_VISIBLE_DEVICES"),
|
2026-05-25 18:19:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
if model_name not in models:
|
|
|
|
|
|
available = ", ".join(sorted(models.keys()))
|
|
|
|
|
|
_die(f"unknown MODEL {model_name!r}; available: {available}")
|
|
|
|
|
|
|
|
|
|
|
|
url = models[model_name]
|
2026-05-28 07:05:41 +00:00
|
|
|
|
_emit_event("fetching_model", stage=stage, model=model_name, url=url, wall_ms=_wall_ms())
|
2026-05-16 05:49:43 +00:00
|
|
|
|
print(
|
|
|
|
|
|
f"pp_tinygrad_worker: stage={stage}/{num_stages} fetching {model_name}",
|
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
|
flush=True,
|
|
|
|
|
|
)
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_fetch_start = time.monotonic()
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# Spec §4.1: download only this stage's tensor byte ranges.
|
|
|
|
|
|
# Spec §4.2: cache idempotently with an integrity check; a
|
|
|
|
|
|
# complete-and-valid cache MUST NOT trigger a network fetch.
|
|
|
|
|
|
# Spec §4.7: `pp_download_progress` events are emitted by
|
|
|
|
|
|
# `_pp_download_sharded` while the fetch is in progress and
|
|
|
|
|
|
# NEVER on a cache hit. The unused `fetch` import remains as
|
|
|
|
|
|
# documentation of the prior code path; the sharded fetcher
|
|
|
|
|
|
# replaces it.
|
|
|
|
|
|
_ = fetch # silence the linter; kept for the diff reader
|
|
|
|
|
|
gguf_path = _pp_download_sharded(url, stage, num_stages)
|
|
|
|
|
|
fetch_ms = int((time.monotonic() - _fetch_start) * 1000)
|
|
|
|
|
|
# `gguf_bytes` is the file's apparent size (matches the source's
|
|
|
|
|
|
# total_size); actual on-disk usage is O(per-stage shard). Bundle
|
|
|
|
|
|
# readers reading `model_fetched.gguf_bytes` see the same value
|
|
|
|
|
|
# they did before the §4.1 change — the per-stage usage shows up
|
|
|
|
|
|
# in `pp_download_progress.bytes_total` (header + kept ranges).
|
|
|
|
|
|
try:
|
|
|
|
|
|
gguf_bytes = os.path.getsize(gguf_path)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
gguf_bytes = 0
|
|
|
|
|
|
fetch_mb = gguf_bytes / 1_000_000
|
|
|
|
|
|
# A near-instant return with a non-zero apparent size means the
|
|
|
|
|
|
# sharded cache hit short-circuited the fetch. Distinguishable
|
|
|
|
|
|
# in the bundle from a real download via the presence (or not)
|
|
|
|
|
|
# of `pp_download_progress` events.
|
|
|
|
|
|
cache_hit = gguf_bytes > 0 and fetch_ms < 2000
|
|
|
|
|
|
download_mb_per_s = None if cache_hit else round(fetch_mb / max(fetch_ms / 1000, 1e-3), 1)
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_emit_event(
|
|
|
|
|
|
"model_fetched",
|
|
|
|
|
|
stage=stage,
|
2026-05-28 07:05:41 +00:00
|
|
|
|
elapsed_ms=fetch_ms,
|
2026-05-25 18:19:06 +00:00
|
|
|
|
gguf_path=str(gguf_path),
|
2026-05-28 07:05:41 +00:00
|
|
|
|
gguf_bytes=gguf_bytes,
|
|
|
|
|
|
gguf_mb=round(fetch_mb, 1),
|
|
|
|
|
|
download_mb_per_s=download_mb_per_s,
|
|
|
|
|
|
cache_hit=cache_hit,
|
2026-05-25 18:19:06 +00:00
|
|
|
|
)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
print(
|
|
|
|
|
|
f"pp_tinygrad_worker: stage={stage} loading model from {gguf_path}",
|
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
|
flush=True,
|
|
|
|
|
|
)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
_emit_event("loading_model", stage=stage, model=model_name, wall_ms=_wall_ms())
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_load_start = time.monotonic()
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# Shard at load time: only this stage's block range (+ embed/output on the
|
|
|
|
|
|
# end stages) is copied to the compute device, so an 18 GB model fits on a
|
|
|
|
|
|
# 12 GB GPU. See _load_sharded_transformer for why stock from_gguf can't.
|
|
|
|
|
|
# The loader emits its own `model_load_breakdown` (header/dequant/realize).
|
|
|
|
|
|
model, kv, start, end = _load_sharded_transformer(
|
|
|
|
|
|
gguf_path, stage, num_stages, max_context=512
|
2026-05-25 18:19:06 +00:00
|
|
|
|
)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
load_ms = int((time.monotonic() - _load_start) * 1000)
|
|
|
|
|
|
tokenizer = SimpleTokenizer.from_gguf_kv(kv)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
arch = kv["general.architecture"]
|
|
|
|
|
|
hidden_dim = int(kv[f"{arch}.embedding_length"])
|
|
|
|
|
|
total_blocks = int(kv[f"{arch}.block_count"])
|
|
|
|
|
|
vocab_size = len(kv["tokenizer.ggml.tokens"])
|
|
|
|
|
|
|
2026-05-28 07:05:41 +00:00
|
|
|
|
_emit_event(
|
|
|
|
|
|
"model_loaded",
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
elapsed_ms=load_ms,
|
|
|
|
|
|
rss_mb=_rss_mb(),
|
|
|
|
|
|
blocks_resident=end - start,
|
|
|
|
|
|
total_blocks=total_blocks,
|
|
|
|
|
|
)
|
|
|
|
|
|
# Stash the cold-start breakdown so main() can emit one `boot_profile`
|
|
|
|
|
|
# summary once the worker is ready (import + fetch + load + total).
|
|
|
|
|
|
self.timing = {
|
|
|
|
|
|
"import_ms": import_ms,
|
|
|
|
|
|
"fetch_ms": fetch_ms,
|
|
|
|
|
|
"fetch_cache_hit": cache_hit,
|
|
|
|
|
|
"gguf_bytes": gguf_bytes,
|
|
|
|
|
|
"download_mb_per_s": download_mb_per_s,
|
|
|
|
|
|
"load_ms": load_ms,
|
|
|
|
|
|
}
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
# Pin EOS token ids (best-effort) for callers that want to detect
|
|
|
|
|
|
# end-of-text from the sampled stream. We don't enforce stop here
|
|
|
|
|
|
# — the Rust Stage-1 actor owns the EOS decision — but exposing
|
|
|
|
|
|
# them in the ready line lets the orchestrator configure itself.
|
|
|
|
|
|
tokens_list = kv.get("tokenizer.ggml.tokens", [])
|
|
|
|
|
|
eos_ids: list[int] = []
|
|
|
|
|
|
for i, tok in enumerate(tokens_list):
|
|
|
|
|
|
if tok in ("<|end_of_text|>", "<|eot_id|>", "</s>", "<|endoftext|>"):
|
|
|
|
|
|
eos_ids.append(i)
|
|
|
|
|
|
|
|
|
|
|
|
# Tinygrad and numpy are kept as instance attributes so the
|
|
|
|
|
|
# per-request handlers don't re-import them.
|
|
|
|
|
|
self._np = np
|
|
|
|
|
|
self._Tensor = Tensor
|
|
|
|
|
|
self.model = model
|
|
|
|
|
|
self.tokenizer = tokenizer
|
|
|
|
|
|
self.hidden_dim = hidden_dim
|
|
|
|
|
|
self.vocab_size = vocab_size
|
|
|
|
|
|
self.total_blocks = total_blocks
|
|
|
|
|
|
self.stage = stage
|
|
|
|
|
|
self.num_stages = num_stages
|
|
|
|
|
|
self.start = start
|
|
|
|
|
|
self.end = end
|
|
|
|
|
|
self.eos_ids = eos_ids
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# Per-op compute breakdown (deserialize / compute / host-copy ms), set by
|
|
|
|
|
|
# the forward ops and folded into the serve loop's `op` timing event.
|
|
|
|
|
|
self._last_compute: "dict | None" = None
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"pp_tinygrad_worker: stage={stage} ready "
|
|
|
|
|
|
f"(blocks={total_blocks}, range=[{start},{end}), "
|
|
|
|
|
|
f"hidden_dim={hidden_dim}, vocab={vocab_size}, eos={eos_ids})",
|
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
|
flush=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# --- per-stage forward ops -------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def embed_and_forward(self, tokens: Sequence[int], position: int) -> tuple[bytes, int]:
|
|
|
|
|
|
Tensor = self._Tensor
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t0 = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
t = Tensor([list(tokens)], dtype="int32")
|
|
|
|
|
|
x = self.model.token_embd(t)
|
|
|
|
|
|
for block in self.model.blk[self.start : self.end]:
|
|
|
|
|
|
x = block(x, position)
|
|
|
|
|
|
# Cast to half (2 bytes/elem) for the wire format, matching the
|
|
|
|
|
|
# stub. The model's weights are float16; the op output may have
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# promoted to float32, so an explicit cast normalises this. tinygrad is
|
|
|
|
|
|
# lazy: embed + blocks + cast all *execute* at .realize() below (incl.
|
|
|
|
|
|
# JIT kernel compile on the first call), so compute_ms captures them.
|
|
|
|
|
|
t1 = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
x = x.cast("half").realize()
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t2 = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
arr = x.numpy() # shape (1, seq_len, hidden_dim), dtype float16
|
2026-05-28 07:05:41 +00:00
|
|
|
|
out = arr.tobytes()
|
|
|
|
|
|
t3 = time.monotonic()
|
|
|
|
|
|
self._last_compute = {
|
|
|
|
|
|
"build_ms": round((t1 - t0) * 1000, 2),
|
|
|
|
|
|
"compute_ms": round((t2 - t1) * 1000, 2),
|
|
|
|
|
|
"host_copy_ms": round((t3 - t2) * 1000, 2),
|
|
|
|
|
|
"out_bytes": len(out),
|
|
|
|
|
|
}
|
|
|
|
|
|
return out, int(arr.shape[1])
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
def forward_range(
|
|
|
|
|
|
self, hidden_bytes: bytes, position: int, seq_len: int
|
|
|
|
|
|
) -> tuple[bytes, int]:
|
|
|
|
|
|
"""Run this middle stage's block range over an incoming hidden state.
|
|
|
|
|
|
|
|
|
|
|
|
Input is a flat float16 buffer of shape ``(1, seq_len, hidden_dim)``;
|
|
|
|
|
|
output is the same shape after applying ``model.blk[start:end]``
|
|
|
|
|
|
with KV-cache ``position``. Returns ``(bytes, out_seq_len)``.
|
|
|
|
|
|
"""
|
|
|
|
|
|
np = self._np
|
|
|
|
|
|
Tensor = self._Tensor
|
|
|
|
|
|
expected = seq_len * self.hidden_dim * BYTES_PER_ELEM
|
|
|
|
|
|
if len(hidden_bytes) != expected:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"hidden length {len(hidden_bytes)} does not match "
|
|
|
|
|
|
f"seq_len*hidden_dim*2 ({seq_len}*{self.hidden_dim}*{BYTES_PER_ELEM} "
|
|
|
|
|
|
f"= {expected})"
|
|
|
|
|
|
)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t0 = time.monotonic()
|
2026-05-20 07:41:30 +00:00
|
|
|
|
arr = (
|
|
|
|
|
|
np.frombuffer(hidden_bytes, dtype=np.float16)
|
|
|
|
|
|
.reshape((1, seq_len, self.hidden_dim))
|
|
|
|
|
|
.copy()
|
|
|
|
|
|
)
|
|
|
|
|
|
x = Tensor(arr)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t1 = time.monotonic()
|
2026-05-20 07:41:30 +00:00
|
|
|
|
for block in self.model.blk[self.start : self.end]:
|
|
|
|
|
|
x = block(x, position)
|
|
|
|
|
|
x = x.cast("half").realize()
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t2 = time.monotonic()
|
|
|
|
|
|
out_arr = x.numpy()
|
|
|
|
|
|
out = out_arr.tobytes()
|
|
|
|
|
|
t3 = time.monotonic()
|
|
|
|
|
|
self._last_compute = {
|
|
|
|
|
|
"deserialize_ms": round((t1 - t0) * 1000, 2),
|
|
|
|
|
|
"compute_ms": round((t2 - t1) * 1000, 2),
|
|
|
|
|
|
"host_copy_ms": round((t3 - t2) * 1000, 2),
|
|
|
|
|
|
"in_bytes": len(hidden_bytes),
|
|
|
|
|
|
"out_bytes": len(out),
|
|
|
|
|
|
}
|
|
|
|
|
|
return out, int(out_arr.shape[1])
|
2026-05-20 07:41:30 +00:00
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
def forward_and_sample(
|
|
|
|
|
|
self, hidden_bytes: bytes, position: int, seq_len: int
|
|
|
|
|
|
) -> int:
|
|
|
|
|
|
np = self._np
|
|
|
|
|
|
Tensor = self._Tensor
|
|
|
|
|
|
expected = seq_len * self.hidden_dim * BYTES_PER_ELEM
|
|
|
|
|
|
if len(hidden_bytes) != expected:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"hidden length {len(hidden_bytes)} does not match "
|
|
|
|
|
|
f"seq_len*hidden_dim*2 ({seq_len}*{self.hidden_dim}*{BYTES_PER_ELEM} "
|
|
|
|
|
|
f"= {expected})"
|
|
|
|
|
|
)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t0 = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
arr = (
|
|
|
|
|
|
np.frombuffer(hidden_bytes, dtype=np.float16)
|
|
|
|
|
|
.reshape((1, seq_len, self.hidden_dim))
|
|
|
|
|
|
.copy()
|
|
|
|
|
|
)
|
|
|
|
|
|
x = Tensor(arr)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t1 = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
for block in self.model.blk[self.start : self.end]:
|
|
|
|
|
|
x = block(x, position)
|
|
|
|
|
|
x = self.model.output_norm(x)
|
|
|
|
|
|
logits = self.model.output(x)
|
|
|
|
|
|
# Argmax on the last position's logits. Matches what
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# ``Transformer.forward`` does at llm.py:178. The .item() forces the
|
|
|
|
|
|
# blocks + output projection (over the full vocab) to execute here.
|
2026-05-16 05:49:43 +00:00
|
|
|
|
token_id = int(logits[0, -1, :].argmax().item())
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t2 = time.monotonic()
|
|
|
|
|
|
self._last_compute = {
|
|
|
|
|
|
"deserialize_ms": round((t1 - t0) * 1000, 2),
|
|
|
|
|
|
"compute_ms": round((t2 - t1) * 1000, 2),
|
|
|
|
|
|
"in_bytes": len(hidden_bytes),
|
|
|
|
|
|
"token_id": token_id,
|
|
|
|
|
|
}
|
2026-05-16 05:49:43 +00:00
|
|
|
|
return token_id
|
|
|
|
|
|
|
|
|
|
|
|
def generate_full(self, prompt: str, max_tokens: int) -> list[int]:
|
|
|
|
|
|
"""End-to-end inference over the full block range.
|
|
|
|
|
|
|
|
|
|
|
|
Used as the reference for the sliced-vs-full equivalence tests.
|
|
|
|
|
|
Runs the same unjitted block iteration that the per-stage ops use
|
|
|
|
|
|
(``embed_and_forward`` + ``forward_and_sample``), but on all
|
|
|
|
|
|
``model.blk`` blocks within a single process — so the result is
|
|
|
|
|
|
bit-identical to a correctly-sliced pipeline with argmax sampling.
|
|
|
|
|
|
|
|
|
|
|
|
Returns the ``max_tokens`` sampled token ids (excluding the
|
|
|
|
|
|
prompt). Each invocation starts a fresh prefill at ``position=0``;
|
|
|
|
|
|
the lazily-allocated per-block KV cache is overwritten in place
|
|
|
|
|
|
as positions are revisited, so the same worker can serve multiple
|
|
|
|
|
|
independent prompts.
|
|
|
|
|
|
"""
|
2026-05-28 07:05:41 +00:00
|
|
|
|
if self.start != 0 or self.end != self.total_blocks:
|
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
|
"generate_full needs the whole model resident, but this stage only "
|
|
|
|
|
|
f"holds blk[{self.start}:{self.end}) of {self.total_blocks}. Use the "
|
|
|
|
|
|
"per-stage ops (embed_and_forward / forward_range / forward_and_sample)."
|
|
|
|
|
|
)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
Tensor = self._Tensor
|
|
|
|
|
|
if max_tokens <= 0:
|
|
|
|
|
|
return []
|
|
|
|
|
|
prompt_tokens = self.tokenizer.encode(prompt)
|
|
|
|
|
|
if len(prompt_tokens) == 0:
|
|
|
|
|
|
raise ValueError("prompt tokenized to an empty list")
|
|
|
|
|
|
|
|
|
|
|
|
def _forward_sample(token_ids: Sequence[int], position: int) -> int:
|
|
|
|
|
|
t = Tensor([list(token_ids)], dtype="int32")
|
|
|
|
|
|
x = self.model.token_embd(t)
|
|
|
|
|
|
for block in self.model.blk[0 : self.total_blocks]:
|
|
|
|
|
|
x = block(x, position)
|
|
|
|
|
|
x = self.model.output_norm(x)
|
|
|
|
|
|
logits = self.model.output(x)
|
|
|
|
|
|
return int(logits[0, -1, :].argmax().item())
|
|
|
|
|
|
|
|
|
|
|
|
out: list[int] = []
|
|
|
|
|
|
# Prefill: full prompt at position 0; sample at the last position.
|
|
|
|
|
|
next_id = _forward_sample(prompt_tokens, 0)
|
|
|
|
|
|
out.append(next_id)
|
|
|
|
|
|
# Autoregressive decode: feed the newly-sampled token at the
|
|
|
|
|
|
# next position. This mirrors what ``Stage0Actor`` does over the
|
|
|
|
|
|
# wire when ``NextToken { position, token_id }`` arrives.
|
|
|
|
|
|
pos = len(prompt_tokens)
|
|
|
|
|
|
for _ in range(max_tokens - 1):
|
|
|
|
|
|
next_id = _forward_sample([next_id], pos)
|
|
|
|
|
|
out.append(next_id)
|
|
|
|
|
|
pos += 1
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _validate_tokens_list(tokens) -> str | None:
|
|
|
|
|
|
"""Return None if ``tokens`` is a list of non-bool ints, else an error msg."""
|
|
|
|
|
|
if not isinstance(tokens, list):
|
|
|
|
|
|
return "'tokens' must be a list of ints"
|
|
|
|
|
|
for t in tokens:
|
|
|
|
|
|
if not isinstance(t, int) or isinstance(t, bool) or t < 0:
|
|
|
|
|
|
return "'tokens' must be a list of non-negative ints"
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_request(
|
|
|
|
|
|
req: dict,
|
|
|
|
|
|
stage: int,
|
|
|
|
|
|
num_stages: int,
|
|
|
|
|
|
real_state: "_RealModelState | None" = None,
|
|
|
|
|
|
) -> dict:
|
|
|
|
|
|
rid = req.get("request_id")
|
|
|
|
|
|
if "op" not in req:
|
|
|
|
|
|
return {"request_id": rid, "error": "missing 'op' field"}
|
|
|
|
|
|
op = req["op"]
|
|
|
|
|
|
is_first = stage == 0
|
|
|
|
|
|
is_last = stage == num_stages - 1
|
2026-05-20 07:41:30 +00:00
|
|
|
|
is_middle = not is_first and not is_last
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
# Tokenize lives on the first stage; detokenize on the last. Routing
|
|
|
|
|
|
# both through their natural roles avoids ambiguity when an N-stage
|
|
|
|
|
|
# cluster has a tokenizer-bearing worker on every node (real mode).
|
2026-05-16 05:49:43 +00:00
|
|
|
|
if op == "tokenize":
|
2026-05-20 07:41:30 +00:00
|
|
|
|
if not is_first:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": f"op {op!r} is only valid on stage 0 "
|
|
|
|
|
|
f"(this worker is stage {stage} of {num_stages})",
|
|
|
|
|
|
}
|
2026-05-16 05:49:43 +00:00
|
|
|
|
prompt = req.get("prompt")
|
|
|
|
|
|
if not isinstance(prompt, str):
|
|
|
|
|
|
return {"request_id": rid, "error": "'prompt' must be a string"}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
tokens = real_state.tokenizer.encode(prompt)
|
|
|
|
|
|
else:
|
|
|
|
|
|
tokens = [
|
|
|
|
|
|
int(t) for t in _stub_tokenize(prompt)
|
|
|
|
|
|
]
|
|
|
|
|
|
return {"request_id": rid, "tokens": tokens}
|
|
|
|
|
|
|
|
|
|
|
|
if op == "detokenize":
|
2026-05-20 07:41:30 +00:00
|
|
|
|
if not is_last:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": f"op {op!r} is only valid on the final stage "
|
|
|
|
|
|
f"(this worker is stage {stage} of {num_stages})",
|
|
|
|
|
|
}
|
2026-05-16 05:49:43 +00:00
|
|
|
|
tokens = req.get("tokens")
|
|
|
|
|
|
err = _validate_tokens_list(tokens)
|
|
|
|
|
|
if err is not None:
|
|
|
|
|
|
return {"request_id": rid, "error": err}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
text = real_state.tokenizer.decode(tokens)
|
|
|
|
|
|
else:
|
|
|
|
|
|
text = " ".join(str(t) for t in tokens)
|
|
|
|
|
|
return {"request_id": rid, "text": text}
|
|
|
|
|
|
|
|
|
|
|
|
if op in ("embed_and_forward", "decode_step"):
|
|
|
|
|
|
if not is_first:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": f"op {op!r} is only valid on stage 0 "
|
|
|
|
|
|
f"(this worker is stage {stage} of {num_stages})",
|
|
|
|
|
|
}
|
|
|
|
|
|
if op == "embed_and_forward":
|
|
|
|
|
|
tokens = req.get("tokens")
|
|
|
|
|
|
position = req.get("position", 0)
|
|
|
|
|
|
err = _validate_tokens_list(tokens)
|
|
|
|
|
|
if err is not None:
|
|
|
|
|
|
return {"request_id": rid, "error": err}
|
|
|
|
|
|
if len(tokens) == 0:
|
|
|
|
|
|
return {"request_id": rid, "error": "'tokens' must be non-empty"}
|
|
|
|
|
|
if not _is_nonneg_int(position):
|
|
|
|
|
|
return {"request_id": rid, "error": "'position' must be a non-negative int"}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
hidden, out_seq_len = real_state.embed_and_forward(tokens, position)
|
|
|
|
|
|
else:
|
|
|
|
|
|
hidden = _stub_hidden_bytes(tokens, position)
|
|
|
|
|
|
out_seq_len = len(tokens)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"hidden_b64": base64.b64encode(hidden).decode("ascii"),
|
|
|
|
|
|
"seq_len": out_seq_len,
|
|
|
|
|
|
}
|
|
|
|
|
|
# decode_step
|
|
|
|
|
|
token_id = req.get("token_id")
|
|
|
|
|
|
position = req.get("position")
|
|
|
|
|
|
if not _is_nonneg_int(token_id):
|
|
|
|
|
|
return {"request_id": rid, "error": "'token_id' must be a non-negative int"}
|
|
|
|
|
|
if not _is_nonneg_int(position):
|
|
|
|
|
|
return {"request_id": rid, "error": "'position' must be a non-negative int"}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
hidden, out_seq_len = real_state.embed_and_forward([token_id], position)
|
|
|
|
|
|
else:
|
|
|
|
|
|
hidden = _stub_hidden_bytes([token_id], position)
|
|
|
|
|
|
out_seq_len = 1
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"hidden_b64": base64.b64encode(hidden).decode("ascii"),
|
|
|
|
|
|
"seq_len": out_seq_len,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
if op == "forward_range":
|
|
|
|
|
|
if not is_middle:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": f"op {op!r} is only valid on a middle stage "
|
|
|
|
|
|
f"(this worker is stage {stage} of {num_stages})",
|
|
|
|
|
|
}
|
|
|
|
|
|
hidden_b64 = req.get("hidden_b64")
|
|
|
|
|
|
position = req.get("position")
|
|
|
|
|
|
seq_len = req.get("seq_len")
|
|
|
|
|
|
if not isinstance(hidden_b64, str):
|
|
|
|
|
|
return {"request_id": rid, "error": "'hidden_b64' must be a string"}
|
|
|
|
|
|
if not _is_nonneg_int(position):
|
|
|
|
|
|
return {"request_id": rid, "error": "'position' must be a non-negative int"}
|
|
|
|
|
|
if not _is_nonneg_int(seq_len) or seq_len == 0:
|
|
|
|
|
|
return {"request_id": rid, "error": "'seq_len' must be a positive int"}
|
|
|
|
|
|
try:
|
|
|
|
|
|
hidden = base64.b64decode(hidden_b64, validate=True)
|
|
|
|
|
|
except (base64.binascii.Error, ValueError) as e:
|
|
|
|
|
|
return {"request_id": rid, "error": f"invalid base64 in hidden_b64: {e}"}
|
|
|
|
|
|
hidden_dim = real_state.hidden_dim if real_state is not None else STUB_HIDDEN_DIM
|
|
|
|
|
|
expected = seq_len * hidden_dim * BYTES_PER_ELEM
|
|
|
|
|
|
if len(hidden) != expected:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": (
|
|
|
|
|
|
f"hidden length {len(hidden)} does not match "
|
|
|
|
|
|
f"seq_len*hidden_dim*2 ({seq_len}*{hidden_dim}*{BYTES_PER_ELEM} "
|
|
|
|
|
|
f"= {expected})"
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
hidden_out, out_seq_len = real_state.forward_range(hidden, position, seq_len)
|
|
|
|
|
|
else:
|
|
|
|
|
|
hidden_out = _stub_forward_range_bytes(hidden, position, seq_len)
|
|
|
|
|
|
out_seq_len = seq_len
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"hidden_b64": base64.b64encode(hidden_out).decode("ascii"),
|
|
|
|
|
|
"seq_len": out_seq_len,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
if op == "generate_full":
|
|
|
|
|
|
# Reference path used by the sliced-vs-full equivalence tests. Always
|
|
|
|
|
|
# runs over the full block range, so the worker's STAGE/NUM_STAGES
|
2026-05-20 07:41:30 +00:00
|
|
|
|
# are ignored here — any ``NUM_STAGES >= 2`` works since the op
|
|
|
|
|
|
# iterates ``model.blk`` directly. The example does not boot at
|
|
|
|
|
|
# ``NUM_STAGES=1`` (single-node configurations belong to
|
|
|
|
|
|
# ``examples/single-gpu-inference``).
|
2026-05-16 05:49:43 +00:00
|
|
|
|
if real_state is None:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": "op 'generate_full' requires real mode (stub mode has no full model)",
|
|
|
|
|
|
}
|
|
|
|
|
|
prompt = req.get("prompt")
|
|
|
|
|
|
max_tokens = req.get("max_tokens")
|
|
|
|
|
|
if not isinstance(prompt, str):
|
|
|
|
|
|
return {"request_id": rid, "error": "'prompt' must be a string"}
|
|
|
|
|
|
if not _is_nonneg_int(max_tokens) or max_tokens == 0:
|
|
|
|
|
|
return {"request_id": rid, "error": "'max_tokens' must be a positive int"}
|
|
|
|
|
|
try:
|
|
|
|
|
|
tokens = real_state.generate_full(prompt, max_tokens)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {"request_id": rid, "error": f"generate_full: {e}"}
|
|
|
|
|
|
return {"request_id": rid, "tokens": tokens}
|
|
|
|
|
|
|
|
|
|
|
|
if op == "forward_and_sample":
|
|
|
|
|
|
if not is_last:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": f"op {op!r} is only valid on the final stage "
|
|
|
|
|
|
f"(this worker is stage {stage} of {num_stages})",
|
|
|
|
|
|
}
|
|
|
|
|
|
hidden_b64 = req.get("hidden_b64")
|
|
|
|
|
|
position = req.get("position")
|
|
|
|
|
|
seq_len = req.get("seq_len")
|
|
|
|
|
|
if not isinstance(hidden_b64, str):
|
|
|
|
|
|
return {"request_id": rid, "error": "'hidden_b64' must be a string"}
|
|
|
|
|
|
if not _is_nonneg_int(position):
|
|
|
|
|
|
return {"request_id": rid, "error": "'position' must be a non-negative int"}
|
|
|
|
|
|
if not _is_nonneg_int(seq_len) or seq_len == 0:
|
|
|
|
|
|
return {"request_id": rid, "error": "'seq_len' must be a positive int"}
|
|
|
|
|
|
try:
|
|
|
|
|
|
hidden = base64.b64decode(hidden_b64, validate=True)
|
|
|
|
|
|
except (base64.binascii.Error, ValueError) as e:
|
|
|
|
|
|
return {"request_id": rid, "error": f"invalid base64 in hidden_b64: {e}"}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
hidden_dim = real_state.hidden_dim
|
|
|
|
|
|
else:
|
|
|
|
|
|
hidden_dim = STUB_HIDDEN_DIM
|
|
|
|
|
|
expected = seq_len * hidden_dim * BYTES_PER_ELEM
|
|
|
|
|
|
if len(hidden) != expected:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"request_id": rid,
|
|
|
|
|
|
"error": (
|
|
|
|
|
|
f"hidden length {len(hidden)} does not match "
|
|
|
|
|
|
f"seq_len*hidden_dim*2 ({seq_len}*{hidden_dim}*{BYTES_PER_ELEM} "
|
|
|
|
|
|
f"= {expected})"
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
token_id = real_state.forward_and_sample(hidden, position, seq_len)
|
|
|
|
|
|
else:
|
|
|
|
|
|
token_id = _stub_token_id(hidden, position)
|
|
|
|
|
|
return {"request_id": rid, "token_id": token_id}
|
|
|
|
|
|
|
|
|
|
|
|
return {"request_id": rid, "error": f"unknown op {op!r}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stub_tokenize(prompt: str) -> list[int]:
|
|
|
|
|
|
"""Whitespace-split a prompt into deterministic small integer ids.
|
|
|
|
|
|
|
|
|
|
|
|
Only used in stub mode. The exact mapping is not part of the
|
|
|
|
|
|
worker's contract — callers just need a list of ints whose length
|
|
|
|
|
|
equals the number of whitespace-separated words.
|
|
|
|
|
|
"""
|
|
|
|
|
|
out: list[int] = []
|
|
|
|
|
|
for i, word in enumerate(prompt.split()):
|
|
|
|
|
|
s = sum(ord(c) for c in word)
|
|
|
|
|
|
out.append((s % 1024) + i)
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
2026-05-25 18:19:06 +00:00
|
|
|
|
# Install diagnostic hooks first thing so any failure during arg
|
|
|
|
|
|
# parsing or env validation still produces a structured event.
|
|
|
|
|
|
_install_excepthook()
|
|
|
|
|
|
_install_signal_handlers()
|
|
|
|
|
|
|
2026-05-16 05:49:43 +00:00
|
|
|
|
parser = argparse.ArgumentParser(description="pipeline-parallel tinygrad worker")
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--stub",
|
|
|
|
|
|
action="store_true",
|
|
|
|
|
|
help="Stub mode: skip model loading, use deterministic in-memory ops",
|
|
|
|
|
|
)
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
"--model",
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
help="Model name (defaults to $MODEL or llama3.2:1b in real mode)",
|
|
|
|
|
|
)
|
|
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
|
|
|
|
|
|
|
|
stub_mode = args.stub or os.environ.get("PP_WORKER_STUB", "").strip() == "1"
|
|
|
|
|
|
|
|
|
|
|
|
stage = _parse_env_int("STAGE")
|
|
|
|
|
|
num_stages = _parse_env_int("NUM_STAGES")
|
2026-05-25 18:19:06 +00:00
|
|
|
|
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"starting",
|
|
|
|
|
|
pid=os.getpid(),
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
num_stages=num_stages,
|
|
|
|
|
|
stub=stub_mode,
|
|
|
|
|
|
model=(args.model or os.environ.get("MODEL", "")).strip() or None,
|
|
|
|
|
|
python_version=sys.version.split()[0],
|
|
|
|
|
|
argv=list(sys.argv),
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# wall_ms anchors this worker's boot against the orchestrator's vast.ai
|
|
|
|
|
|
# instance create/lease time — the only way to measure image-pull +
|
|
|
|
|
|
# container-boot latency, which the worker can't observe directly.
|
|
|
|
|
|
wall_ms=_wall_ms(),
|
|
|
|
|
|
host=os.uname().nodename,
|
2026-05-25 18:19:06 +00:00
|
|
|
|
)
|
|
|
|
|
|
_start_heartbeat()
|
|
|
|
|
|
|
2026-05-20 07:41:30 +00:00
|
|
|
|
if num_stages < 2:
|
|
|
|
|
|
_die(
|
|
|
|
|
|
f"NUM_STAGES must be >= 2 (single-node configurations are not "
|
|
|
|
|
|
f"served by this example), got {num_stages}"
|
|
|
|
|
|
)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
if not (0 <= stage < num_stages):
|
|
|
|
|
|
_die(f"STAGE {stage} out of range [0, {num_stages})")
|
|
|
|
|
|
|
|
|
|
|
|
real_state: _RealModelState | None = None
|
|
|
|
|
|
if not stub_mode:
|
|
|
|
|
|
model_name = (args.model or os.environ.get("MODEL", "")).strip() or "llama3.2:1b"
|
|
|
|
|
|
try:
|
|
|
|
|
|
real_state = _RealModelState(model_name, stage, num_stages)
|
|
|
|
|
|
except SystemExit:
|
|
|
|
|
|
raise
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
2026-05-25 18:19:06 +00:00
|
|
|
|
tb_text = traceback.format_exc()
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"model_load_failed",
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
model=model_name,
|
|
|
|
|
|
type=type(e).__name__,
|
|
|
|
|
|
value=str(e),
|
|
|
|
|
|
traceback=tb_text,
|
|
|
|
|
|
)
|
|
|
|
|
|
print(tb_text, file=sys.stderr, flush=True)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
_die(f"failed to load model {model_name!r}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
ready: dict = {"status": "ready", "pid": os.getpid(), "stage": stage}
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
ready["hidden_dim"] = real_state.hidden_dim
|
|
|
|
|
|
ready["vocab_size"] = real_state.vocab_size
|
|
|
|
|
|
ready["total_blocks"] = real_state.total_blocks
|
|
|
|
|
|
ready["layer_range"] = [real_state.start, real_state.end]
|
|
|
|
|
|
ready["eos_token_ids"] = real_state.eos_ids
|
|
|
|
|
|
_write(ready)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# Mirror ready as a structured event under the spec's event name
|
|
|
|
|
|
# (§4.6 / §6.9: per-stage filter on `pp_worker_ready`). The
|
|
|
|
|
|
# `pp_*` event kind is passed through unchanged by the Rust actor
|
|
|
|
|
|
# so the bundle records `Custom("pp_worker_ready")`, matching the
|
|
|
|
|
|
# name the orchestrator-side wired check filters on. The protocol
|
|
|
|
|
|
# `status: "ready"` line above is unchanged for the actor's ready
|
|
|
|
|
|
# signal.
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_emit_event(
|
2026-05-28 07:05:41 +00:00
|
|
|
|
"pp_worker_ready",
|
2026-05-25 18:19:06 +00:00
|
|
|
|
pid=os.getpid(),
|
2026-05-28 07:05:41 +00:00
|
|
|
|
stage_index=stage,
|
2026-05-25 18:19:06 +00:00
|
|
|
|
uptime_ms=_uptime_ms(),
|
|
|
|
|
|
rss_mb=_rss_mb(),
|
|
|
|
|
|
)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
# One-stop cold-start breakdown so a single event answers "where did
|
|
|
|
|
|
# bring-up time go" per node: import + (fetch|cache) + load == time-to-ready.
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"boot_profile",
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
total_to_ready_ms=_uptime_ms(),
|
|
|
|
|
|
rss_mb=_rss_mb(),
|
|
|
|
|
|
blocks_resident=real_state.end - real_state.start,
|
|
|
|
|
|
**real_state.timing,
|
|
|
|
|
|
)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-25 18:19:06 +00:00
|
|
|
|
global _REQUESTS_SERVED
|
2026-05-28 07:05:41 +00:00
|
|
|
|
seen_ops: set = set()
|
|
|
|
|
|
last_op_end = time.monotonic()
|
2026-05-16 05:49:43 +00:00
|
|
|
|
for line in sys.stdin:
|
|
|
|
|
|
line = line.strip()
|
|
|
|
|
|
if not line:
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
req = json.loads(line)
|
|
|
|
|
|
except (json.JSONDecodeError, ValueError) as e:
|
|
|
|
|
|
_write({"error": f"invalid JSON: {e}"})
|
|
|
|
|
|
continue
|
|
|
|
|
|
if not isinstance(req, dict):
|
|
|
|
|
|
_write({"error": f"request must be a JSON object, got {type(req).__name__}"})
|
|
|
|
|
|
continue
|
2026-05-28 07:05:41 +00:00
|
|
|
|
op = req.get("op")
|
|
|
|
|
|
if real_state is not None:
|
|
|
|
|
|
real_state._last_compute = None
|
|
|
|
|
|
t_start = time.monotonic()
|
|
|
|
|
|
# idle_ms_before is the pipeline bubble: how long this worker sat
|
|
|
|
|
|
# blocked on its upstream stage between finishing the last op and
|
|
|
|
|
|
# receiving this one. High idle => the bottleneck is elsewhere.
|
|
|
|
|
|
idle_ms = round((t_start - last_op_end) * 1000, 2)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
try:
|
|
|
|
|
|
reply = _handle_request(req, stage, num_stages, real_state=real_state)
|
|
|
|
|
|
except Exception as e: # last-ditch safety net so the worker stays up
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
print(traceback.format_exc(), file=sys.stderr, flush=True)
|
|
|
|
|
|
reply = {"request_id": req.get("request_id"), "error": f"internal: {e}"}
|
|
|
|
|
|
_write(reply)
|
2026-05-28 07:05:41 +00:00
|
|
|
|
t_end = time.monotonic()
|
|
|
|
|
|
# Per-op trace — the granular signal for end-to-end latency. `first_call`
|
|
|
|
|
|
# flags the JIT-compile-bearing first invocation of each op (kernels are
|
|
|
|
|
|
# compiled once, then cached). `rid` (not `request_id`) keeps the Rust
|
|
|
|
|
|
# actor from ever mistaking this event line for an op reply.
|
|
|
|
|
|
is_first = op not in seen_ops
|
|
|
|
|
|
seen_ops.add(op)
|
|
|
|
|
|
_emit_event(
|
|
|
|
|
|
"op",
|
|
|
|
|
|
op=op,
|
|
|
|
|
|
rid=req.get("request_id"),
|
|
|
|
|
|
stage=stage,
|
|
|
|
|
|
duration_ms=round((t_end - t_start) * 1000, 2),
|
|
|
|
|
|
idle_ms_before=idle_ms,
|
|
|
|
|
|
first_call=is_first,
|
|
|
|
|
|
ok=isinstance(reply, dict) and "error" not in reply,
|
|
|
|
|
|
in_tokens=len(req["tokens"]) if isinstance(req.get("tokens"), list) else None,
|
|
|
|
|
|
in_seq_len=req.get("seq_len"),
|
|
|
|
|
|
out_seq_len=reply.get("seq_len") if isinstance(reply, dict) else None,
|
|
|
|
|
|
compute=(real_state._last_compute if real_state is not None else None),
|
|
|
|
|
|
uptime_ms=_uptime_ms(),
|
|
|
|
|
|
)
|
|
|
|
|
|
last_op_end = t_end
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_REQUESTS_SERVED += 1
|
2026-05-16 05:49:43 +00:00
|
|
|
|
|
2026-05-25 18:19:06 +00:00
|
|
|
|
_emit_event(
|
|
|
|
|
|
"exiting",
|
|
|
|
|
|
reason="eof",
|
|
|
|
|
|
uptime_ms=_uptime_ms(),
|
|
|
|
|
|
requests_served=_REQUESTS_SERVED,
|
|
|
|
|
|
)
|
2026-05-16 05:49:43 +00:00
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
sys.exit(main())
|