Download source/src/strata/eval/scoring.py from nur-dev/strata-headquotient-q25: direct link, hf CLI and curl.
- Browser
- Download file 4.67 kB
-
https://huggingface.co/nur-dev/strata-headquotient-q25/resolve/main/source/src/strata/eval/scoring.py
- Command line
-
hf download hf://nur-dev/strata-headquotient-q25/source/src/strata/eval/scoring.py
-
curl -L -o scoring.py https://huggingface.co/nur-dev/strata-headquotient-q25/resolve/main/source/src/strata/eval/scoring.py
4.67 kB
| """Causal-LM sequence scoring used by the zero-shot evaluation tasks. | |
| The core primitive returns, for each input sequence, the model's summed (or | |
| length-normalised) log-probability over a scored token region. Grammaticality | |
| (minimal pairs) scores the whole sentence; multiple-choice scores only the | |
| continuation tokens after a shared context. | |
| Batched scoring is exact: sequences are right-padded, and only real, | |
| in-region target positions contribute. Causal attention means real tokens never | |
| attend to right-padding, so per-token scores are identical to unpadded scoring | |
| (verified in tests). | |
| """ | |
| from __future__ import annotations | |
| import torch | |
| def common_prefix_len(a: list[int], b: list[int]) -> int: | |
| n = 0 | |
| for x, y in zip(a, b): | |
| if x != y: | |
| break | |
| n += 1 | |
| return n | |
| def encode_with_continuation(tokenizer, context_text: str, continuation_text: str) -> tuple[list[int], int]: | |
| """Tokenise ``context_text + continuation_text`` and return ``(ids, context_len)``. | |
| ``context_len`` is the number of leading tokens that belong to the context | |
| (and are therefore *not* scored). Computed as the shared token prefix between | |
| the context alone and the full string, which is robust to subword-merge | |
| effects at the boundary. | |
| """ | |
| context_ids = list(tokenizer.encode(context_text, add_bos=True).input_ids) | |
| full_ids = list(tokenizer.encode(context_text + continuation_text, add_bos=True).input_ids) | |
| context_len = common_prefix_len(context_ids, full_ids) | |
| # Guarantee at least one scored token and a non-empty context. | |
| context_len = max(1, min(context_len, len(full_ids) - 1)) | |
| return full_ids, context_len | |
| def _autocast(device_type: str, precision: str): | |
| if precision == "fp32" or device_type == "cpu": | |
| return torch.autocast(device_type=device_type, enabled=False) | |
| dtype = torch.bfloat16 if precision == "bf16" else torch.float16 | |
| return torch.autocast(device_type=device_type, dtype=dtype) | |
| def score_sequences( | |
| model, | |
| sequences: list[list[int]], | |
| context_lens: list[int], | |
| *, | |
| device: torch.device, | |
| pad_id: int = 0, | |
| batch_size: int = 16, | |
| precision: str = "bf16", | |
| length_normalize: bool = False, | |
| predicate_memory_intervention: str = "none", | |
| predicate_memory_residual_scale: float | None = None, | |
| graph_object_residual_scale: float | None = None, | |
| ) -> list[float]: | |
| """Return a score per sequence: summed log-prob over positions ``[context_len, L)``. | |
| With ``length_normalize=True`` the sum is divided by the number of scored | |
| tokens (recommended for comparing continuations of different lengths). | |
| """ | |
| if len(sequences) != len(context_lens): | |
| raise ValueError("sequences and context_lens must have equal length") | |
| model.eval() | |
| device_type = device.type | |
| scores: list[float] = [] | |
| for start in range(0, len(sequences), batch_size): | |
| chunk = sequences[start : start + batch_size] | |
| chunk_ctx = context_lens[start : start + batch_size] | |
| max_len = max(len(seq) for seq in chunk) | |
| batch = torch.full((len(chunk), max_len), pad_id, dtype=torch.long) | |
| real_len = torch.empty(len(chunk), dtype=torch.long) | |
| ctx = torch.tensor(chunk_ctx, dtype=torch.long) | |
| for i, seq in enumerate(chunk): | |
| batch[i, : len(seq)] = torch.tensor(seq, dtype=torch.long) | |
| real_len[i] = len(seq) | |
| batch = batch.to(device) | |
| attention_mask = (batch != pad_id).long() | |
| with _autocast(device_type, precision): | |
| logits = model( | |
| batch, | |
| attention_mask=attention_mask, | |
| predicate_memory_intervention=predicate_memory_intervention, | |
| predicate_memory_residual_scale=predicate_memory_residual_scale, | |
| graph_object_residual_scale=graph_object_residual_scale, | |
| ).logits | |
| logprobs = torch.log_softmax(logits[:, :-1, :].float(), dim=-1) | |
| targets = batch[:, 1:] | |
| token_lp = logprobs.gather(-1, targets.unsqueeze(-1)).squeeze(-1) # [B, S-1] | |
| # Target at shifted index j predicts absolute position (j+1). Score it | |
| # when context_len <= j+1 < real_len. | |
| positions = torch.arange(1, max_len, device=device).unsqueeze(0) # [1, S-1] | |
| ctx = ctx.to(device).unsqueeze(1) | |
| rl = real_len.to(device).unsqueeze(1) | |
| mask = (positions >= ctx) & (positions < rl) | |
| summed = (token_lp * mask).sum(dim=1) | |
| counts = mask.sum(dim=1).clamp(min=1) | |
| result = summed / counts if length_normalize else summed | |
| scores.extend(result.tolist()) | |
| return scores | |