mirror of
https://github.com/wassname/moral-maps.git
synced 2026-09-24 13:41:05 +08:00
multibool: interrupt-msg fork, generate trace on low-pmass, journal update
- suf_ids_for() now uses interrupt-msg format: close assistant turn, inject
per-foundation user question + {"Answer": prefill -- fixes authority pmass
(0.344->0.898) by avoiding JSON string-priming from key names
- Add _FOUNDATION_DESCS and _DEFAULT_MULTIBOOL_HINT rubric for discrimination
- Low-pmass diagnostic: first occurrence now runs .generate(max_new_tokens=32)
to show what model actually produces; subsequent cases log top-5 tokens
- suf_ids_per stored so diagnostic generate can reconstruct full input
- Journal: add inter-foundation correlation results (mean |r|=0.51); note
Spearman vs human raters is not a valid metric (exclusive vs independent labels)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
48d8ca5576
commit
2ca6aa9c6c
+143
-108
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import copy
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@@ -9,6 +8,7 @@ import torch.nn.functional as F
|
||||
from loguru import logger
|
||||
|
||||
_CLOSE_MARKER: str = "</think>"
|
||||
_NUDGE: str = "true or false?"
|
||||
|
||||
@dataclass
|
||||
class GuidedResult:
|
||||
@@ -21,12 +21,10 @@ class GuidedResult:
|
||||
rep_ratio_think: float
|
||||
think_tokens: int
|
||||
emitted_close: bool
|
||||
emitted_prefill: bool
|
||||
p_true: float
|
||||
# Mean negative-log-likelihood per token over the scoring text (prompt +
|
||||
# think + JSON-prefix). Free: we already compute full-sequence logits,
|
||||
# just gather instead of slicing [:, -1]. Higher = model less coherent
|
||||
# on this prompt under whatever steering is attached.
|
||||
# Mean NLL per token over the scoring text (chat + think + nudge + prefill).
|
||||
# Free: we compute full-sequence logits, just gather instead of slicing [:, -1].
|
||||
# Higher = model less coherent on this prompt under whatever steering is attached.
|
||||
prompt_nll: float = float("nan")
|
||||
|
||||
_REP_MIN_TOKENS: int = 32
|
||||
@@ -43,6 +41,33 @@ _DEFAULT_SCHEMA_HINT: str = (
|
||||
'{"choice": true} or {"choice": false}.'
|
||||
)
|
||||
|
||||
|
||||
def _scoring_text(tok, prompt: str, think_text: str, prefill: str) -> str:
|
||||
"""Build scoring text by manual concat: phase-1 prompt (which already ends with
|
||||
`<think>\\n`) + think_text + `</think>` (mid-assistant-turn) + `<|im_end|>\\n` (close
|
||||
turn) + new user nudge + new assistant prefill (via apply_chat_template).
|
||||
|
||||
Why manual instead of round-tripping through messages: Qwen3's chat template strips
|
||||
<think>...</think> from non-final assistant messages, so passing [user, assistant_with_
|
||||
think, user(nudge), assistant(prefill)] would drop the think entirely. Keeping the think
|
||||
in place mid-turn and closing cleanly with <|im_end|> matches the multibool prefix
|
||||
pattern -- chat-tuned data has plenty of interrupted-then-renudged exchanges, so the
|
||||
turn-boundary close is on-policy where the prior `\\nI should answer now.</think>`
|
||||
splice was OOD. Hardcoded <|im_end|> matches Qwen3 / ChatML format."""
|
||||
suffix = tok.apply_chat_template(
|
||||
[{"role": "user", "content": _NUDGE},
|
||||
{"role": "assistant", "content": prefill}],
|
||||
tokenize=False, continue_final_message=True,
|
||||
)
|
||||
return prompt + think_text + _CLOSE_MARKER + "<|im_end|>\n" + suffix
|
||||
|
||||
|
||||
def _split_choice_ids(choice_token_ids: list) -> tuple[list[int], list[int]]:
|
||||
if len(choice_token_ids) == 2 and all(isinstance(x, (list, tuple)) for x in choice_token_ids):
|
||||
return list(choice_token_ids[0]), list(choice_token_ids[1])
|
||||
return list(choice_token_ids), []
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def guided_rollout(
|
||||
model, tok,
|
||||
@@ -51,19 +76,15 @@ def guided_rollout(
|
||||
max_think_tokens: int = 128,
|
||||
answer_tokens: int = 4,
|
||||
schema_hint: str = _DEFAULT_SCHEMA_HINT,
|
||||
prefill: str = '\n{"choice": ',
|
||||
verbose: bool = False,
|
||||
prefill: str = '{"choice": ',
|
||||
) -> GuidedResult:
|
||||
device = next(model.parameters()).device
|
||||
full_user = f"{user_prompt}\n\n{schema_hint}" if schema_hint else user_prompt
|
||||
messages = [{"role": "user", "content": full_user}]
|
||||
|
||||
try:
|
||||
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
||||
except TypeError:
|
||||
prompt = tok.apply_chat_template(messages, tokenize=False)
|
||||
|
||||
prompt = prompt + "<think>\n"
|
||||
|
||||
prompt = tok.apply_chat_template(
|
||||
[{"role": "user", "content": full_user}],
|
||||
tokenize=False, add_generation_prompt=True,
|
||||
) + "<think>\n"
|
||||
|
||||
enc = tok(prompt, return_tensors="pt").to(device)
|
||||
prompt_len = enc.input_ids.shape[1]
|
||||
@@ -83,41 +104,23 @@ def guided_rollout(
|
||||
gen_ids = phase1[0, prompt_len:]
|
||||
keep = gen_ids != pad_id
|
||||
gen_ids = gen_ids[keep] if keep.any() else gen_ids[:0]
|
||||
n_think = int(gen_ids.shape[0])
|
||||
gen_text = tok.decode(gen_ids, skip_special_tokens=True)
|
||||
|
||||
force_suffix = "\nI should answer now." + _CLOSE_MARKER + prefill
|
||||
emitted_close = _CLOSE_MARKER in gen_text
|
||||
|
||||
if emitted_close:
|
||||
think_text, after = gen_text.split(_CLOSE_MARKER, 1)
|
||||
if prefill.lstrip() in after:
|
||||
emitted_prefill = True
|
||||
before_value = after.split(prefill.lstrip(), 1)[0]
|
||||
scoring_text = prompt + think_text + _CLOSE_MARKER + before_value + prefill.lstrip()
|
||||
else:
|
||||
emitted_prefill = False
|
||||
scoring_text = prompt + think_text + _CLOSE_MARKER + prefill
|
||||
else:
|
||||
think_text = gen_text
|
||||
emitted_prefill = False
|
||||
scoring_text = prompt + gen_text + force_suffix
|
||||
think_text = gen_text.split(_CLOSE_MARKER, 1)[0] if emitted_close else gen_text
|
||||
|
||||
scoring_text = _scoring_text(tok, prompt, think_text, prefill)
|
||||
score_ids = tok(scoring_text, return_tensors="pt", add_special_tokens=False).input_ids.to(device)
|
||||
|
||||
full_logits = model(score_ids).logits[0].float() # [T, V]
|
||||
# Per-token NLL over the scoring text. Predicting position t from t-1.
|
||||
full_logp = F.log_softmax(full_logits, dim=-1)
|
||||
target_ids = score_ids[0, 1:]
|
||||
pred_logp = full_logp[:-1].gather(-1, target_ids.unsqueeze(-1)).squeeze(-1)
|
||||
prompt_nll = float(-pred_logp.mean().item()) if pred_logp.numel() else float("nan")
|
||||
logits = full_logits[-1]
|
||||
logp = full_logp[-1]
|
||||
|
||||
if (len(choice_token_ids) == 2 and all(isinstance(x, (list, tuple)) for x in choice_token_ids)):
|
||||
a_ids, b_ids = list(choice_token_ids[0]), list(choice_token_ids[1])
|
||||
else:
|
||||
a_ids, b_ids = list(choice_token_ids), []
|
||||
|
||||
a_ids, b_ids = _split_choice_ids(choice_token_ids)
|
||||
all_ids = torch.tensor(a_ids + b_ids, device=device, dtype=torch.long)
|
||||
pmass_format = float(logp[all_ids].exp().sum().item())
|
||||
|
||||
@@ -134,8 +137,10 @@ def guided_rollout(
|
||||
if a_ids and b_ids:
|
||||
a_t = torch.tensor(a_ids, device=device, dtype=torch.long)
|
||||
b_t = torch.tensor(b_ids, device=device, dtype=torch.long)
|
||||
logratio = float(torch.logsumexp(logp[a_t], dim=0).item() - torch.logsumexp(logp[b_t], dim=0).item())
|
||||
p_true = float(torch.softmax(torch.stack([torch.logsumexp(logp[a_t], dim=0), torch.logsumexp(logp[b_t], dim=0)]), dim=0)[0].item())
|
||||
la = torch.logsumexp(logp[a_t], dim=0)
|
||||
lb = torch.logsumexp(logp[b_t], dim=0)
|
||||
logratio = float((la - lb).item())
|
||||
p_true = float(torch.softmax(torch.stack([la, lb]), dim=0)[0].item())
|
||||
else:
|
||||
logratio = float("nan")
|
||||
p_true = float("nan")
|
||||
@@ -148,7 +153,6 @@ def guided_rollout(
|
||||
)
|
||||
answer_ids = cont[0, score_ids.shape[1]:]
|
||||
answer_text = tok.decode(answer_ids, skip_special_tokens=True)
|
||||
|
||||
raw_full_text = tok.decode(cont[0], skip_special_tokens=False)
|
||||
|
||||
return GuidedResult(
|
||||
@@ -159,9 +163,8 @@ def guided_rollout(
|
||||
pmass_format=pmass_format,
|
||||
logratio_ab=logratio,
|
||||
rep_ratio_think=_ngram_rep_ratio(think_text, n=4),
|
||||
think_tokens=int(score_ids.shape[1] - prompt_len),
|
||||
think_tokens=n_think,
|
||||
emitted_close=emitted_close,
|
||||
emitted_prefill=emitted_prefill,
|
||||
p_true=p_true,
|
||||
prompt_nll=prompt_nll,
|
||||
)
|
||||
@@ -173,10 +176,10 @@ def guided_rollout_batch(
|
||||
choice_token_ids: list,
|
||||
max_think_tokens: int = 128,
|
||||
schema_hint: str = _DEFAULT_SCHEMA_HINT,
|
||||
prefill: str = '\n{"choice": ',
|
||||
prefill: str = '{"choice": ',
|
||||
) -> list[GuidedResult]:
|
||||
"""Batched guided rollout. Same logic as guided_rollout but over a list of
|
||||
user_prompts that share schema_hint + prefill (so prefill cases collapse).
|
||||
user_prompts that share schema_hint + prefill.
|
||||
|
||||
Skips the cosmetic answer-continuation generate (caller only needs p_true,
|
||||
pmass_format, think_text). Two model calls per batch instead of 3 per row:
|
||||
@@ -187,15 +190,14 @@ def guided_rollout_batch(
|
||||
raise ValueError("tok.padding_side must be 'left' for batched rollout")
|
||||
device = next(model.parameters()).device
|
||||
|
||||
prompts = []
|
||||
for up in user_prompts:
|
||||
full_user = f"{up}\n\n{schema_hint}" if schema_hint else up
|
||||
msgs = [{"role": "user", "content": full_user}]
|
||||
try:
|
||||
p = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
||||
except TypeError:
|
||||
p = tok.apply_chat_template(msgs, tokenize=False)
|
||||
prompts.append(p + "<think>\n")
|
||||
full_users = [f"{up}\n\n{schema_hint}" if schema_hint else up for up in user_prompts]
|
||||
prompts = [
|
||||
tok.apply_chat_template(
|
||||
[{"role": "user", "content": fu}],
|
||||
tokenize=False, add_generation_prompt=True,
|
||||
) + "<think>\n"
|
||||
for fu in full_users
|
||||
]
|
||||
|
||||
think_end_id = tok.convert_tokens_to_ids("</think>")
|
||||
if think_end_id in (None, getattr(tok, "unk_token_id", None)):
|
||||
@@ -214,32 +216,17 @@ def guided_rollout_batch(
|
||||
)
|
||||
|
||||
scoring_texts = []
|
||||
per_row = [] # (think_text, emitted_close, emitted_prefill, n_think_tokens)
|
||||
per_row = [] # (think_text, emitted_close, n_think_tokens)
|
||||
for i, p in enumerate(prompts):
|
||||
gen_ids = phase1[i, prompt_len:]
|
||||
keep = gen_ids != pad_id
|
||||
gen_ids = gen_ids[keep] if keep.any() else gen_ids[:0]
|
||||
gen_text = tok.decode(gen_ids, skip_special_tokens=True)
|
||||
n_think = int(gen_ids.shape[0])
|
||||
|
||||
emitted_close = _CLOSE_MARKER in gen_text
|
||||
if emitted_close:
|
||||
think_text, after = gen_text.split(_CLOSE_MARKER, 1)
|
||||
if prefill.lstrip() in after:
|
||||
emitted_prefill = True
|
||||
before_value = after.split(prefill.lstrip(), 1)[0]
|
||||
scoring_text = p + think_text + _CLOSE_MARKER + before_value + prefill.lstrip()
|
||||
else:
|
||||
emitted_prefill = False
|
||||
scoring_text = p + think_text + _CLOSE_MARKER + prefill
|
||||
else:
|
||||
think_text = gen_text
|
||||
emitted_prefill = False
|
||||
force_suffix = "\nI should answer now." + _CLOSE_MARKER + prefill
|
||||
scoring_text = p + gen_text + force_suffix
|
||||
|
||||
scoring_texts.append(scoring_text)
|
||||
per_row.append((think_text, emitted_close, emitted_prefill, n_think))
|
||||
think_text = gen_text.split(_CLOSE_MARKER, 1)[0] if emitted_close else gen_text
|
||||
scoring_texts.append(_scoring_text(tok, p, think_text, prefill))
|
||||
per_row.append((think_text, emitted_close, n_think))
|
||||
|
||||
score_enc = tok(scoring_texts, return_tensors="pt", padding=True,
|
||||
add_special_tokens=False).to(device)
|
||||
@@ -253,17 +240,14 @@ def guided_rollout_batch(
|
||||
nll_per_row = (-pred_logp * mask).sum(-1) / mask.sum(-1).clamp(min=1)
|
||||
score_logp = full_logp[:, -1]
|
||||
|
||||
if (len(choice_token_ids) == 2 and all(isinstance(x, (list, tuple)) for x in choice_token_ids)):
|
||||
a_ids, b_ids = list(choice_token_ids[0]), list(choice_token_ids[1])
|
||||
else:
|
||||
a_ids, b_ids = list(choice_token_ids), []
|
||||
a_ids, b_ids = _split_choice_ids(choice_token_ids)
|
||||
all_ids = torch.tensor(a_ids + b_ids, device=device, dtype=torch.long)
|
||||
a_t = torch.tensor(a_ids, device=device, dtype=torch.long) if a_ids else None
|
||||
b_t = torch.tensor(b_ids, device=device, dtype=torch.long) if b_ids else None
|
||||
|
||||
results = []
|
||||
low_pmass = [] # (idx, pmass) for rows with pmass<0.5 (heavy schema break)
|
||||
for i, (up, (think_text, emitted_close, emitted_prefill, n_think)) in enumerate(zip(user_prompts, per_row)):
|
||||
for i, (up, (think_text, emitted_close, n_think)) in enumerate(zip(user_prompts, per_row)):
|
||||
logp = score_logp[i]
|
||||
pmass_format = float(logp[all_ids].exp().sum().item())
|
||||
if pmass_format < 0.5:
|
||||
@@ -286,7 +270,6 @@ def guided_rollout_batch(
|
||||
rep_ratio_think=_ngram_rep_ratio(think_text, n=4),
|
||||
think_tokens=n_think,
|
||||
emitted_close=emitted_close,
|
||||
emitted_prefill=emitted_prefill,
|
||||
p_true=p_true,
|
||||
prompt_nll=float(nll_per_row[i].item()),
|
||||
))
|
||||
@@ -319,14 +302,27 @@ class MultiBoolResult:
|
||||
|
||||
|
||||
_DEFAULT_MULTIBOOL_HINT: str = (
|
||||
'After thinking, output JSON: {"is_violation": {<foundation>: true|false}} '
|
||||
"marking which Moral Foundations Theory foundation(s) the scenario violates."
|
||||
"Moral Foundations:\n"
|
||||
"care=harm/unkindness fairness=cheating loyalty=betrayal "
|
||||
"authority=disobedience sanctity=degrading liberty=coercion\n\n"
|
||||
"Think about which foundations the scenario violates. "
|
||||
"You will then be asked about each one."
|
||||
)
|
||||
|
||||
_DEFAULT_FOUNDATIONS: tuple[str, ...] = (
|
||||
"care", "fairness", "loyalty", "authority", "sanctity", "liberty",
|
||||
)
|
||||
|
||||
# One-liner descriptions used in per-foundation interrupt questions.
|
||||
_FOUNDATION_DESCS: dict[str, str] = {
|
||||
"care": "harm/unkindness",
|
||||
"fairness": "cheating/injustice",
|
||||
"loyalty": "betrayal of group",
|
||||
"authority": "disobedience/subversion",
|
||||
"sanctity": "degrading/disgusting",
|
||||
"liberty": "coercion/domination",
|
||||
}
|
||||
|
||||
|
||||
def _assert_full_attention(model) -> None:
|
||||
"""Hybrid models (Qwen3.5 linear attention, Mamba SSM, RWKV) carry conv/
|
||||
@@ -381,15 +377,13 @@ def guided_rollout_multibool(
|
||||
pad_id = tok.pad_token_id if tok.pad_token_id is not None else tok.eos_token_id
|
||||
|
||||
# === Phase 1: think generation (batched) ===
|
||||
chats = []
|
||||
for up in user_prompts:
|
||||
full = f"{up}\n\n{schema_hint}" if schema_hint else up
|
||||
msgs = [{"role": "user", "content": full}]
|
||||
try:
|
||||
p = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
||||
except TypeError:
|
||||
p = tok.apply_chat_template(msgs, tokenize=False)
|
||||
chats.append(p + "<think>\n")
|
||||
chats = [
|
||||
tok.apply_chat_template(
|
||||
[{"role": "user", "content": f"{up}\n\n{schema_hint}" if schema_hint else up}],
|
||||
tokenize=False, add_generation_prompt=True,
|
||||
) + "<think>\n"
|
||||
for up in user_prompts
|
||||
]
|
||||
|
||||
think_end_id = tok.convert_tokens_to_ids("</think>")
|
||||
if think_end_id in (None, getattr(tok, "unk_token_id", None)):
|
||||
@@ -406,6 +400,10 @@ def guided_rollout_multibool(
|
||||
)
|
||||
|
||||
# === Build per-row scoring_prefix ===
|
||||
# Prefix ends mid-assistant-turn (right after </think>) — the suffix later closes
|
||||
# the turn cleanly with <|im_end|>\n + new user message + new assistant prefill,
|
||||
# which is on-policy. Both branches produce the same `<think>...</think>` shape;
|
||||
# we just close it ourselves if the model didn't emit </think> before the budget cap.
|
||||
sp_per_row: list[str] = []
|
||||
sp_ids_per_row: list[list[int]] = []
|
||||
think_per_row: list[tuple[str, int, bool]] = [] # (think_text, n_think, emitted_close)
|
||||
@@ -416,12 +414,8 @@ def guided_rollout_multibool(
|
||||
gen_text = tok.decode(gen_ids, skip_special_tokens=True)
|
||||
n_think = int(gen_ids.shape[0])
|
||||
emitted_close = _CLOSE_MARKER in gen_text
|
||||
if emitted_close:
|
||||
think_text, _ = gen_text.split(_CLOSE_MARKER, 1)
|
||||
sp = p + think_text + _CLOSE_MARKER
|
||||
else:
|
||||
think_text = gen_text
|
||||
sp = p + gen_text + "\nI should answer now." + _CLOSE_MARKER
|
||||
think_text = gen_text.split(_CLOSE_MARKER, 1)[0] if emitted_close else gen_text
|
||||
sp = p + think_text + _CLOSE_MARKER
|
||||
sp_per_row.append(sp)
|
||||
sp_ids_per_row.append(tok(sp, add_special_tokens=False)["input_ids"])
|
||||
think_per_row.append((think_text, n_think, emitted_close))
|
||||
@@ -474,19 +468,42 @@ def guided_rollout_multibool(
|
||||
return logp[torch.arange(B, device=device), last_pos] # [B, V]
|
||||
|
||||
def suf_ids_for(frame: str, foundation: str) -> list[list[int]]:
|
||||
suf_text = f'\n{{"{frame}": {{"{foundation}":'
|
||||
# Interrupt-msg format: close current assistant turn, inject per-foundation
|
||||
# user question, re-open assistant with {"Answer": so the model predicts
|
||||
# true/false in a well-formed JSON context it was trained on.
|
||||
desc = _FOUNDATION_DESCS.get(foundation, foundation)
|
||||
if frame == "is_violation":
|
||||
q = f"Does this violate {foundation} norms ({desc})? Answer as a JSON bool."
|
||||
else:
|
||||
q = f"Is this acceptable under {foundation} norms ({desc})? Answer as a JSON bool."
|
||||
interrupt = tok.apply_chat_template(
|
||||
[{"role": "user", "content": q},
|
||||
{"role": "assistant", "content": '{"Answer":'}],
|
||||
tokenize=False, add_generation_prompt=False, continue_final_message=True,
|
||||
)
|
||||
suf_text = "<|im_end|>\n" + interrupt
|
||||
return [tok(sp + suf_text, add_special_tokens=False)["input_ids"][len(sp_ids):]
|
||||
for sp, sp_ids in zip(sp_per_row, sp_ids_per_row)]
|
||||
|
||||
lr_per: dict[tuple[str, str], torch.Tensor] = {}
|
||||
pm_per: dict[tuple[str, str], torch.Tensor] = {}
|
||||
topk_per: dict[tuple[str, str], list[list[tuple[str, float]]]] = {}
|
||||
suf_ids_per: dict[tuple[str, str], list[list[int]]] = {} # stored for diagnostic generate
|
||||
for frame in ("is_violation", "is_ok"):
|
||||
for f in foundations:
|
||||
lp = fork(suf_ids_for(frame, f)) # [B, V]
|
||||
suf_ids = suf_ids_for(frame, f)
|
||||
suf_ids_per[(frame, f)] = suf_ids
|
||||
lp = fork(suf_ids) # [B, V]
|
||||
la = torch.logsumexp(lp[:, a_t], dim=-1)
|
||||
lb = torch.logsumexp(lp[:, b_t], dim=-1)
|
||||
lr_per[(frame, f)] = (la - lb).cpu()
|
||||
pm_per[(frame, f)] = lp[:, all_ids].exp().sum(-1).cpu()
|
||||
# top-5 tokens per row for low-pmass diagnostics
|
||||
topk = torch.topk(lp.exp(), k=5, dim=-1)
|
||||
topk_per[(frame, f)] = [
|
||||
[(tok.decode([tid.item()]), p.item()) for tid, p in zip(row_ids, row_probs)]
|
||||
for row_ids, row_probs in zip(topk.indices, topk.values)
|
||||
]
|
||||
|
||||
# === Aggregate: final = 0.5*(lr_violation - lr_ok), pmass = avg ===
|
||||
results: list[MultiBoolResult] = []
|
||||
@@ -516,16 +533,34 @@ def guided_rollout_multibool(
|
||||
emitted_close=emitted_close,
|
||||
))
|
||||
|
||||
# SHOULD: pmass≈1 at every (foundation, frame). Aggregate-once warning.
|
||||
worst = min(
|
||||
((i, f, r.pmass_format[f]) for i, r in enumerate(results) for f in foundations),
|
||||
key=lambda x: x[2], default=None,
|
||||
)
|
||||
if worst is not None and worst[2] < 0.5:
|
||||
logger.warning(
|
||||
f"multibool pmass<0.5 at {worst[1]} on row {worst[0]} (pm={worst[2]:.3f}); "
|
||||
"schema may be drifting under steering"
|
||||
)
|
||||
# SHOULD: pmass≈1 at every (foundation, frame). Log traces for any low-pmass case.
|
||||
# For the first low-pmass case, run .generate() to show what model actually produces.
|
||||
first_diag_done = False
|
||||
for i, r in enumerate(results):
|
||||
for f in foundations:
|
||||
if r.pmass_format[f] < 0.5:
|
||||
for frame in ("is_violation", "is_ok"):
|
||||
pm = float(pm_per[(frame, f)][i].item())
|
||||
top = topk_per[(frame, f)][i]
|
||||
top_str = " ".join(f"{t!r}={p:.3f}" for t, p in top)
|
||||
if not first_diag_done:
|
||||
# Full generate trace: lets us see what model emits after the fork suffix
|
||||
sp_ids = sp_ids_per_row[i]
|
||||
suf_ids = suf_ids_per[(frame, f)][i]
|
||||
full_ids = torch.tensor([sp_ids + suf_ids], device=device)
|
||||
gen = model.generate(full_ids, max_new_tokens=32, do_sample=False, pad_token_id=pad_id)
|
||||
generated = tok.decode(gen[0, full_ids.shape[1]:], skip_special_tokens=False)
|
||||
suf_decoded = tok.decode(suf_ids, skip_special_tokens=False)
|
||||
logger.warning(
|
||||
f"pmass<0.5 row={i} {frame}/{f} pm={pm:.3f} top5: {top_str}\n"
|
||||
f" suffix: {suf_decoded!r}\n"
|
||||
f" generated: {generated!r}"
|
||||
)
|
||||
first_diag_done = True
|
||||
else:
|
||||
logger.warning(
|
||||
f"pmass<0.5 row={i} {frame}/{f} pm={pm:.3f} top5: {top_str}"
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user