Skip to content

Commit 982e80c

Browse files
DaoTwentyclaude
andcommitted
Add SamplingSession.score_from_tokens: teacher-forced replay scoring
score_from_tokens(token_ids) computes log P(token_ids | session context) by replaying a previously generated token sequence: one forward pass over [context + tokens], stepping the grammar state token-by-token to apply the same legality masks used at sampling time, and returning per-token log-probs under the masked T=1 distribution. This enables cross-conditional scoring (score a generation made under conditioning A against conditioning B) without re-sampling. Sequences longer than the model's positional window are scored with a sliding window (stride = n_positions/2, each position keeps at least half a window of left context) instead of overflowing the position embeddings; behavior is unchanged when the sequence fits in one window. Generation paths are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 15d57a1 commit 982e80c

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

src/python/midigpt/inference/session.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,151 @@ def _sample_step(self, score: Score, step, temperature: float, seed: int = -1) -
817817

818818
return result
819819

820+
def score_from_tokens(
821+
self,
822+
token_ids: list,
823+
step_idx: int = 0,
824+
use_velocity: int = -1,
825+
) -> dict:
826+
"""Teacher-forced log P(token_ids | context from this session).
827+
828+
Uses a single forward pass over [context_tokens + token_ids], then
829+
steps the grammar state token-by-token to get per-position masks.
830+
Returns log P of each token_id under the masked distribution (T=1).
831+
832+
step_idx: which planner step to use (0 = first/cold-start, which is
833+
the most informative for cross-conditional scoring).
834+
"""
835+
import copy
836+
try:
837+
import torch
838+
except ImportError:
839+
raise ImportError("pip install midigpt[inference]") from None
840+
841+
cfg = self._request.config
842+
mask = self._build_selection_mask()
843+
import json as _json
844+
enc_cfg = self._engine._tokenizer._vocab.config()
845+
try:
846+
dims = sorted(
847+
set(_json.loads(enc_cfg.to_json()).get("num_bars_map") or []),
848+
reverse=True,
849+
)
850+
except Exception:
851+
dims = []
852+
candidates = [d for d in dims if d <= cfg.model_dim] or [cfg.model_dim]
853+
enc_cfg.model_dim = candidates[0]
854+
855+
planner = _core.StepPlanner(
856+
mask, enc_cfg, cfg.bars_per_step, cfg.tracks_per_step
857+
)
858+
steps = list(planner.plan())
859+
if step_idx >= len(steps) or not token_ids:
860+
return {"log_probs": [], "total_logp": 0.0, "n_tokens": 0, "per_type": {}}
861+
step = steps[step_idx]
862+
863+
# Build score + SessionState (minimal setup — no analyzer attributes)
864+
py_score = self._score if isinstance(self._score, Score) else from_cpp(self._score)
865+
score = self._engine._tokenizer.normalize_input(copy.deepcopy(py_score))
866+
867+
# _build_constraints uses self._full_ar_ids; infill scoring has none
868+
self._full_ar_ids = set()
869+
state = _core.SessionState(
870+
to_cpp(score), step,
871+
self._engine._tokenizer._vocab,
872+
self._build_constraints(step),
873+
self._engine._tokenizer._encoder,
874+
self._engine._tokenizer._decoder,
875+
use_span_masks=False,
876+
remove_future_bars=False,
877+
use_velocity=use_velocity, use_microtiming=-1, genre=-1,
878+
)
879+
context_tokens = list(state.context_tokens())
880+
ctx_len = len(context_tokens)
881+
882+
# Prefill over the full sequence [ctx + token_ids]. Generation never
883+
# exceeds the positional window (it stops at the budget), but replay
884+
# scoring can: fall back to sliding-window scoring with stride =
885+
# half window, so every position keeps at least half a window of
886+
# left context.
887+
full_seq = context_tokens + list(token_ids)
888+
dev = next(self._engine._model.parameters()).device
889+
890+
def _forward(seq):
891+
inp = torch.tensor([seq], dtype=torch.long, device=dev)
892+
with torch.no_grad():
893+
try:
894+
out = self._engine._model(inp)
895+
except Exception:
896+
try:
897+
out = self._engine._model(inp, None)
898+
except Exception:
899+
out = self._engine._model(inp.cpu())
900+
if not isinstance(out, tuple):
901+
out = (out,)
902+
return out[0].cpu()[0] # [seq_len, vocab]
903+
904+
n_pos = self._model_max_context()
905+
if len(full_seq) <= n_pos:
906+
_logits_seq = _forward(full_seq)
907+
908+
def _logits_at(p):
909+
return _logits_seq[p]
910+
else:
911+
_stride = max(1, n_pos // 2)
912+
_window_cache: dict[int, torch.Tensor] = {}
913+
914+
def _logits_at(p):
915+
if p < n_pos:
916+
start = 0
917+
else:
918+
# smallest stride-aligned start that keeps >= n_pos - stride
919+
# tokens of left context for position p
920+
start = ((p - n_pos + 1 + _stride - 1) // _stride) * _stride
921+
if start not in _window_cache:
922+
_window_cache[start] = _forward(full_seq[start:start + n_pos])
923+
return _window_cache[start][p - start]
924+
925+
vocab = self._engine._tokenizer._vocab
926+
log_probs: list[float] = []
927+
per_type: dict[str, list] = {}
928+
929+
for i, tid in enumerate(token_ids):
930+
logit_pos = ctx_len - 1 + i
931+
logits = _logits_at(logit_pos)
932+
933+
mask_cpu = torch.as_tensor(state.logit_mask(), dtype=torch.bool)
934+
n_legal = int(mask_cpu.sum().item())
935+
if n_legal > 0:
936+
masked_logits = logits.masked_fill(~mask_cpu, float("-inf"))
937+
else:
938+
masked_logits = logits
939+
940+
log_p_dist = torch.log_softmax(masked_logits, dim=-1)
941+
lp = float(log_p_dist[int(tid)].item())
942+
log_probs.append(lp)
943+
944+
try:
945+
tt = vocab.get_type(int(tid)).name
946+
except Exception:
947+
tt = "?"
948+
per_type.setdefault(tt, []).append(lp)
949+
950+
try:
951+
state.advance(int(tid))
952+
except Exception:
953+
break
954+
955+
return {
956+
"log_probs": log_probs,
957+
"total_logp": float(sum(log_probs)),
958+
"n_tokens": len(log_probs),
959+
"per_type": {
960+
k: {"mean": sum(v) / len(v), "sum": sum(v), "n": len(v)}
961+
for k, v in per_type.items()
962+
},
963+
}
964+
820965
def _snapshot_prompt_state(self, step) -> dict:
821966
"""Per-bar prompt state for the current step, post-mask_bars patch.
822967

0 commit comments

Comments
 (0)