12 KiB
fable.md — research-direction notes from implementation
Notes written while building the IOL-AI solver seed (see AGENTS.md for the plan, README.md for repo state). Each item is something the implementation surfaced that the research plan should absorb.
Update (real-data phase): items 4, 8, 9 are now RESOLVED — the official eval notebook (rita-berrada/iolai-2026-workshop) fixed the semantics, and facebook/linguini gave 160 real dev puzzles. New findings start at §11.
11. Real-data baseline resets expectations (and the plan's emphasis)
Symbolic-only on all 160 Linguini puzzles: EM 2.4%, chrF 20, in ~9 s. (The synthetic dev set scored 1.0 — constructed puzzles flatter substitution methods.) Real translation items are compositional, real paradigms involve metathesis/harmony/infixation, real numeral systems have morphophonology that breaks token-level CSP. Consequences:
- The LLM direct path (notebook format) is the main score carrier for now; the symbolic stack's near-term value is (a) exact hits where structure is clean, (b) the chrF floor, (c) verified confidences that route the LLM budget (157/160 puzzles flagged low-confidence — correctly).
- The DSL/CEGIS path is the ceiling-raiser, not the floor: its job is to convert LLM linguistic insight into verified exact hits. Ablate it against direct-LLM on the T4 before investing in finetuning.
12. Official semantics differ from my assumptions in scoring-relevant ways
- EM is
strip().lower()ONLY — final punctuation and internal spacing are significant. Answers must carry the gold's punctuation conventions; a format-inducer pass (§3) is now demonstrably worth EM points. - Gold items can be a LIST OF ALTERNATIVES (14/160 rows); scorer takes max.
- chrF is sacrebleu with effective-order smoothing; my reimplementation now matches to 0.0 delta over 500 fuzz cases (tests/test_scorer.py).
13. Linguini formats: what the parser must survive (now does, 158/160)
Pipe tables 2-5 columns with header rows; numbered example sentences whose numbering the query CONTINUES (item "17." refers to nothing in the query); bare-line items with no numbering; (k)-blank markers in any column, both directions in one puzzle, including damaged rows ("(5) to tie" merged cell); items living in the CONTEXT while the query is instruction-only ("Determine the correct correspondences", "Fill in the blanks (1–14)"); task-language strings that end in ':' (vowel length) — never use trailing colon alone to detect instructions. Two rows remain unparseable-by-count: one has misaligned gold (7 answers, 6 items), one enumerates payloads inline in prose. Position-aligned scoring makes over/under-parsing cost only the misaligned tail — worth a guard that pads rather than truncates.
14. match_letters needs morpheme-level CSP, not surface matching
16 puzzles / 223 items (25% of all items) are unordered form↔meaning matching with usually ZERO attested pairs. Surface similarity carries no signal (5.4% EM ≈ barely above random). The real structure: recurring morphemes across forms must map consistently to recurring words across meanings (Zuni doko:ko ↔ 'chicken', mo:chikwa ↔ 'peach'). That is a small constraint-satisfaction / bilingual-lexicon-induction problem over sets — highly verifiable (a candidate assignment implies a consistent lexicon or it doesn't) and a perfect fit for the propose-verify architecture: LLM proposes morpheme↔word hypotheses, a solver checks global consistency, Hungarian finishes. My soft quadratic-assignment attempt (power iteration over similarity graphs) was neutral; the discrete version is the right next try.
15. Throughput reality on the T4 (from the notebook)
The baseline runs ~1 min/problem at 1536 new tokens on the 14B AWQ — a full 160-puzzle set would need ~2.7 h sequential. The 30-minute budget therefore REQUIRES the two-pass design: symbolic first (~9 s), then batched direct-LLM on the low-confidence subset, weakest-first, with a hard budget cutoff (script.py implements this). Concrete knobs to tune on Colab: batch size (KV-cache limited on 16 GB with a 14B model — try 2-4), MAX_NEW_TOKENS (1536 baseline; shorter for translation-only puzzles), and whether a 7B model with bigger batches beats the 14B with tiny batches at fixed wall clock. That ablation needs the actual T4.
16a. v1 migration to single-shot-with-tools (done 2026-07-21)
The architecture inversion recommended by the landscape survey is
implemented: the tools now feed the answering model (scaffold in the prompt:
segmentation, alignment, verified numeral values, labeled symbolic
candidates), verified symbolic output overrides the model, unverified output
is demoted to hints, and CEGIS left the runtime path. Explanations ride in
the same generation (EXPLANATION: after FINAL ANSWERS:) — the
human-judged track costs zero extra passes. Open empirical questions for the
T4: does the scaffold help a 14B AWQ model the way it helped GPT-4-class
models (the segmentation evidence says gains concentrate above ~15% baseline
— a 14B may be below it); and the deferred CoT-vs-IO prompt A/B (§ "Probing
LLMs", arXiv:2502.00817).
16b. First live submission: the T4 constraint is prefill logits, not KV
The first real submission OOMed: on the sandbox's transformers, generate
computes LM-head logits over EVERY prompt position and casts to float32 —
batch 6 × ~2.9k tokens × 152k vocab × 4B = the exact 9.82 GiB failed
allocation. KV cache (what batch sizing usually optimizes) is negligible
under GQA; total prompt tokens per batch is the real T4 memory bound at
~0.9 MB/token. Fixes: token-budget batch packing (3,500), OOM → retry
singly → abstain, logits_to_keep=1 where supported. The deeper lesson:
the crash killed the script before submission.csv existed — "never empty"
must hold at the FILE level, not the item level. submission.csv is now
checkpoint-written from ~15 s in and atomically replaced as results improve.
16. The sandbox runs OLDER transformers than Colab
The notebook comments that apply_chat_template(..., return_dict=True)
returns a dict on Colab but a bare tensor in the sandbox. llm.py sidesteps
the whole class of drift by using tokenize=False + a normal tokenizer
call, which is stable across versions. Keep any future transformers usage on
the oldest-common-API path; there is no way to pin the sandbox's version.
1. The symbolic template translator may be the real workhorse, not the DSL
The plan positions LLM→DSL synthesis as the core translation engine. But the
minimal-pair substitution translator (solver/template.py: find the
attested sentence closest to the query, swap the differing tokens through
alignment links) solved every synthetic translation item exactly, with zero
LLM tokens, once three refinements landed:
- competition / explaining-away in alignment (
align.py): demote a target token already strongly claimed by another source token. Tiny corpora make co-occurrence ties pervasive; this one change flipped several wrong answers. - morphological back-off: single-word glosses (
moko = dog) project into inflected forms containing them (namoko). - affix-matched substitution: when replacing a form, prefer the candidate sharing an affix with the form replaced (kupu:nakupu :: moko:namoko) — a proportional analogy inside the template frame.
Implication: budget the DSL/CEGIS path for the residual — items where the query is far (bag-distance > ~len/2) from every attested sentence, long compositional sentences, and phenomena the template can't reach (reordering, harmony). Measure on real dev data what fraction that residual actually is; it directly sets the LLM compute budget and the finetuning priority.
2. Verifier honesty requires two scoring regimes (found a real trap)
Memorizing predictors (template translator, fallback) score EM=1.0 on attested
pairs by construction, so scoring every candidate with plain evaluate() lets
memorization beat a generalizing grammar on ties — with MDL penalty it beats
it always. The fix in router._pick_direction_solver: fixed programs
(grammars) are scored directly (they must reproduce the data), while
fit-from-data predictors are scored leave-one-out. Any future candidate
added to the selection pool must declare which regime it belongs to. This also
matters for RL later: the reward must be LOO-style for anything with data
access, or the policy learns to memorize.
3. Answer-format induction is a first-class subproblem the plan ignores
The synthetic num_to_text case exposed it: for 23, hun ox (20+3) and
kan hun ox (1×20+3) are both arithmetically valid; the gold followed the
attested style (explicit unit multiplier). The fix was a style model scoring
candidates by consistency with attested phrasing. This generalizes: whenever
multiple surface answers are semantically correct, the scorer only rewards the
one matching the dataset's convention (capitalization, articles, hyphenation
of morph boundaries, multiplier explicitness). Recommend: an explicit
"format inducer" pass that learns per-puzzle answer conventions from context
examples, applied as the last stage of every task type. Cheap, and it converts
chrF-close answers into EM hits — which the geometric mean doubly rewards.
4. Context parsing is still the top empirical risk
Everything downstream consumes extract_pairs. The current parser
(separator voting + per-line fallback) handles the formats I could construct,
but real Linguini CSVs were not available in this environment. Before any
model work, pull the actual Linguini/PuzzLing dev CSVs and fuzz the parser
against every context in them; count rows where zero pairs are extracted —
each such row is guaranteed near-zero score. A cheap LLM repair path (ask the
model to emit the pair list as JSON when the symbolic parser yields < 2 pairs)
would cap this risk at one batched call per malformed puzzle.
5. Direction detection deserves attested-data validation, not just wording
detect_direction keys off phrases like "into English". Real queries may
say "What does X mean?", "Give the form meaning ...", or nothing explicit.
A robust cross-check: try both directions and see which side of the attested
pairs the query string is script-similar to (character n-gram overlap with
task-language vs work-language material). If the query looks like the unknown
language, it's an analysis item. Cheap and language-agnostic.
6. CEGIS should get verifier-guided repair hints, not just failures
The refine prompt currently shows failing (input, expected, got) triples. The symbolic layer knows more: which morpheme boundary the output diverged at, which affix went unused, whether the failure is pure reordering. Feeding a one-line diagnosis per failure ("output differs only in word order", "expected form contains attested morph 'na-' that your grammar never attaches") should cut CEGIS rounds — worth an ablation axis in phase 2/3.
7. Self-consistency has a free implementation via round-trip
The interpreter runs both directions, so analyze(generate(x)) == x is a
zero-cost consistency check usable as (a) a tie-breaker in grammar selection
(already anticipated in AGENTS.md), and (b) a confidence signal for budget
allocation — spend refinement rounds on items whose round-trip fails.
8. Points weighting has no data column — resolve early
The scorer supports per-item weights, but the Linguini schema (id/context/query/...) carries no points column. If official IOL point values weight the leaderboard metric, they must be embedded somewhere (query text? separate mapping by id?). Resolve this with a probe submission early — it changes budget allocation (high-point items deserve the CEGIS rounds).
9. Local scorer vs sacrebleu chrF: verify parity once, in the eval image
eval/scorer.py implements chrF2 (n≤6, β=2, whitespace stripped) by hand to
stay dependency-free. sacrebleu differs in epsilon smoothing on zero-match
orders. Before trusting local ablations, run both on a few hundred string
pairs in the submission container and confirm the delta is < 1e-3; otherwise
model selection could silently optimize the wrong metric.
10. Prompt library format drifted from the plan (deliberately)
AGENTS.md says YAML prompts; the repo uses plain .md templates with
str.format placeholders (prompts/*.md). Rationale: zero dependencies, no
YAML-escaping pain with multiline linguistic data. If config-driven prompt
variants are needed for best-of-N diversity (the greedy-decoding constraint
means diversity must come from prompts, not sampling temperature — see
synth.synthesize_best_of_n), add a small variants list per template rather
than reintroducing YAML.