"""ARBITER: Attentive Relational Bidirectional Impartial Typed Encoder for Reasoning. Official inference and decision model with Set-Attention (Inter-Candidate Multi-Head Self-Attention) and Dynamic HyperFiLM modulation. Fully autonomous loading from local safetensors without external downloads. """ from __future__ import annotations import json import logging import os import re from pathlib import Path from typing import Any import torch import torch.nn as nn from safetensors.torch import load_file as load_safetensors_file from transformers import PreTrainedModel, AutoTokenizer try: from transformers.models.embedding_gemma2.modeling_embedding_gemma2 import EmbeddingGemma2TextModel except ImportError: try: from transformers import EmbeddingGemma2TextModel except ImportError: EmbeddingGemma2TextModel = None try: from .configuration_arbiter import ArbiterConfig except (ImportError, ValueError): from configuration_arbiter import ArbiterConfig logger = logging.getLogger(__name__) def _build_mlp(in_dim: int = 512, hidden_dim: int = 256, dropout_p: float = 0.1) -> nn.Sequential: return nn.Sequential( nn.Linear(in_dim, hidden_dim, dtype=torch.float32), nn.GELU(), nn.Dropout(dropout_p), nn.Linear(hidden_dim, 1, dtype=torch.float32), ) class SetTransformerHyperFiLMHead(nn.Module): """ ARBITER Relational Decision Head: Inter-Candidate Multihead Attention + Dynamic HyperFiLM Modulation. Pipeline: 1. Inter-Candidate Multihead Self-Attention: H_attn = H + alpha_attn * MHA(LN(H), LN(H), LN(H)) 2. Dynamic Context & Task Modulation: c = [h_context || e_t] [gamma_raw, beta_raw] = HyperNet(c) gamma = 1.0 + alpha_film * gamma_raw beta = alpha_film * beta_raw H' = gamma * H_attn + beta 3. Final Scoring: z_i = MLP(h'_i) """ def __init__( self, dropout_p: float = 0.1, num_heads: int = 4, cond_dim: int = 512, type_embed_dim: int = 64, hypernet_hidden_dim: int = 128, ): super().__init__() self.head = _build_mlp(512, 256, dropout_p) self.ln = nn.LayerNorm(512, dtype=torch.float32) self.mha = nn.MultiheadAttention( embed_dim=512, num_heads=num_heads, batch_first=True, dtype=torch.float32, ) self.alpha_attn = nn.Parameter(torch.zeros(1, dtype=torch.float32)) self.type_to_id = {"choice": 0, "noul": 1, "score": 2} self.type_embed = nn.Embedding(3, type_embed_dim, dtype=torch.float32) self.hypernet = nn.Sequential( nn.Linear(cond_dim + type_embed_dim, hypernet_hidden_dim, dtype=torch.float32), nn.GELU(), nn.Linear(hypernet_hidden_dim, 512 * 2, dtype=torch.float32), ) self.alpha_film = nn.Parameter(torch.zeros(1, dtype=torch.float32)) def forward( self, hidden: torch.Tensor, qtype: str = "choice", cond_vector: torch.Tensor | None = None, ) -> torch.Tensor: # hidden: [K, 512] h_norm = self.ln(hidden) attn_out, _ = self.mha( h_norm.unsqueeze(0), h_norm.unsqueeze(0), h_norm.unsqueeze(0), need_weights=False, ) attn_out = attn_out.squeeze(0) h_attn = hidden + self.alpha_attn * attn_out t_id = torch.tensor(self.type_to_id.get(qtype, 0), device=hidden.device) e_t = self.type_embed(t_id) if cond_vector is None: cond_vector = hidden.mean(dim=0) c = torch.cat([cond_vector, e_t], dim=-1).unsqueeze(0) film_params = self.hypernet(c) gamma_raw, beta_raw = film_params.chunk(2, dim=-1) gamma = 1.0 + self.alpha_film * gamma_raw beta = self.alpha_film * beta_raw modulated = gamma * h_attn + beta return self.head(modulated).squeeze(-1).float() class ArbiterModel(PreTrainedModel): """ ARBITER Decision Model: - Backbone: EmbeddingGemma-2 Text Encoder (512-dim native hidden states) - Decision Head: ARBITER (Set-Attention + HyperFiLM) - Full unified multi-task support: choice, noul (boolean), score (ordinal) """ config_class = ArbiterConfig base_model_prefix = "arbiter" def __init__(self, config: ArbiterConfig): super().__init__(config) self.config = config if EmbeddingGemma2TextModel is None: raise ImportError( "ARBITER requires transformers>=5.19.0 (with EmbeddingGemma2 support).\n" "Your current environment has an older version of transformers without EmbeddingGemma2.\n" "To resolve this, please upgrade transformers:\n" " pip install --upgrade 'transformers>=5.19.0'\n" "or install the official spectral-arbiter package:\n" " pip install --upgrade spectral-arbiter" ) # Instantiate language model directly from config without downloading Google weights text_config = config.get_text_config() text_config.vocab_size = config.vocab_size self.language_model = EmbeddingGemma2TextModel(text_config) self.language_model.embedding_projection = nn.Identity() # Decision head self.decision_head = SetTransformerHyperFiLMHead( dropout_p=0.1, num_heads=config.num_heads, cond_dim=config.hidden_size, type_embed_dim=config.type_embed_dim, hypernet_hidden_dim=config.hypernet_hidden_dim, ) self.tokenizer = None self.option_token = config.option_token self.option_token_id = config.option_token_id @classmethod def from_pretrained( cls, pretrained_model_name_or_path: str | Path, device: str | torch.device | None = None, dtype: torch.dtype | str | None = None, torch_dtype: torch.dtype | str | None = None, local_files_only: bool = False, **kwargs, ) -> ArbiterModel: """ Load ARBITER from local directory or Hugging Face Hub repo. """ # Handle device_map gracefully for Hugging Face ergonomics device_map = kwargs.pop("device_map", None) if device_map is not None: logger.info( "ARBITER is a compact 272M parameter single-device model. " "Interpreting device_map=%r as single-device placement.", device_map, ) if device is None: if str(device_map).lower() in ("auto", "balanced", "cuda"): device = "cuda:0" if torch.cuda.is_available() else "cpu" elif str(device_map).lower() == "cpu": device = "cpu" elif isinstance(device_map, str) and device_map.startswith("cuda"): device = device_map elif isinstance(device_map, dict): val = next(iter(device_map.values()), "cpu") device = f"cuda:{val}" if isinstance(val, int) else str(val) else: device = "cuda:0" if torch.cuda.is_available() else "cpu" hub_options = {"revision", "cache_dir", "token", "force_download", "local_files_only"} hf_internal_kwargs = {"trust_remote_code", "config", "adapter_kwargs", "code_revision", "proxies"} kwargs = {k: v for k, v in kwargs.items() if not k.startswith("_")} for k in hf_internal_kwargs: kwargs.pop(k, None) unsupported = set(kwargs) - hub_options if unsupported: raise TypeError(f"Unsupported loading arguments: {sorted(unsupported)}. " "Use device or device_map for device placement.") model_path_str = str(pretrained_model_name_or_path) model_path = Path(model_path_str) hub_remote_only = {"revision", "token", "force_download"} if model_path.exists() and any(k in kwargs for k in hub_remote_only): raise TypeError("Hub download arguments apply only to remote repositories, not local paths.") if model_path.exists(): for k in list(kwargs): if k in hub_options: kwargs.pop(k) # Resolve remote repo if path does not exist locally if not model_path.exists(): try: from huggingface_hub import snapshot_download model_path_str = snapshot_download( repo_id=model_path_str, local_files_only=local_files_only, **kwargs, ) model_path = Path(model_path_str) except Exception as e: if local_files_only: raise FileNotFoundError(f"Local files not found for {model_path_str} and local_files_only=True") from e raise # 1. Load config config_file = model_path / "config.json" if config_file.exists(): with open(config_file, "r", encoding="utf-8") as f: config_dict = json.load(f) config = ArbiterConfig(**config_dict) else: config = ArbiterConfig() # 2. Determine target dtype safely dtype_arg = dtype if dtype is not None else torch_dtype if dtype_arg is None or dtype_arg == "auto": cfg_dt = getattr(config, "dtype", None) or getattr(config, "torch_dtype", None) or "bfloat16" if isinstance(cfg_dt, torch.dtype): target_dtype = cfg_dt elif str(cfg_dt).replace("torch.", "").lower() in ("bfloat16", "bf16"): target_dtype = torch.bfloat16 elif str(cfg_dt).replace("torch.", "").lower() in ("float16", "fp16"): target_dtype = torch.float16 elif str(cfg_dt).replace("torch.", "").lower() in ("float32", "fp32"): target_dtype = torch.float32 else: raise ValueError(f"Unsupported dtype in config: {cfg_dt!r}") elif isinstance(dtype_arg, torch.dtype): target_dtype = dtype_arg elif isinstance(dtype_arg, str): clean_dt = dtype_arg.replace("torch.", "").lower() if clean_dt in ("bfloat16", "bf16"): target_dtype = torch.bfloat16 elif clean_dt in ("float16", "fp16"): target_dtype = torch.float16 elif clean_dt in ("float32", "fp32"): target_dtype = torch.float32 else: raise ValueError( f"Unsupported dtype string '{dtype_arg}'. Expected one of 'bfloat16', 'float16', 'float32', or 'auto'." ) else: raise ValueError(f"Unsupported dtype type: {type(dtype_arg)}. Expected torch.dtype or str.") # 3. Instantiate model model = cls(config) # 4. Load weights directly from safetensors or pt safetensors_path = model_path / "model.safetensors" pt_path = model_path / "best_model.pt" if safetensors_path.exists(): state_dict = load_safetensors_file(str(safetensors_path)) elif pt_path.exists(): ckpt = torch.load(str(pt_path), map_location="cpu", weights_only=True) state_dict = ckpt.get("model_state_dict", ckpt) else: # Fallback search for any .safetensors in directory st_files = list(model_path.glob("*.safetensors")) if len(st_files) > 1: raise ValueError("Ambiguous weights: provide a single model.safetensors file; sharded packages are not supported.") if st_files: state_dict = load_safetensors_file(str(st_files[0])) else: raise FileNotFoundError(f"No safetensors or pt weights found in {model_path}") # Cast state_dict to target dtype and load strictly state_dict_converted = {k: v.to(dtype=target_dtype) for k, v in state_dict.items()} model.to(dtype=target_dtype) model.load_state_dict(state_dict_converted, strict=True) # 5. Load tokenizer locally from the directory if present (fully autonomous) has_local_tok = (model_path / "tokenizer.json").exists() or (model_path / "tokenizer_config.json").exists() if has_local_tok: tokenizer = AutoTokenizer.from_pretrained(str(model_path), local_files_only=True) elif not local_files_only: try: tokenizer = AutoTokenizer.from_pretrained(str(model_path)) except Exception: raise FileNotFoundError( f"Tokenizer files not found in {model_path}. Autonomous loading requires tokenizer.json in release directory." ) else: raise FileNotFoundError( f"Tokenizer files (tokenizer.json) not found in {model_path} with local_files_only=True" ) # Strict validation of option marker, token ID and embedding table size if config.option_token not in tokenizer.get_vocab(): raise ValueError( f"Special marker '{config.option_token}' is missing from tokenizer vocabulary. Corrupted or mismatched tokenizer." ) tok_opt_id = tokenizer.convert_tokens_to_ids(config.option_token) if tok_opt_id != config.option_token_id: raise ValueError( f"Option token ID mismatch: tokenizer assigns {tok_opt_id} to '{config.option_token}', " f"but config specifies {config.option_token_id}." ) embed_vocab_size = model.language_model.embed_tokens.weight.shape[0] if embed_vocab_size < len(tokenizer): raise ValueError( f"Embedding table size ({embed_vocab_size}) is smaller than tokenizer vocabulary ({len(tokenizer)})." ) model.tokenizer = tokenizer model.option_token_id = tok_opt_id # Move to target device target_device = device or ("cuda:0" if torch.cuda.is_available() else "cpu") model.to(device=target_device) model.eval() return model def forward( self, input_ids: torch.Tensor, attention_mask: torch.Tensor, option_indices: list[list[int]], question_types: list[str] | None = None, ) -> dict[str, torch.Tensor]: batch_size = input_ids.shape[0] max_options = max((len(opts) for opts in option_indices), default=1) qtypes = question_types if question_types is not None else ["choice"] * batch_size encoder_out = self.language_model( input_ids=input_ids, attention_mask=attention_mask, ) hidden_states = encoder_out.last_hidden_state # [B, seq_len, 512] dev = input_ids.device padded_logits = torch.full((batch_size, max_options), -1e4, device=dev, dtype=torch.float32) mask = torch.zeros((batch_size, max_options), device=dev, dtype=torch.bool) for b in range(batch_size): opts = option_indices[b] k = len(opts) if k == 0: continue opts_tensor = torch.tensor(opts, device=dev, dtype=torch.long) opt_hidden = hidden_states[b, opts_tensor, :] # [k, 512] qtype = qtypes[b] if b < len(qtypes) else "choice" cond_vec = hidden_states[b, 0, :] # [512] at scores = self.decision_head(opt_hidden, qtype, cond_vector=cond_vec) padded_logits[b, :k] = scores mask[b, :k] = True probs = torch.softmax(padded_logits.masked_fill(~mask, -1e4), dim=-1) return { "logits": padded_logits, "probabilities": probs, "mask": mask, } def _prepare_decision( self, context: str, instruction: str, criteria: dict[str, str] | list[str] | None, question_type: str = "choice", max_length: int = 4096, allow_truncation: bool = False, ) -> dict[str, Any]: """ High-level single-query decision interface. Supports choice, noul, and score question types with contract validation. """ if self.tokenizer is None: raise RuntimeError("Model tokenizer is not loaded. Ensure the model was loaded via from_pretrained.") # Input contract validations if not isinstance(context, str) or not isinstance(instruction, str): raise TypeError("Both 'context' and 'instruction' must be strings.") if not context.strip(): raise ValueError("Context cannot be empty.") if not instruction.strip(): raise ValueError("Instruction cannot be empty.") if isinstance(max_length, bool) or not isinstance(max_length, int) or max_length <= 0: raise ValueError("max_length must be a positive integer") if max_length > self.config.max_position_embeddings: raise ValueError( f"Requested max_length ({max_length}) exceeds model max_position_embeddings " f"({self.config.max_position_embeddings})." ) if question_type not in ("choice", "noul", "score"): raise ValueError(f"Invalid question_type '{question_type}'. Must be 'choice', 'noul', or 'score'.") # Canonicalize and validate criteria per question type if question_type == "noul": if criteria is None or (isinstance(criteria, (dict, list)) and len(criteria) == 0): criteria = {"false": "The statement is false.", "true": "The statement is true."} elif isinstance(criteria, dict): raw_keys = list(criteria.keys()) if len(raw_keys) != 2: raise ValueError(f"For question_type='noul', criteria must contain exactly 2 options (got {len(raw_keys)}).") norm_keys = [str(k).lower().strip() for k in raw_keys] if len(set(norm_keys)) != len(norm_keys): raise ValueError( f"Criteria contains conflicting or colliding boolean keys: {raw_keys}. " "Cannot resolve ambiguous duplicate boolean representations." ) if set(norm_keys) != {"false", "true"}: raise ValueError( f"For question_type='noul', criteria dictionary keys must be canonical ('false' and 'true'). " f"Got: {raw_keys}. Arbitrary keys are rejected to prevent semantic inversion." ) clean_dict = {norm_keys[i]: str(criteria[raw_keys[i]]) for i in range(len(raw_keys))} criteria = {"false": clean_dict["false"], "true": clean_dict["true"]} elif isinstance(criteria, (list, tuple)): if len(criteria) != 2: raise ValueError(f"For question_type='noul', criteria must contain exactly 2 options (got {len(criteria)}).") c0 = str(criteria[0]).lower().strip() c1 = str(criteria[1]).lower().strip() if {c0, c1} == {"false", "true"}: desc_f = str(criteria[0]) if c0 == "false" else str(criteria[1]) desc_t = str(criteria[1]) if c1 == "true" else str(criteria[0]) criteria = {"false": desc_f, "true": desc_t} else: raise ValueError( f"For question_type='noul', criteria list items must be canonical ('false', 'true'). " f"Got: {list(criteria)}." ) else: raise TypeError(f"Invalid type for criteria: {type(criteria)}. Expected dict, list, or None.") elif question_type in ("choice", "score"): if criteria is None or len(criteria) < 2: raise ValueError(f"For question_type='{question_type}', criteria must contain at least 2 options.") if isinstance(criteria, dict): raw_keys = list(criteria.keys()) norm_keys = [str(k).strip() for k in raw_keys] if len(set(norm_keys)) != len(norm_keys): raise ValueError(f"Criteria contains duplicate or colliding keys: {raw_keys}") options = [(norm_keys[i], str(criteria[raw_keys[i]])) for i in range(len(raw_keys))] keys = norm_keys elif isinstance(criteria, (list, tuple)): options = [(str(i), str(desc)) for i, desc in enumerate(criteria)] keys = [str(i) for i in range(len(criteria))] else: raise TypeError(f"Invalid type for criteria: {type(criteria)}. Expected dict or list.") if any(self.option_token in key for key in keys): raise ValueError("Criterion keys must not contain the reserved