ActionCodec2 / runtime /backends /actioncodec2 /bpe_vocabulary.py
ZibinDong's picture
Publish audited ActionCodec2 release files
1bfc3f0 verified
Raw History Blame Contribute Delete
6.68 kB
'Runtime plain-BPE vocabulary over the row-major flattened cell stream.'
from __future__ import annotations
import heapq
from collections.abc import Sequence
from dataclasses import dataclass
import numpy as np
from ._validation import active_dimensions
from .native_serialization import NativeCacheMixin, decode_native, encode_native
__all__ = ["BPEVocabulary"]
@dataclass
class BPEVocabulary(NativeCacheMixin):
"""Primitive atoms plus ordered, adjacency-based merge rules."""
bins: tuple[int, ...]
merges: tuple[tuple[int, int], ...]
def __post_init__(self) -> None:
if not self.bins or any(count < 1 for count in self.bins):
raise ValueError("every dimension needs at least one bin")
self.offsets = np.concatenate(([0], np.cumsum(np.asarray(self.bins, dtype=np.int64))))
self.atom_count = int(self.offsets[-1])
self.vocab_size = self.atom_count + len(self.merges)
atom_dims = np.repeat(
np.arange(len(self.bins), dtype=np.int64), np.asarray(self.bins, dtype=np.int64)
)
expansions: list[tuple[int, ...]] = [(token,) for token in range(self.atom_count)]
for rank, (left, right) in enumerate(self.merges):
token = self.atom_count + rank
if not 0 <= left < token or not 0 <= right < token:
raise ValueError("a merge rule must reference already-created tokens")
expansions.append(expansions[left] + expansions[right])
self.expansions = tuple(expansions)
self.token_dims = tuple(
tuple(int(atom_dims[atom]) for atom in expansion) for expansion in expansions
)
self.token_cells = np.asarray([len(expansion) for expansion in expansions], dtype=np.int64)
self._rules = {
(int(left), int(right)): (self.atom_count + rank, rank)
for rank, (left, right) in enumerate(self.merges)
}
@property
def dimension(self) -> int:
return len(self.bins)
def atom(self, dimension: int, value: int) -> int:
return int(self.offsets[dimension]) + int(value)
def _flatten(self, values: np.ndarray, dimensions: tuple[int, ...]) -> list[int]:
counts = np.asarray(self.bins, dtype=np.int64)
if np.any(values < -1) or np.any(values >= counts[None, :]):
raise ValueError("cells contain an invalid active or absent index")
support = values >= 0
if np.any(support != support[:1]):
raise ValueError("active dimensions must remain constant within a trajectory")
present = tuple(int(dim) for dim in np.flatnonzero(support[0]))
if present != dimensions:
raise ValueError("cells do not activate exactly the requested dimensions")
if not present:
raise ValueError("every control step needs at least one active coordinate")
active = values[:, list(present)]
base = np.asarray([int(self.offsets[dim]) for dim in present], dtype=np.int64)
return (active + base[None, :]).reshape(-1).tolist()
def encode(self, cells: np.ndarray) -> list[int]:
'Apply the merges greedily in creation-rank order to one trajectory.'
values = np.asarray(cells, dtype=np.int64)
if values.ndim != 2 or len(values) < 1 or values.shape[1] < 1:
raise ValueError("cells must have non-empty shape [T, D]")
dimensions = tuple(int(dim) for dim in np.flatnonzero(values[0] >= 0))
stream = self._flatten(values, dimensions)
result = encode_native(self, values[None])
if result is not None:
return result[0]
count = len(stream)
previous = list(range(-1, count - 1))
following = list(range(1, count + 1))
following[-1] = -1
alive = [True] * count
rules = self._rules
heap: list[tuple[int, int]] = []
for index in range(count - 1):
rule = rules.get((stream[index], stream[index + 1]))
if rule is not None:
heap.append((rule[1], index))
heapq.heapify(heap)
while heap:
rank, index = heapq.heappop(heap)
if not alive[index]:
continue
follower = following[index]
if follower < 0 or not alive[follower]:
continue
rule = rules.get((stream[index], stream[follower]))
if rule is None or rule[1] != rank:
continue
stream[index] = rule[0]
alive[follower] = False
after = following[follower]
following[index] = after
if after >= 0:
previous[after] = index
successor = rules.get((stream[index], stream[after]))
if successor is not None:
heapq.heappush(heap, (successor[1], index))
before = previous[index]
if before >= 0:
predecessor = rules.get((stream[before], stream[index]))
if predecessor is not None:
heapq.heappush(heap, (predecessor[1], before))
return [stream[index] for index in range(count) if alive[index]]
def decode(
self,
tokens: Sequence[int],
*,
active_dims: Sequence[int] | None = None,
) -> np.ndarray:
"""Expand tokens to dense cells, writing ``-1`` in absent coordinates."""
dimensions = active_dimensions(active_dims, self.dimension)
result = decode_native(self, [[int(token) for token in tokens]], dimensions)
if result is not None:
return result[0]
atoms: list[int] = []
for index, raw_token in enumerate(tokens):
token = int(raw_token)
if not 0 <= token < self.vocab_size:
raise ValueError(f"tokens[{index}] is outside the vocabulary")
atoms.extend(self.expansions[token])
width = len(dimensions)
if not atoms or len(atoms) % width:
raise ValueError("token stream does not cover a whole number of routed control steps")
values = np.asarray(atoms, dtype=np.int64).reshape(-1, width)
starts = np.asarray([int(self.offsets[dim]) for dim in dimensions], dtype=np.int64)
stops = np.asarray([int(self.offsets[dim + 1]) for dim in dimensions], dtype=np.int64)
if np.any(values < starts[None, :]) or np.any(values >= stops[None, :]):
raise ValueError("token stream does not follow the routed dimension order")
cells = np.full((len(values), self.dimension), -1, dtype=np.int64)
cells[:, list(dimensions)] = values - starts[None, :]
return cells