squad-0.8b / qev_fast /encoder.py
qmichalwarda's picture
squad-0.8b 2026-10-26: retrained e2e expert (real web-app data, held-out tests 29/82 vs 17/82), prefix cache in the runtime
091795c verified
Raw History Blame Contribute Delete
6.32 kB
"""A fast, token-identical System One prompt encoder (qev_fast.systemone.encode without decider's per-piece
transformers calls).
decider.prompt.build (state-first, plain layout, no shuffling, max_options = MAX_OPTIONS) encodes, per request:
- "Context:\\n" + the rendered state, as one string;
- per question row k, either one string (<= NARROW options: header, "\\n(A) option" lines, answer tail), or (wide) the
header, then per option the ids of "\\n(" + its label token + the ids of ") option", then the answer tail.
FastEncoder builds exactly those strings (decider's own render_state / render_question / plan_rows and
systemone.neutralize_options), encodes all strings of all requests of a call in one Rust `encode_batch` of the
tokenizer backend (the same tokenizer object transformers' fast tokenizer wraps, so the same ids), caches the strings
that repeat (option texts, whole narrow question blocks of fixed schemas) and assembles ids / slots / nopts / golds.
Anything it does not model (more than MAX_OPTIONS options) goes to the reference encoder; `stats` counts both.
"""
import json
from decider.prompt import LETTERS, MAX_OPTIONS, NARROW, label_table
from decider.systemone import plan_rows, render_question, render_state, row_types
from . import systemone as S1
class FastEncoder:
def __init__(self, tok, cache_size: int = 200_000):
self.tok = tok
self.raw = tok.backend_tokenizer # tokenizers.Tokenizer (Rust)
_, self.lab_ids, self.open_ids = label_table(tok)
self.cache: dict = {}
self.cache_size = cache_size
self.stats = {"fast": 0, "fallback": 0, "cache_hits": 0, "strings_encoded": 0}
# ---- planning: the strings decider.prompt.build encodes ---------------------------------------------------------
def _plan(self, state, questions: dict):
ctx = render_state(state) # as S1.encode (callers pass a string or what render_state accepts)
rqs = {k: render_question(v) for k, v in questions.items()}
flat, index = plan_rows(rqs, False)
multi = len(flat) > 1
parts = [("s", "Context:\n" + ctx, False)] # (kind, string, cacheable)
rows = []
for k, r in enumerate(flat):
opts = S1.neutralize_options(r["options"])
if len(opts) > MAX_OPTIONS:
return None # option sampling (decider keeps the gold + abstain options): the reference handles it
head = f"\n\nQuestion{' ' + str(k + 1) if multi else ''}: {r['question']}\nOptions:"
tail = f"\nAnswer{' ' + str(k + 1) if multi else ''}: ("
if len(opts) <= NARROW:
parts.append(("s", head + "".join(f"\n({LETTERS[j]}) {o}" for j, o in enumerate(opts)) + tail, True))
rows.append(("n", len(parts) - 1, len(opts)))
else:
h = len(parts)
parts.append(("s", head, True))
for o in opts:
parts.append(("s", f") {o}", True))
parts.append(("s", tail, True))
rows.append(("w", h, len(opts)))
return rqs, index, parts, rows
# ---- encoding -----------------------------------------------------------------------------------------------
def encode_many(self, requests: list) -> list:
"""[(state, questions)] -> per request S1.encode's dict (+ "golds"), or an exception instance for a request
the prompt builder refuses (ValueError / KeyError / TypeError / AssertionError, as S1.encode raises)."""
plans, todo = [], {}
for state, qs in requests:
try:
p = self._plan(state, qs)
except (ValueError, KeyError, TypeError, AssertionError) as e:
plans.append(e)
continue
plans.append(p if p is not None else ("fallback", state, qs))
if p is not None:
for _kind, s, cacheable in p[2]:
if not (cacheable and s in self.cache):
todo[s] = None
else:
self.stats["cache_hits"] += 1
if todo:
strs = list(todo)
enc = self.raw.encode_batch(strs, add_special_tokens=False)
for s, e in zip(strs, enc, strict=True):
todo[s] = e.ids
self.stats["strings_encoded"] += len(strs)
out = []
for p in plans:
if isinstance(p, Exception):
out.append(p)
continue
if p[0] == "fallback":
self.stats["fallback"] += 1
try:
e = S1.encode(self.tok, p[1], p[2])
e["golds"] = None
out.append(e)
except (ValueError, KeyError, TypeError, AssertionError) as ex:
out.append(ex)
continue
rqs, index, parts, rows = p
def ids_of(i):
s, cacheable = parts[i][1], parts[i][2]
v = todo.get(s)
if v is None:
v = self.cache[s]
elif cacheable and len(self.cache) < self.cache_size:
self.cache[s] = v
return v
ids = list(ids_of(0))
state_len = len(ids) # "Context:\n" + the state, encoded as one string: the row's state prefix
slots, nopts = [], []
for kind, i, n in rows:
if kind == "n":
ids += ids_of(i)
else:
ids += ids_of(i)
for j in range(n):
ids += self.open_ids
ids.append(self.lab_ids[j])
ids += ids_of(i + 1 + j)
ids += ids_of(i + 1 + n)
slots.append(len(ids) - 1)
nopts.append(n)
self.stats["fast"] += 1
out.append({"ids": ids, "slots": slots, "nopts": nopts, "golds": [0] * len(rows), "rqs": rqs,
"index": index, "types": row_types(rqs, index), "state_len": state_len})
return out
def encode(self, state, questions: dict) -> dict:
e = self.encode_many([(state, questions)])[0]
if isinstance(e, Exception):
raise e
return e