physicsrob's picture
Publish E1M1 38-layer seed-0 bundle
ed03dd6 verified
Raw
History Blame Contribute Delete
12.4 kB
"""The sole Doom inference program: portable stock-Hugging-Face generation.
This file is copied byte-identical to the root of every published Doom
bundle (``<bundle>/infer.py``) and executed there as a subprocess — by
anyone who downloads a bundle and by production render orchestration
(``run.py``) alike. It is executed, never imported. It intentionally
imports no TorchWright or ``torchwright_doom`` code: text enters through
the saved tokenizer, a stock ``Phi3ForCausalLM`` produces rows (a "row" is
one tokenizer id — one row of the tied embedding matrix, one sequence
position), and the only outputs are canonical integer ids plus their raw
tokenizer text. No pixels are produced here: the bundle's standalone
tools (``tools/txt_to_png.py``; token protocol in ``PROTOCOL.md`` in the
source repo) decode those ids into a frame afterward and do no inference.
The bundle manifest's schema, field meanings, and completeness gate live
in ``torchwright_doom/bundle/manifest.py`` in the source repo.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import time
from pathlib import Path
# Multi-shard checkpoints otherwise load serially. Keep these defaults in the
# portable program so downloaded bundles and production use the same loader.
os.environ.setdefault("HF_ENABLE_PARALLEL_LOADING", "true")
os.environ.setdefault("HF_PARALLEL_LOADING_WORKERS", "8")
import torch
import transformers
from transformers import TextGenerationPipeline, pipeline
_PROGRESS_INTERVAL_SECONDS = 15.0
def _canonical_json(value) -> bytes:
return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
def _sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _cuda_devices(model) -> list[torch.device]:
return sorted(
{parameter.device for parameter in model.parameters() if parameter.is_cuda},
key=str,
)
class _ProgressStreamer:
"""Report generation throughput without changing or collecting tokens."""
def __init__(self, prompt_rows: int, max_new_tokens: int) -> None:
self.prompt_rows = prompt_rows
self.max_new_tokens = max_new_tokens
self.started = time.monotonic()
self.last_progress = self.started
self.prefill_seconds: float | None = None
self.finished: float | None = None
self.generated_rows = 0
self._saw_prompt = False
def put(self, value: torch.Tensor) -> None:
# GenerationMixin streams the complete prompt once before any emitted
# row. It is already accounted for separately in ``prompt_rows``.
if not self._saw_prompt:
self._saw_prompt = True
return
now = time.monotonic()
if self.prefill_seconds is None:
self.prefill_seconds = now - self.started
print(
f"[infer] prefill complete; rows={self.prompt_rows} "
f"elapsed={self.prefill_seconds:.1f}s",
flush=True,
)
self.generated_rows += value.numel()
if now - self.last_progress >= _PROGRESS_INTERVAL_SECONDS:
elapsed = now - self.started - self.prefill_seconds
last_row = int(value.reshape(-1)[-1])
print(
f"[infer] decode rows={self.generated_rows}/{self.max_new_tokens} "
f"total_position={self.prompt_rows + self.generated_rows} "
f"elapsed={elapsed:.1f}s "
f"rows/s={self.generated_rows / elapsed:.1f} "
f"last_row={last_row}",
flush=True,
)
self.last_progress = now
def end(self) -> None:
self.finished = time.monotonic()
@property
def decode_seconds(self) -> float:
stopped = self.finished or time.monotonic()
prefill = self.prefill_seconds or 0.0
return stopped - self.started - prefill
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Run stock Phi-3 Doom inference")
parser.add_argument("--model", type=Path)
parser.add_argument("--prompt", type=Path)
parser.add_argument("--output", type=Path, default=Path("out"))
parser.add_argument(
"--device", default="cuda" if torch.cuda.is_available() else "cpu"
)
parser.add_argument("--max-new-tokens", type=int)
args = parser.parse_args(argv)
# This file sits at the bundle root, so its own directory is the bundle.
model_dir = (args.model or Path(__file__).resolve().parent).resolve()
prompt_path = args.prompt or model_dir / "examples" / "e1m1_prompt.txt"
manifest = json.loads((model_dir / "doom_bundle_manifest.json").read_text())
if not manifest.get("validation", {}).get("complete"):
raise ValueError("Doom bundle manifest is not complete")
prompt_bytes = prompt_path.read_bytes()
prompt_sha256 = _sha(prompt_bytes)
bundled_prompt = prompt_sha256 == manifest["prompt"]["sha256"]
load_t0 = time.monotonic()
model_kwargs = {
"attn_implementation": "eager",
# Read each shard's bytes eagerly: deferring them to mmap page faults
# stalls badly on network filesystems, and eager reads are harmless on
# local disks.
"disable_mmap": True,
}
generate: TextGenerationPipeline
if args.device != "cpu":
# Accelerate builds the skeleton on meta and dispatches each shard
# directly to the target device. This avoids a second full-model
# ``model.to(cuda)`` pass through CPU-backed mmap pages.
generate = pipeline(
"text-generation",
model=str(model_dir),
dtype=torch.float32,
model_kwargs=model_kwargs,
device_map=args.device,
)
else:
generate = pipeline(
"text-generation",
model=str(model_dir),
dtype=torch.float32,
model_kwargs=model_kwargs,
)
tokenizer = generate.tokenizer
if tokenizer is None:
raise RuntimeError("text-generation pipeline loaded without a tokenizer")
model = generate.model
model.eval()
cuda_devices = _cuda_devices(model)
for cuda_device in cuda_devices:
# Reset after loading: the current allocation still includes all
# weights, while the peak will additionally capture generation cache
# and runtime workspace. This is the consumer-fit measurement.
torch.cuda.reset_peak_memory_stats(cuda_device)
attention_implementation = getattr(model.config, "_attn_implementation", None)
if attention_implementation != "eager":
# Eager is the implementation the published render was validated
# under; fused kernels change fp accumulation order, and this check
# keeps every run on the validated numerics.
raise RuntimeError(
"Doom inference requires eager attention, got "
f"{attention_implementation!r}"
)
if (
model.config.original_max_position_embeddings
!= model.config.max_position_embeddings
):
raise RuntimeError(
"default-RoPE Doom model has inconsistent original/max position "
"capacity; GenerationMixin would discard its cache at the boundary"
)
load_seconds = time.monotonic() - load_t0
prompt_text = prompt_bytes.decode("utf-8")
encoded_prompt = tokenizer(
prompt_text,
return_tensors="pt",
add_special_tokens=False,
)
input_device = next(model.parameters()).device
prompt_ids = [int(row) for row in encoded_prompt.input_ids[0].tolist()]
prompt_ids_sha256 = _sha(_canonical_json(prompt_ids))
# Only the bundled prompt has a manifest row-id expectation; a custom
# prompt is permitted, never verified, and recorded in the payload as
# matches_bundled_prompt=false.
if bundled_prompt and prompt_ids_sha256 != manifest["prompt"]["row_ids_sha256"]:
raise ValueError("bundled prompt text does not reproduce its manifest rows")
default_new = int(manifest["generation"]["max_new_tokens"])
max_new = default_new if args.max_new_tokens is None else int(args.max_new_tokens)
if max_new < 1:
raise ValueError("max-new-tokens must be >= 1")
if len(prompt_ids) + max_new > model.config.max_position_embeddings:
raise ValueError("requested generation exceeds model position capacity")
print(
f"[infer] model ready in {load_seconds:.1f}s; prompt={len(prompt_ids)} "
f"max_new_tokens={max_new} device={input_device}",
flush=True,
)
generate_t0 = time.monotonic()
progress = _ProgressStreamer(len(prompt_ids), max_new)
print(f"[infer] generation started; max_new_tokens={max_new}", flush=True)
with torch.inference_mode():
records = generate(
prompt_text,
add_special_tokens=False,
return_tensors=True,
do_sample=False,
use_cache=True,
max_new_tokens=max_new,
eos_token_id=tokenizer.eos_token_id,
pad_token_id=tokenizer.pad_token_id,
streamer=progress,
)
generate_seconds = time.monotonic() - generate_t0
sequence = records[0]["generated_token_ids"]
generated = [int(row) for row in sequence[len(prompt_ids) :]]
for cuda_device in cuda_devices:
torch.cuda.synchronize(cuda_device)
cuda_memory = [
{
"device": str(cuda_device),
"peak_allocated_bytes": torch.cuda.max_memory_allocated(cuda_device),
"peak_reserved_bytes": torch.cuda.max_memory_reserved(cuda_device),
}
for cuda_device in cuda_devices
]
prefill_seconds = progress.prefill_seconds or generate_seconds
decode_seconds = progress.decode_seconds
raw_text = tokenizer.decode(
generated, skip_special_tokens=False, clean_up_tokenization_spaces=False
)
if not isinstance(raw_text, str):
raise TypeError("tokenizer returned a batched decode for one row list")
if tokenizer(raw_text, add_special_tokens=False)["input_ids"] != generated:
raise ValueError("decoded output text does not round-trip to generated rows")
args.output.mkdir(parents=True, exist_ok=True)
emitted_ids_sha256 = _sha(_canonical_json(generated))
stopped = bool(generated and generated[-1] == tokenizer.eos_token_id)
payload = {
"format": "torchwright_doom.output_ids.v1",
"bundle": manifest.get("bundle_identity"),
"compile_payload_sha256": manifest.get("compile_payload_sha256"),
"row_vocab_fingerprint": manifest.get("row_vocab_fingerprint"),
"prompt": {
"sha256": prompt_sha256,
"matches_bundled_prompt": bundled_prompt,
"row_ids": prompt_ids,
"row_ids_sha256": prompt_ids_sha256,
},
"emitted_row_ids": generated,
"emitted_row_ids_sha256": emitted_ids_sha256,
"generation": {
"mode": "transformers_pipeline",
"max_new_tokens": max_new,
"termination_reason": "terminal" if stopped else "cap",
},
"timing_seconds": {
"load": load_seconds,
"prefill": prefill_seconds,
"decode": decode_seconds,
"generate": generate_seconds,
},
"attention_implementation": attention_implementation,
"cuda_memory": cuda_memory,
"transformers_version": transformers.__version__,
}
(args.output / "output.ids.json").write_text(
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
(args.output / "output.txt").write_text(raw_text + "\n", encoding="utf-8")
print(
f"[infer] wrote {len(generated)} rows in {generate_seconds:.1f}s; "
f"stopped={payload['generation']['termination_reason']}",
flush=True,
)
for memory in cuda_memory:
peak_allocated = int(memory["peak_allocated_bytes"])
peak_reserved = int(memory["peak_reserved_bytes"])
print(
f"[infer] {memory['device']} peak allocated="
f"{peak_allocated / 1024**3:.2f} GiB "
f"reserved={peak_reserved / 1024**3:.2f} GiB",
flush=True,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())