CoolFace
Modelpublic

physicsrob/torchwright-doom-e1m1

sourceHugging Faceupdated 2mo agoView on Hugging Face
3likes145downloads
infer.py303 linesDownload Raw Back to root
1"""The sole Doom inference program: portable stock-Hugging-Face generation.2 3This file is copied byte-identical to the root of every published Doom4bundle (``<bundle>/infer.py``) and executed there as a subprocess — by5anyone who downloads a bundle and by production render orchestration6(``run.py``) alike.  It is executed, never imported.  It intentionally7imports no TorchWright or ``torchwright_doom`` code: text enters through8the saved tokenizer, a stock ``Phi3ForCausalLM`` produces rows (a "row" is9one tokenizer id — one row of the tied embedding matrix, one sequence10position), and the only outputs are canonical integer ids plus their raw11tokenizer text.  No pixels are produced here: the bundle's standalone12tools (``tools/txt_to_png.py``; token protocol in ``PROTOCOL.md`` in the13source repo) decode those ids into a frame afterward and do no inference.14The bundle manifest's schema, field meanings, and completeness gate live15in ``torchwright_doom/bundle/manifest.py`` in the source repo.16"""17 18from __future__ import annotations19 20import argparse21import hashlib22import json23import os24import time25from pathlib import Path26 27# Multi-shard checkpoints otherwise load serially.  Keep these defaults in the28# portable program so downloaded bundles and production use the same loader.29os.environ.setdefault("HF_ENABLE_PARALLEL_LOADING", "true")30os.environ.setdefault("HF_PARALLEL_LOADING_WORKERS", "8")31 32import torch33import transformers34from transformers import TextGenerationPipeline, pipeline35 36_PROGRESS_INTERVAL_SECONDS = 15.037 38 39def _canonical_json(value) -> bytes:40    return json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")41 42 43def _sha(data: bytes) -> str:44    return hashlib.sha256(data).hexdigest()45 46 47def _cuda_devices(model) -> list[torch.device]:48    return sorted(49        {parameter.device for parameter in model.parameters() if parameter.is_cuda},50        key=str,51    )52 53 54class _ProgressStreamer:55    """Report generation throughput without changing or collecting tokens."""56 57    def __init__(self, prompt_rows: int, max_new_tokens: int) -> None:58        self.prompt_rows = prompt_rows59        self.max_new_tokens = max_new_tokens60        self.started = time.monotonic()61        self.last_progress = self.started62        self.prefill_seconds: float | None = None63        self.finished: float | None = None64        self.generated_rows = 065        self._saw_prompt = False66 67    def put(self, value: torch.Tensor) -> None:68        # GenerationMixin streams the complete prompt once before any emitted69        # row.  It is already accounted for separately in ``prompt_rows``.70        if not self._saw_prompt:71            self._saw_prompt = True72            return73        now = time.monotonic()74        if self.prefill_seconds is None:75            self.prefill_seconds = now - self.started76            print(77                f"[infer] prefill complete; rows={self.prompt_rows} "78                f"elapsed={self.prefill_seconds:.1f}s",79                flush=True,80            )81        self.generated_rows += value.numel()82        if now - self.last_progress >= _PROGRESS_INTERVAL_SECONDS:83            elapsed = now - self.started - self.prefill_seconds84            last_row = int(value.reshape(-1)[-1])85            print(86                f"[infer] decode rows={self.generated_rows}/{self.max_new_tokens} "87                f"total_position={self.prompt_rows + self.generated_rows} "88                f"elapsed={elapsed:.1f}s "89                f"rows/s={self.generated_rows / elapsed:.1f} "90                f"last_row={last_row}",91                flush=True,92            )93            self.last_progress = now94 95    def end(self) -> None:96        self.finished = time.monotonic()97 98    @property99    def decode_seconds(self) -> float:100        stopped = self.finished or time.monotonic()101        prefill = self.prefill_seconds or 0.0102        return stopped - self.started - prefill103 104 105def main(argv: list[str] | None = None) -> int:106    parser = argparse.ArgumentParser(description="Run stock Phi-3 Doom inference")107    parser.add_argument("--model", type=Path)108    parser.add_argument("--prompt", type=Path)109    parser.add_argument("--output", type=Path, default=Path("out"))110    parser.add_argument(111        "--device", default="cuda" if torch.cuda.is_available() else "cpu"112    )113    parser.add_argument("--max-new-tokens", type=int)114    args = parser.parse_args(argv)115 116    # This file sits at the bundle root, so its own directory is the bundle.117    model_dir = (args.model or Path(__file__).resolve().parent).resolve()118    prompt_path = args.prompt or model_dir / "examples" / "e1m1_prompt.txt"119    manifest = json.loads((model_dir / "doom_bundle_manifest.json").read_text())120    if not manifest.get("validation", {}).get("complete"):121        raise ValueError("Doom bundle manifest is not complete")122 123    prompt_bytes = prompt_path.read_bytes()124    prompt_sha256 = _sha(prompt_bytes)125    bundled_prompt = prompt_sha256 == manifest["prompt"]["sha256"]126 127    load_t0 = time.monotonic()128    model_kwargs = {129        "attn_implementation": "eager",130        # Read each shard's bytes eagerly: deferring them to mmap page faults131        # stalls badly on network filesystems, and eager reads are harmless on132        # local disks.133        "disable_mmap": True,134    }135    generate: TextGenerationPipeline136    if args.device != "cpu":137        # Accelerate builds the skeleton on meta and dispatches each shard138        # directly to the target device.  This avoids a second full-model139        # ``model.to(cuda)`` pass through CPU-backed mmap pages.140        generate = pipeline(141            "text-generation",142            model=str(model_dir),143            dtype=torch.float32,144            model_kwargs=model_kwargs,145            device_map=args.device,146        )147    else:148        generate = pipeline(149            "text-generation",150            model=str(model_dir),151            dtype=torch.float32,152            model_kwargs=model_kwargs,153        )154    tokenizer = generate.tokenizer155    if tokenizer is None:156        raise RuntimeError("text-generation pipeline loaded without a tokenizer")157    model = generate.model158    model.eval()159    cuda_devices = _cuda_devices(model)160    for cuda_device in cuda_devices:161        # Reset after loading: the current allocation still includes all162        # weights, while the peak will additionally capture generation cache163        # and runtime workspace. This is the consumer-fit measurement.164        torch.cuda.reset_peak_memory_stats(cuda_device)165    attention_implementation = getattr(model.config, "_attn_implementation", None)166    if attention_implementation != "eager":167        # Eager is the implementation the published render was validated168        # under; fused kernels change fp accumulation order, and this check169        # keeps every run on the validated numerics.170        raise RuntimeError(171            "Doom inference requires eager attention, got "172            f"{attention_implementation!r}"173        )174    if (175        model.config.original_max_position_embeddings176        != model.config.max_position_embeddings177    ):178        raise RuntimeError(179            "default-RoPE Doom model has inconsistent original/max position "180            "capacity; GenerationMixin would discard its cache at the boundary"181        )182    load_seconds = time.monotonic() - load_t0183 184    prompt_text = prompt_bytes.decode("utf-8")185    encoded_prompt = tokenizer(186        prompt_text,187        return_tensors="pt",188        add_special_tokens=False,189    )190    input_device = next(model.parameters()).device191    prompt_ids = [int(row) for row in encoded_prompt.input_ids[0].tolist()]192    prompt_ids_sha256 = _sha(_canonical_json(prompt_ids))193    # Only the bundled prompt has a manifest row-id expectation; a custom194    # prompt is permitted, never verified, and recorded in the payload as195    # matches_bundled_prompt=false.196    if bundled_prompt and prompt_ids_sha256 != manifest["prompt"]["row_ids_sha256"]:197        raise ValueError("bundled prompt text does not reproduce its manifest rows")198 199    default_new = int(manifest["generation"]["max_new_tokens"])200    max_new = default_new if args.max_new_tokens is None else int(args.max_new_tokens)201    if max_new < 1:202        raise ValueError("max-new-tokens must be >= 1")203    if len(prompt_ids) + max_new > model.config.max_position_embeddings:204        raise ValueError("requested generation exceeds model position capacity")205 206    print(207        f"[infer] model ready in {load_seconds:.1f}s; prompt={len(prompt_ids)} "208        f"max_new_tokens={max_new} device={input_device}",209        flush=True,210    )211    generate_t0 = time.monotonic()212    progress = _ProgressStreamer(len(prompt_ids), max_new)213    print(f"[infer] generation started; max_new_tokens={max_new}", flush=True)214    with torch.inference_mode():215        records = generate(216            prompt_text,217            add_special_tokens=False,218            return_tensors=True,219            do_sample=False,220            use_cache=True,221            max_new_tokens=max_new,222            eos_token_id=tokenizer.eos_token_id,223            pad_token_id=tokenizer.pad_token_id,224            streamer=progress,225        )226    generate_seconds = time.monotonic() - generate_t0227    sequence = records[0]["generated_token_ids"]228    generated = [int(row) for row in sequence[len(prompt_ids) :]]229    for cuda_device in cuda_devices:230        torch.cuda.synchronize(cuda_device)231    cuda_memory = [232        {233            "device": str(cuda_device),234            "peak_allocated_bytes": torch.cuda.max_memory_allocated(cuda_device),235            "peak_reserved_bytes": torch.cuda.max_memory_reserved(cuda_device),236        }237        for cuda_device in cuda_devices238    ]239    prefill_seconds = progress.prefill_seconds or generate_seconds240    decode_seconds = progress.decode_seconds241    raw_text = tokenizer.decode(242        generated, skip_special_tokens=False, clean_up_tokenization_spaces=False243    )244    if not isinstance(raw_text, str):245        raise TypeError("tokenizer returned a batched decode for one row list")246    if tokenizer(raw_text, add_special_tokens=False)["input_ids"] != generated:247        raise ValueError("decoded output text does not round-trip to generated rows")248 249    args.output.mkdir(parents=True, exist_ok=True)250    emitted_ids_sha256 = _sha(_canonical_json(generated))251    stopped = bool(generated and generated[-1] == tokenizer.eos_token_id)252    payload = {253        "format": "torchwright_doom.output_ids.v1",254        "bundle": manifest.get("bundle_identity"),255        "compile_payload_sha256": manifest.get("compile_payload_sha256"),256        "row_vocab_fingerprint": manifest.get("row_vocab_fingerprint"),257        "prompt": {258            "sha256": prompt_sha256,259            "matches_bundled_prompt": bundled_prompt,260            "row_ids": prompt_ids,261            "row_ids_sha256": prompt_ids_sha256,262        },263        "emitted_row_ids": generated,264        "emitted_row_ids_sha256": emitted_ids_sha256,265        "generation": {266            "mode": "transformers_pipeline",267            "max_new_tokens": max_new,268            "termination_reason": "terminal" if stopped else "cap",269        },270        "timing_seconds": {271            "load": load_seconds,272            "prefill": prefill_seconds,273            "decode": decode_seconds,274            "generate": generate_seconds,275        },276        "attention_implementation": attention_implementation,277        "cuda_memory": cuda_memory,278        "transformers_version": transformers.__version__,279    }280    (args.output / "output.ids.json").write_text(281        json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"282    )283    (args.output / "output.txt").write_text(raw_text + "\n", encoding="utf-8")284    print(285        f"[infer] wrote {len(generated)} rows in {generate_seconds:.1f}s; "286        f"stopped={payload['generation']['termination_reason']}",287        flush=True,288    )289    for memory in cuda_memory:290        peak_allocated = int(memory["peak_allocated_bytes"])291        peak_reserved = int(memory["peak_reserved_bytes"])292        print(293            f"[infer] {memory['device']} peak allocated="294            f"{peak_allocated / 1024**3:.2f} GiB "295            f"reserved={peak_reserved / 1024**3:.2f} GiB",296            flush=True,297        )298    return 0299 300 301if __name__ == "__main__":302    raise SystemExit(main())303