decision-2b-preview / runtime /scripts /native_scoring.py
denti's picture
Publish frozen native-pointer evaluation preview
df57b75 verified
Raw History Blame Contribute Delete
4.48 kB
"""Option likelihoods under one prefix, with the prefix computed once.
The likelihood readout scores each option's text as a continuation of the same chat-template prompt. The
first implementation re-ran the whole prompt for every option, so a hard item with a 1,400-token prompt and
four options put about 5,600 tokens through the model while the report counted 1,400. Here the prompt goes
through once, its key/value cache is reused for every option, and `processed_tokens` is exactly what the
model saw: the prompt plus the option continuations. The numbers are the same as the uncached computation
(`tests/test_native_scoring.py`); only the work and the honest token count change.
"""
import copy
import math
def score_options(model, prefix_ids, pieces, device, max_tokens, batch=8):
"""(probabilities, processed_tokens) over the options, or (None, widest) when an option would not fit.
Probabilities are the softmax of the length-normalised log-likelihood of each option's tokens given the
prefix. `pieces` are the options' token ids; options of equal length are scored in one batch of `batch`.
"""
import torch
from transformers import DynamicCache
if not pieces or any(not piece for piece in pieces):
raise ValueError('every option needs at least one token')
widest = len(prefix_ids) + max(len(piece) for piece in pieces)
if widest > max_tokens:
return None, widest
totals = [None] * len(pieces)
with torch.no_grad():
prefix = torch.tensor([list(prefix_ids)], device=device)
out = model(input_ids=prefix, past_key_values=DynamicCache(), use_cache=True, logits_to_keep=1)
first = torch.log_softmax(out.logits[0, -1].float(), dim=-1) # predicts each option's first token
cache = out.past_key_values
by_length = {}
for i, piece in enumerate(pieces):
by_length.setdefault(len(piece), []).append(i)
for length, indices in by_length.items():
for start in range(0, len(indices), batch):
chunk = indices[start:start + batch]
ids = torch.tensor([pieces[i] for i in chunk], device=device)
shared = copy.deepcopy(cache)
if len(chunk) > 1:
shared.batch_repeat_interleave(len(chunk))
logprobs = None
if length > 1:
logits = model(input_ids=ids, past_key_values=shared, use_cache=True).logits.float()
logprobs = torch.log_softmax(logits, dim=-1) # position j predicts token j + 1
for row, i in enumerate(chunk):
piece = pieces[i]
total = float(first[piece[0]])
for j in range(1, length):
total += float(logprobs[row, j - 1, piece[j]])
totals[i] = total
means = [t / len(piece) for t, piece in zip(totals, pieces)]
top = max(means)
weights = [math.exp(m - top) for m in means]
processed = len(prefix_ids) + sum(len(piece) for piece in pieces)
return [w / sum(weights) for w in weights], processed
def score_options_uncached(model, prefix_ids, pieces, device, max_tokens, batch=8):
"""The original computation, kept as the reference the cached one is tested against."""
import torch
widest = len(prefix_ids) + max(len(piece) for piece in pieces)
if widest > max_tokens:
return None, widest
totals = [None] * len(pieces)
by_length = {}
for i, piece in enumerate(pieces):
by_length.setdefault(len(piece), []).append(i)
prefix_tensor = torch.tensor(list(prefix_ids))
for length, indices in by_length.items():
for start in range(0, len(indices), batch):
chunk = indices[start:start + batch]
ids = torch.stack([torch.cat([prefix_tensor, torch.tensor(pieces[i])]) for i in chunk]).to(device)
with torch.no_grad():
logits = model(input_ids=ids, use_cache=False, logits_to_keep=length + 1).logits.float()
logprobs = torch.log_softmax(logits, dim=-1)
for row, i in enumerate(chunk):
totals[i] = float(sum(logprobs[row, j, token] for j, token in enumerate(pieces[i])))
means = [t / len(piece) for t, piece in zip(totals, pieces)]
top = max(means)
weights = [math.exp(m - top) for m in means]
return [w / sum(weights) for w in weights], sum(len(prefix_ids) + len(piece) for piece in pieces)