Spaces:
Running on Zero
Running on Zero
| """Transformers inference for sequential and batched PaDoc decoding.""" | |
| from __future__ import annotations | |
| import argparse | |
| import copy | |
| import json | |
| import logging | |
| from collections.abc import Iterator | |
| from pathlib import Path | |
| from typing import Any | |
| import torch | |
| from padoc.modeling import load_padoc_model | |
| logger = logging.getLogger(__name__) | |
| EXECUTION_MODES = ("parallel", "sequential") | |
| def _fork_id_map(tokenizer, values: dict[str, str]) -> dict[int, list[int]]: | |
| result: dict[int, list[int]] = {} | |
| for trigger, target in values.items(): | |
| trigger_ids = tokenizer.encode(trigger, add_special_tokens=False) | |
| target_ids = tokenizer.encode(target, add_special_tokens=False) | |
| if len(trigger_ids) != 1 or len(target_ids) != 1: | |
| raise ValueError( | |
| f"Invalid atomic fork mapping {trigger!r}->{target!r}: {trigger_ids}->{target_ids}" | |
| ) | |
| result[trigger_ids[0]] = list(target_ids) | |
| return result | |
| class SequentialPaDocEngine: | |
| """Greedy PaDoc decoder with sequential and lockstep-batched execution.""" | |
| def __init__( | |
| self, | |
| model, | |
| processor, | |
| fork_token_map: dict[str, str] | dict[int, list[int]], | |
| *, | |
| max_new_tokens: int = 512, | |
| max_branch_tokens: int | None = None, | |
| max_concurrent_branches: int = 8, | |
| max_total_branches: int = 64, | |
| execution_mode: str = "sequential", | |
| strict: bool = True, | |
| ) -> None: | |
| if max_new_tokens < 1 or (max_branch_tokens is not None and max_branch_tokens < 1): | |
| raise ValueError("Token limits must be positive.") | |
| if max_total_branches < 0: | |
| raise ValueError("max_total_branches must be non-negative.") | |
| if max_concurrent_branches < 1: | |
| raise ValueError("max_concurrent_branches must be positive.") | |
| if execution_mode not in EXECUTION_MODES: | |
| raise ValueError( | |
| f"Unknown execution_mode {execution_mode!r}; choose from {EXECUTION_MODES}." | |
| ) | |
| self.model = model | |
| self.processor = processor | |
| self.tokenizer = processor.tokenizer | |
| if fork_token_map and isinstance(next(iter(fork_token_map)), str): | |
| fork_token_map = _fork_id_map(self.tokenizer, fork_token_map) | |
| self.fork_token_map = dict(fork_token_map) | |
| self.max_new_tokens = max_new_tokens | |
| self.max_branch_tokens = max_branch_tokens or max_new_tokens | |
| self.max_concurrent_branches = ( | |
| min(max_concurrent_branches, max_total_branches) | |
| if max_total_branches | |
| else max_concurrent_branches | |
| ) | |
| self.max_total_branches = max_total_branches | |
| self.execution_mode = execution_mode | |
| self.strict = strict | |
| self.eos_token_id = self.tokenizer.eos_token_id | |
| if self.eos_token_id is None: | |
| raise ValueError("The tokenizer must define eos_token_id.") | |
| self.device = model.device | |
| parameters = getattr(model, "parameters", None) | |
| self.devices = ( | |
| sorted({str(parameter.device) for parameter in parameters()}) | |
| if callable(parameters) | |
| else [str(self.device)] | |
| ) | |
| self._branch_forbidden_ids: list[int] = [] | |
| for token in ("<SP_LAYOUT>", "</SP_LAYOUT>"): | |
| token_ids = self.tokenizer.encode(token, add_special_tokens=False) | |
| if len(token_ids) == 1: | |
| self._branch_forbidden_ids.append(token_ids[0]) | |
| def from_pretrained( | |
| cls, | |
| model_path: str | Path, | |
| *, | |
| dtype: torch.dtype = torch.bfloat16, | |
| device: str = "cuda:0", | |
| attn_implementation: str = "sdpa", | |
| **engine_kwargs, | |
| ) -> SequentialPaDocEngine: | |
| if device == "auto": | |
| raise ValueError( | |
| "PaDoc Transformers inference is single-device; choose an explicit " | |
| "device such as 'cuda:0' or 'cpu'." | |
| ) | |
| device_map: dict[str, str] = {"": device} | |
| model, processor, fork_map = load_padoc_model( | |
| model_path, | |
| dtype=dtype, | |
| device_map=device_map, | |
| attn_implementation=attn_implementation, | |
| ) | |
| model.eval() | |
| return cls(model, processor, fork_map, **engine_kwargs) | |
| def resolve_execution_mode(self, execution_mode: str | None) -> str: | |
| mode = execution_mode or self.execution_mode | |
| if mode not in EXECUTION_MODES: | |
| raise ValueError(f"Unknown execution_mode {mode!r}; choose from {EXECUTION_MODES}.") | |
| return mode | |
| def _mrope_delta(self, *, multimodal: bool) -> int: | |
| if not multimodal: | |
| return 0 | |
| rope_deltas = getattr(getattr(self.model, "model", None), "rope_deltas", None) | |
| if rope_deltas is None or rope_deltas.numel() == 0: | |
| raise RuntimeError("Multimodal prefill did not produce M-RoPE deltas.") | |
| return int(rope_deltas.reshape(-1)[0].item()) | |
| def _position_ids( | |
| self, | |
| logical_positions: torch.LongTensor, | |
| *, | |
| multimodal: bool, | |
| ) -> torch.LongTensor: | |
| shifted = logical_positions + self._mrope_delta(multimodal=multimodal) | |
| axes = 3 if multimodal else 4 | |
| return shifted.unsqueeze(0).expand(axes, -1, -1).contiguous() | |
| def _next_token(self, logits: torch.Tensor, *, branch: bool) -> int: | |
| values = logits[:, -1, :] | |
| if branch and self.strict and self._branch_forbidden_ids: | |
| values = values.clone() | |
| values[:, self._branch_forbidden_ids] = torch.finfo(values.dtype).min | |
| return int(values.argmax(dim=-1).item()) | |
| def _stream_branch_tokens( | |
| self, | |
| parent_cache, | |
| *, | |
| fork_position: int, | |
| injected_tokens: list[int], | |
| multimodal: bool, | |
| ) -> Iterator[int]: | |
| if not injected_tokens: | |
| raise ValueError("A fork target must contain at least one token.") | |
| cache = copy.deepcopy(parent_cache) | |
| injected = torch.tensor([injected_tokens], dtype=torch.long, device=self.device) | |
| logical = torch.arange( | |
| fork_position, | |
| fork_position + len(injected_tokens), | |
| dtype=torch.long, | |
| device=self.device, | |
| ).unsqueeze(0) | |
| output = self.model( | |
| input_ids=injected, | |
| position_ids=self._position_ids(logical, multimodal=multimodal), | |
| past_key_values=cache, | |
| use_cache=True, | |
| ) | |
| cache = output.past_key_values | |
| next_token = self._next_token(output.logits, branch=True) | |
| generated = 0 | |
| while generated < self.max_branch_tokens: | |
| yield next_token | |
| generated += 1 | |
| if next_token == self.eos_token_id: | |
| break | |
| logical_position = fork_position + len(injected_tokens) + generated - 1 | |
| output = self.model( | |
| input_ids=torch.tensor([[next_token]], dtype=torch.long, device=self.device), | |
| position_ids=self._position_ids( | |
| torch.tensor([[logical_position]], dtype=torch.long, device=self.device), | |
| multimodal=multimodal, | |
| ), | |
| past_key_values=cache, | |
| use_cache=True, | |
| ) | |
| cache = output.past_key_values | |
| next_token = self._next_token(output.logits, branch=True) | |
| def stream( | |
| self, | |
| messages: list[dict[str, Any]], | |
| *, | |
| execution_mode: str | None = None, | |
| ) -> Iterator[dict[str, Any]]: | |
| """Yield a shared event protocol from either Transformers execution mode.""" | |
| mode = self.resolve_execution_mode(execution_mode) | |
| if mode == "parallel": | |
| from padoc.transformers_parallel import stream_parallel | |
| yield from stream_parallel(self, messages) | |
| return | |
| yield from self._stream_sequential(messages) | |
| def _stream_sequential(self, messages: list[dict[str, Any]]) -> Iterator[dict[str, Any]]: | |
| """Decode a branch to completion before resuming the batch=1 main stream.""" | |
| yield {"type": "accepted", "execution_mode": "sequential"} | |
| inputs = self.processor.apply_chat_template( | |
| messages, | |
| tokenize=True, | |
| add_generation_prompt=True, | |
| return_dict=True, | |
| return_tensors="pt", | |
| ).to(self.device) | |
| prompt_ids = inputs["input_ids"][0].tolist() | |
| prompt_length = len(prompt_ids) | |
| multimodal = ( | |
| inputs.get("image_grid_thw") is not None or inputs.get("video_grid_thw") is not None | |
| ) | |
| output = self.model(**inputs, use_cache=True) | |
| cache = output.past_key_values | |
| next_token = self._next_token(output.logits, branch=False) | |
| main_tokens: list[int] = [] | |
| branches: list[dict[str, Any]] = [] | |
| dropped_triggers = 0 | |
| yield { | |
| "type": "scheduler", | |
| "execution_mode": "sequential", | |
| "phase": "prefill_complete", | |
| "main_active": True, | |
| "active_branches": 0, | |
| "queued_branches": 0, | |
| "completed_branches": 0, | |
| "batch_size": 1, | |
| } | |
| while len(main_tokens) < self.max_new_tokens: | |
| response_position = len(main_tokens) | |
| main_tokens.append(next_token) | |
| yield { | |
| "type": "main", | |
| "token_ids": [next_token], | |
| "delta_text": self.tokenizer.decode([next_token], skip_special_tokens=False), | |
| "text": self.tokenizer.decode(main_tokens, skip_special_tokens=False), | |
| "total": len(main_tokens), | |
| } | |
| injected = self.fork_token_map.get(next_token) | |
| if injected: | |
| if len(branches) >= self.max_total_branches: | |
| dropped_triggers += 1 | |
| else: | |
| fork_position = prompt_length + response_position | |
| branch_index = len(branches) | |
| injected_tokens = list(injected) | |
| injected_text = self.tokenizer.decode( | |
| injected_tokens, skip_special_tokens=False | |
| ) | |
| yield { | |
| "type": "fork", | |
| "branch_index": branch_index, | |
| "fork_position": fork_position, | |
| "trigger_token_id": next_token, | |
| "injected_token_ids": injected_tokens, | |
| "injected_text": injected_text, | |
| "branch_state": "active", | |
| } | |
| yield { | |
| "type": "scheduler", | |
| "execution_mode": "sequential", | |
| "phase": "branch_decoding", | |
| "main_active": False, | |
| "active_branches": 1, | |
| "queued_branches": 0, | |
| "completed_branches": len(branches), | |
| "batch_size": 1, | |
| } | |
| generated_branch_tokens: list[int] = [] | |
| for branch_token in self._stream_branch_tokens( | |
| cache, | |
| fork_position=fork_position, | |
| injected_tokens=injected_tokens, | |
| multimodal=multimodal, | |
| ): | |
| generated_branch_tokens.append(branch_token) | |
| branch_tokens = [*injected_tokens, *generated_branch_tokens] | |
| yield { | |
| "type": "branch", | |
| "branch_index": branch_index, | |
| "fork_position": fork_position, | |
| "token_ids": [branch_token], | |
| "delta_text": self.tokenizer.decode( | |
| [branch_token], skip_special_tokens=False | |
| ), | |
| "text": self.tokenizer.decode( | |
| branch_tokens, skip_special_tokens=False | |
| ), | |
| "total": len(branch_tokens), | |
| } | |
| branch_tokens = [*injected_tokens, *generated_branch_tokens] | |
| branches.append( | |
| { | |
| "branch_index": branch_index, | |
| "fork_position": fork_position, | |
| "trigger_token_id": next_token, | |
| "injected_token_ids": injected_tokens, | |
| "token_ids": branch_tokens, | |
| "text": self.tokenizer.decode(branch_tokens, skip_special_tokens=False), | |
| } | |
| ) | |
| yield { | |
| "type": "branch_done", | |
| "branch_index": branch_index, | |
| "total": len(branch_tokens), | |
| } | |
| yield { | |
| "type": "scheduler", | |
| "execution_mode": "sequential", | |
| "phase": "main_resumed", | |
| "main_active": True, | |
| "active_branches": 0, | |
| "queued_branches": 0, | |
| "completed_branches": len(branches), | |
| "batch_size": 1, | |
| } | |
| if next_token == self.eos_token_id or len(main_tokens) >= self.max_new_tokens: | |
| break | |
| logical_position = prompt_length + response_position | |
| output = self.model( | |
| input_ids=torch.tensor([[next_token]], dtype=torch.long, device=self.device), | |
| position_ids=self._position_ids( | |
| torch.tensor([[logical_position]], dtype=torch.long, device=self.device), | |
| multimodal=multimodal, | |
| ), | |
| past_key_values=cache, | |
| use_cache=True, | |
| ) | |
| cache = output.past_key_values | |
| next_token = self._next_token(output.logits, branch=False) | |
| yield {"type": "main_done", "total": len(main_tokens)} | |
| yield { | |
| "type": "done", | |
| "execution_mode": "sequential", | |
| "main": self.tokenizer.decode(main_tokens, skip_special_tokens=False), | |
| "main_token_ids": main_tokens, | |
| "prompt_tokens": prompt_length, | |
| "branches": branches, | |
| "branch_limit_reached": dropped_triggers > 0, | |
| "dropped_branch_triggers": dropped_triggers, | |
| "peak_batch_size": 1, | |
| } | |
| def generate( | |
| self, | |
| messages: list[dict[str, Any]], | |
| *, | |
| execution_mode: str | None = None, | |
| ) -> dict[str, Any]: | |
| """Consume the event stream and return its final result.""" | |
| result: dict[str, Any] | None = None | |
| for event in self.stream(messages, execution_mode=execution_mode): | |
| if event.get("type") == "done": | |
| result = dict(event) | |
| if result is None: | |
| raise RuntimeError("Transformers decoding ended without a final result.") | |
| result.pop("type", None) | |
| return result | |
| def _dtype(name: str) -> torch.dtype: | |
| return { | |
| "bfloat16": torch.bfloat16, | |
| "float16": torch.float16, | |
| "float32": torch.float32, | |
| }[name] | |
| def parse_args(argv: list[str] | None = None) -> argparse.Namespace: | |
| parser = argparse.ArgumentParser(description="Run PaDoc Transformers inference.") | |
| parser.add_argument("--model", required=True) | |
| parser.add_argument("--query", default="Parse this document.") | |
| parser.add_argument("--image", action="append", default=[]) | |
| parser.add_argument("--max-new-tokens", type=int, default=512) | |
| parser.add_argument("--max-branch-tokens", type=int, default=None) | |
| parser.add_argument("--max-concurrent-branches", type=int, default=8) | |
| parser.add_argument("--max-total-branches", type=int, default=64) | |
| parser.add_argument("--execution-mode", choices=EXECUTION_MODES, default="parallel") | |
| parser.add_argument( | |
| "--device", | |
| default="cuda:0", | |
| help="Single inference device (default: cuda:0); automatic sharding is not used.", | |
| ) | |
| parser.add_argument( | |
| "--dtype", | |
| choices=("bfloat16", "float16", "float32"), | |
| default="bfloat16", | |
| ) | |
| parser.add_argument( | |
| "--attn-implementation", | |
| choices=("sdpa", "eager", "flash_attention_2"), | |
| default="sdpa", | |
| ) | |
| parser.add_argument("--json", action="store_true", dest="json_output") | |
| return parser.parse_args(argv) | |
| def main(argv: list[str] | None = None) -> None: | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| args = parse_args(argv) | |
| logger.info("Loading %s", args.model) | |
| engine = SequentialPaDocEngine.from_pretrained( | |
| args.model, | |
| dtype=_dtype(args.dtype), | |
| device=args.device, | |
| attn_implementation=args.attn_implementation, | |
| max_new_tokens=args.max_new_tokens, | |
| max_branch_tokens=args.max_branch_tokens, | |
| max_concurrent_branches=args.max_concurrent_branches, | |
| max_total_branches=args.max_total_branches, | |
| execution_mode=args.execution_mode, | |
| ) | |
| content = [{"type": "image", "image": image} for image in args.image] | |
| content.append({"type": "text", "text": args.query}) | |
| logger.info("Model devices: %s", ", ".join(engine.devices)) | |
| result = engine.generate( | |
| [{"role": "user", "content": content}], execution_mode=args.execution_mode | |
| ) | |
| if args.json_output: | |
| print(json.dumps(result, ensure_ascii=False, indent=2)) | |
| return | |
| print("MAIN") | |
| print(result["main"]) | |
| for branch in result["branches"]: | |
| print(f"\nBRANCH {branch['branch_index']}") | |
| print(branch["text"]) | |
| if __name__ == "__main__": | |
| main() | |