Download code/pluto21/runtime.py from omida-ai/pluto-2.1: direct link, hf CLI and curl.
- Browser
- Download file 6.71 kB
-
https://huggingface.co/omida-ai/pluto-2.1/resolve/main/code/pluto21/runtime.py
- Command line
-
hf download hf://omida-ai/pluto-2.1/code/pluto21/runtime.py
-
curl -L -o runtime.py https://huggingface.co/omida-ai/pluto-2.1/resolve/main/code/pluto21/runtime.py
6.71 kB
| """Standalone runtime for the merged Pluto-2.1 decision model.""" | |
| from __future__ import annotations | |
| import hashlib | |
| import json | |
| import os | |
| import random | |
| from pathlib import Path | |
| from typing import Any, Mapping, Sequence | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| SYSTEM_PROMPT = ( | |
| "Choose exactly one supplied decision token. Return only that token. " | |
| "Do not explain the answer." | |
| ) | |
| DECISION_TYPES = frozenset({"choice", "score", "noul"}) | |
| def _canonical_json(value: Any) -> str: | |
| return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True) | |
| def _compact_criteria(mapped_criteria: Sequence[Mapping[str, Any]]): | |
| parsed_descriptions = [] | |
| description_fields = None | |
| for criterion in mapped_criteria: | |
| try: | |
| parsed = json.loads(str(criterion["d"])) | |
| except json.JSONDecodeError: | |
| parsed = None | |
| if not isinstance(parsed, dict) or not all(isinstance(key, str) for key in parsed): | |
| description_fields = None | |
| break | |
| fields = sorted(parsed) | |
| if description_fields is None: | |
| description_fields = fields | |
| elif fields != description_fields: | |
| description_fields = None | |
| break | |
| parsed_descriptions.append(parsed) | |
| if description_fields is not None and len(parsed_descriptions) == len(mapped_criteria): | |
| rows = [ | |
| [criterion["t"], criterion["o"], criterion["n"], *(parsed[field] for field in description_fields)] | |
| for criterion, parsed in zip(mapped_criteria, parsed_descriptions, strict=True) | |
| ] | |
| return ["t", "o", "n", *description_fields], rows | |
| return ["t", "o", "n", "d"], [ | |
| [criterion["t"], criterion["o"], criterion["n"], criterion["d"]] | |
| for criterion in mapped_criteria | |
| ] | |
| def render_decision_prompt( | |
| *, | |
| state: Any, | |
| instructions: str, | |
| candidates: Sequence[Mapping[str, Any]], | |
| decision_type: str, | |
| decision_tokens: Sequence[str], | |
| seed: int, | |
| row_id: str, | |
| prompt_schema: str, | |
| ): | |
| candidate_count = len(candidates) | |
| if not 2 <= candidate_count <= 255: | |
| raise ValueError("Pluto-2.1 supports 2 to 255 candidates") | |
| if decision_type not in DECISION_TYPES: | |
| raise ValueError("decision_type must be choice, score, or noul") | |
| if len(decision_tokens) < candidate_count: | |
| raise ValueError("not enough decision tokens") | |
| presentation_order = list(range(candidate_count)) | |
| digest = hashlib.sha256(f"{seed}:{row_id}:candidate-order".encode()).digest() | |
| random.Random(int.from_bytes(digest[:8], "big")).shuffle(presentation_order) | |
| mapped = [] | |
| candidate_to_slot = {} | |
| for slot, candidate_index in enumerate(presentation_order): | |
| candidate = candidates[candidate_index] | |
| candidate_id = str(candidate["id"]) | |
| candidate_to_slot[candidate_id] = slot | |
| mapped.append( | |
| { | |
| "t": decision_tokens[slot], | |
| "o": candidate_index, | |
| "n": str(candidate["label"]), | |
| "d": candidate.get("description") or "", | |
| } | |
| ) | |
| fields, criteria = _compact_criteria(mapped) | |
| content = _canonical_json( | |
| { | |
| "schema": prompt_schema, | |
| "state": state, | |
| "question": { | |
| "instructions": instructions, | |
| "type": decision_type, | |
| "criteria_fields": fields, | |
| "criteria": criteria, | |
| }, | |
| } | |
| ) | |
| return content, presentation_order, candidate_to_slot | |
| class Pluto21: | |
| def __init__(self, path: str, device: str | None = "cuda"): | |
| self.path = Path(path) | |
| with open(self.path / "pluto21_config.json", encoding="utf-8") as handle: | |
| self.config = json.load(handle) | |
| self.model = AutoModelForCausalLM.from_pretrained( | |
| path, | |
| dtype=torch.bfloat16, | |
| device_map=device, | |
| ).eval() | |
| tokenizer_path = self.path / "tokenizer" | |
| self.tokenizer = AutoTokenizer.from_pretrained( | |
| str(tokenizer_path if tokenizer_path.is_dir() else path) | |
| ) | |
| self.tokenizer.padding_side = "left" | |
| self.decision_tokens = [ | |
| f"<|decision_{index:03d}|>" | |
| for index in range(int(self.config["decision_token_count"])) | |
| ] | |
| ids = [self.tokenizer.convert_tokens_to_ids(token) for token in self.decision_tokens] | |
| if any(token_id is None for token_id in ids): | |
| raise RuntimeError("tokenizer is missing the decision tokens") | |
| self.decision_token_ids = torch.tensor(ids, dtype=torch.long) | |
| def predict( | |
| self, | |
| *, | |
| state: Any, | |
| instructions: str, | |
| candidates: Sequence[Mapping[str, Any]], | |
| decision_type: str = "choice", | |
| seed: int = 0, | |
| row_id: str = "0", | |
| ) -> dict[str, Any]: | |
| content, presentation_order, candidate_to_slot = render_decision_prompt( | |
| state=state, | |
| instructions=instructions, | |
| candidates=candidates, | |
| decision_type=decision_type, | |
| decision_tokens=self.decision_tokens, | |
| seed=seed, | |
| row_id=row_id, | |
| prompt_schema=self.config["prompt_schema"], | |
| ) | |
| prompt = self.tokenizer.apply_chat_template( | |
| [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": content}, | |
| ], | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| enable_thinking=self.config.get("decision_prompt_enable_thinking", False), | |
| ) | |
| encoded = self.tokenizer(str(prompt), return_tensors="pt", truncation=False) | |
| device = self.model.device | |
| ids = encoded["input_ids"].to(device) | |
| mask = encoded["attention_mask"].to(device) | |
| with torch.inference_mode(): | |
| logits = self.model(input_ids=ids, attention_mask=mask).logits[0, -1] | |
| logits = logits.index_select(0, self.decision_token_ids.to(device)) | |
| slot_logits = logits[: len(candidates)].float() | |
| slot_probs = torch.softmax(slot_logits, dim=-1) | |
| probabilities = torch.empty_like(slot_probs) | |
| for slot, candidate_index in enumerate(presentation_order): | |
| probabilities[candidate_index] = slot_probs[slot] | |
| return { | |
| "content": content, | |
| "prompt": str(prompt), | |
| "candidate_to_slot": candidate_to_slot, | |
| "slot_logits": slot_logits.detach().cpu().tolist(), | |
| "probabilities": probabilities.detach().cpu().tolist(), | |
| } | |
| __all__ = ["Pluto21", "render_decision_prompt"] | |