ZibinDong's picture
Publish audited ActionCodec2 release files
1bfc3f0 verified
Raw History Blame
4.56 kB
"""Explicit ActionCodec2Sparse token constraints for Hugging Face generate()."""
from __future__ import annotations
import torch
from transformers import LogitsProcessor
from ...backends.actioncodec2.cache import BoundedCache
class ActionCodec2SparseLogitsProcessor(LogitsProcessor):
"""Constrain generated action IDs to the sparse grammar; allow EOS once complete."""
def __init__(
self, codec, horizon, *, fps, prompt_length, eos_token_id, token_offset=0, pad_token_id=None
):
for name, value in {
"prompt_length": prompt_length,
"eos_token_id": eos_token_id,
"token_offset": token_offset,
}.items():
if type(value) is not int or value < 0:
raise ValueError(f"{name} must be a nonnegative integer")
if pad_token_id is not None and (type(pad_token_id) is not int or pad_token_id < 0):
raise ValueError("pad_token_id must be a nonnegative integer")
self.grammar = codec.grammar(horizon, fps=fps)
self.prompt_length, self.token_offset = prompt_length, token_offset
self.eos_token_id, self.pad_token_id = eos_token_id, pad_token_id
stop = token_offset + self.grammar.vocab_size
if token_offset <= eos_token_id < stop:
raise ValueError("eos_token_id must be outside the action token interval")
if pad_token_id is not None and token_offset <= pad_token_id < stop:
raise ValueError("pad_token_id must be outside the action token interval")
self._mask_cache = BoundedCache(64)
def _state(self, row):
if self.eos_token_id in row:
end = row.index(self.eos_token_id)
if any(t not in (self.eos_token_id, self.pad_token_id) for t in row[end:]):
raise ValueError("only EOS/padding may follow completed action generation")
if not self.grammar.is_complete([t - self.token_offset for t in row[:end]]):
raise ValueError("EOS occurred before the action chunk was complete")
return "done"
state = self.grammar._consume([t - self.token_offset for t in row])
if state is None:
raise ValueError(
"generated prefix violates the action grammar; check prompt_length and token_offset"
)
return state
def _mask(self, state, scores):
key = (state, str(scores.device), scores.shape[-1])
mask = self._mask_cache.get(key)
if mask is None:
mask = torch.zeros(scores.shape[-1], device=scores.device, dtype=torch.bool)
if state == "done":
mask[self.eos_token_id] = True
if self.pad_token_id is not None:
mask[self.pad_token_id] = True
else:
allowed = self.grammar._mask_from_state(state)
stop = self.token_offset + len(allowed)
mask[self.token_offset : stop] = torch.as_tensor(allowed, device=scores.device)
mask[self.eos_token_id] = self.grammar._state_complete(state)
self._mask_cache[key] = mask
return mask
def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
if input_ids.ndim != 2 or scores.ndim != 2 or input_ids.shape[0] != scores.shape[0]:
raise ValueError("expected input_ids(B,L) and scores(B,V) with the same batch size")
if input_ids.shape[1] < self.prompt_length:
raise ValueError("prompt_length exceeds the supplied input length")
required = max(
self.token_offset + self.grammar.vocab_size,
self.eos_token_id + 1,
0 if self.pad_token_id is None else self.pad_token_id + 1,
)
if scores.shape[1] < required:
raise ValueError("model vocabulary does not cover configured action/EOS/padding IDs")
self.grammar.reserve_states(int(input_ids.shape[0]))
rows = input_ids[:, self.prompt_length :].detach().cpu().tolist()
output = scores.clone()
groups = {}
for index, row in enumerate(rows):
groups.setdefault(self._state(row), []).append(index)
for state, indices in groups.items():
output[indices] = scores[indices].masked_fill(~self._mask(state, scores), -torch.inf)
if not torch.isfinite(output).any(dim=1).all():
raise ValueError(
"all legal action logits were suppressed; check other generation processors"
)
return output