granite-system1-350m-experimental / inference_runtime.py
jeharper's picture
Publish experimental canonical Granite 350M decision scorer and frozen evidence
aacd54d verified
Raw History Blame Contribute Delete
9.14 kB
"""Inference-only formula excerpts from the source-bound canonical runtime."""
from dataclasses import dataclass
import hashlib
import json
import re
from pathlib import Path
import torch
from torch import nn
MAX_TOKENS = 32768
TRAIN_TOKENS = 768
def file_sha256(path):
digest = hashlib.sha256()
with Path(path).open('rb') as stream:
for chunk in iter(lambda: stream.read(4 * 1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def audit_loading_info(info, source_keys):
if not isinstance(info, dict) or not all(k in info for k in ('missing_keys', 'unexpected_keys', 'mismatched_keys')):
raise ValueError('missing loading audit fields')
unexpected = set(info['unexpected_keys'])
allowed = {'lm_head.weight'} if 'lm_head.weight' in source_keys else set()
if info['missing_keys'] or info['mismatched_keys'] or info.get('error_msgs') or unexpected - allowed:
raise ValueError(f'base loading audit failed: {info}')
return {'missing_keys': [], 'mismatched_keys': [], 'unexpected_keys': sorted(unexpected),
'allowed_unexpected_keys': sorted(unexpected),
'exception_reason': ('LM vocabulary head is deliberately excluded by AutoModel; source key confirmed'
if unexpected else None)}
def load_base(lock):
"""Only this boundary touches pretrained assets; local and fail-closed."""
from safetensors import safe_open
from transformers import AutoModel, AutoTokenizer
if lock.get('model') != 'ibm-granite/granite-4.0-350m':
raise ValueError('wrong locked model')
if not isinstance(lock.get('revision'), str) or not re.fullmatch('[0-9a-f]{40}', lock['revision']):
raise ValueError('invalid locked revision')
sha = lock.get('model_safetensors_sha256')
if not isinstance(sha, str) or not re.fullmatch('[0-9a-f]{64}', sha):
raise ValueError('invalid source hash')
local = Path(lock['local_path'])
if not local.is_absolute() or lock.get('max_context_tokens') != MAX_TOKENS:
raise ValueError('invalid local path or context lock')
source = local / 'model.safetensors'
actual = file_sha256(source)
if actual != sha:
raise ValueError('source model hash mismatch')
with safe_open(str(source), framework='pt', device='cpu') as handle:
source_keys = set(handle.keys())
model, info = AutoModel.from_pretrained(str(local), local_files_only=True,
trust_remote_code=False, attn_implementation='sdpa',
dtype=torch.float32, output_loading_info=True)
audit = audit_loading_info(info, source_keys)
if model.config.model_type != 'granitemoehybrid' or model.config.hidden_size != 1024:
raise ValueError('unexpected base architecture (requires GraniteMoeHybrid hidden=1024)')
model.to(device='cpu', dtype=torch.float32).eval()
tokenizer = AutoTokenizer.from_pretrained(str(local), local_files_only=True, trust_remote_code=False)
assets = ('config.json', 'tokenizer.json', 'tokenizer_config.json',
'special_tokens_map.json', 'vocab.json', 'merges.txt', 'added_tokens.json', 'tokenizer.model')
asset_hashes = {name: file_sha256(local/name) for name in assets if (local/name).is_file()}
return model, tokenizer, dict(audit, source_sha256=actual, source_file=str(source), asset_sha256=asset_hashes)
def architecture(model):
return {'schema_version': 1, 'backbone_class': type(model.backbone).__name__,
'backbone_config': json.loads(model.backbone.config.to_json_string(use_diff=False)),
'hidden_size': model.backbone.config.hidden_size, 'head_hidden': 128,
'head_activation': 'GELU', 'head_outputs': 1, 'shared_option_head': True,
'separate_none_head': True, 'rank': 8, 'alpha': 16, 'dropout': 0,
'adapter_targets': model.adapter_targets,
'option_readout': 'option_segment_last_token', 'none_readout': 'final_segment_last_token',
'prompt_version': 'context_question_json_options_decision_v1',
'tokenization': 'encode_each_segment_add_special_tokens_false',
'max_input_tokens': MAX_TOKENS, 'train_max_tokens': TRAIN_TOKENS,
'dtype': 'float32', 'device': 'cpu', 'attention': 'sdpa', 'use_cache': False}
def tensor_hash(items):
digest = hashlib.sha256()
for name, tensor in sorted(items):
value = tensor.detach().cpu().contiguous()
digest.update(name.encode())
digest.update(str(value.dtype).encode())
digest.update(str(tuple(value.shape)).encode())
digest.update(value.reshape(-1).view(torch.uint8).numpy().tobytes())
return digest.hexdigest()
def frozen_hash(model):
return tensor_hash([(n, p) for n, p in model.named_parameters() if not p.requires_grad]
+ list(model.named_buffers()))
def trainable_state(model):
return {n: p.detach().cpu().contiguous() for n, p in model.named_parameters() if p.requires_grad}
class LoRALinear(nn.Module):
"""Frozen linear + rank-eight update; zero B preserves initial backbone."""
def __init__(self, base, rank=8, alpha=16):
super().__init__()
if not isinstance(base, nn.Linear):
raise ValueError('LoRA target must be nn.Linear')
self.base = base
self.base.requires_grad_(False)
self.rank, self.alpha = rank, alpha
self.A = nn.Parameter(torch.empty(rank, base.in_features, dtype=base.weight.dtype))
self.B = nn.Parameter(torch.zeros(base.out_features, rank, dtype=base.weight.dtype))
nn.init.kaiming_uniform_(self.A, a=5 ** 0.5)
def forward(self, x):
return self.base(x) + nn.functional.linear(nn.functional.linear(x, self.A), self.B) * (self.alpha / self.rank)
class OptionScorer(nn.Module):
def __init__(self, backbone):
super().__init__()
self.backbone = backbone
self.backbone.requires_grad_(False)
attention = [(i, layer.self_attn) for i, layer in enumerate(backbone.layers)
if hasattr(layer, 'self_attn') and layer.self_attn is not None]
if len(attention) < 4:
raise ValueError('backbone requires at least four attention layers')
self.adapter_targets = []
for index, attn in attention[-4:]:
for name in ('q_proj', 'v_proj'):
setattr(attn, name, LoRALinear(getattr(attn, name)))
self.adapter_targets.append(f'layers.{index}.self_attn.{name}')
hidden = backbone.config.hidden_size
self.option_head = nn.Sequential(nn.Linear(hidden, 128), nn.GELU(), nn.Linear(128, 1))
self.none_head = nn.Sequential(nn.Linear(hidden, 128), nn.GELU(), nn.Linear(128, 1))
self.train(False)
def train(self, mode=True):
super().train(mode)
self.backbone.eval()
for module in self.backbone.modules():
if isinstance(module, LoRALinear):
module.training = mode
return self
def forward(self, encoded):
ids = torch.tensor([encoded.input_ids], dtype=torch.long, device='cpu')
hidden = self.backbone(input_ids=ids, attention_mask=torch.ones_like(ids),
use_cache=False, return_dict=True).last_hidden_state[0]
options = self.option_head(hidden[encoded.option_positions]).squeeze(-1)
none = self.none_head(hidden[encoded.final_position]).reshape(1)
return torch.cat((options, none))
def validate_request(request, *, labeled=False):
if not isinstance(request, dict):
raise ValueError('request must be an object')
for key in ('context', 'question'):
if not isinstance(request.get(key), str) or (key == 'question' and not request[key].strip()):
raise ValueError(f'{key} must be a string (question nonempty)')
options = request.get('options')
if not isinstance(options, list) or not 2 <= len(options) <= 16:
raise ValueError('options must contain 2..16 choices')
ids = []
for option in options:
if not isinstance(option, dict):
raise ValueError('option must be an object')
for key in ('id', 'description'):
if not isinstance(option.get(key), str) or not option[key].strip():
raise ValueError(f'option {key} must be nonempty string')
ids.append(option['id'])
if len(set(ids)) != len(ids):
raise ValueError('option ids must be unique')
if 'target' in request and request['target'] is not None and request['target'] not in ids:
raise ValueError('target must be option id or null')
if labeled:
for key in ('id', 'family'):
if not isinstance(request.get(key), str) or not request[key].strip():
raise ValueError(f'{key} must be nonempty')
if 'target' not in request:
raise ValueError('labeled row requires target')
return request
@dataclass(frozen=True)
class EncodedRequest:
input_ids: list
option_positions: list
final_position: int
text: str