Does label encounter order limit vocabulary coverage?

Vocabulary and OOV
Train on common and rare labels, changing which labels appear first.

Example

code: rare-left
target: -1.0

The answer is shown here for explanation; it is hidden from the model when scored.

Model tree with record, code, target. Amber cards are hidden prediction targets. Other fields provide the input.

Model tree with record, code, target. Amber cards are hidden prediction targets. Other fields provide the input.

Figure 1: Amber cards are hidden prediction targets. Other fields provide the input.

Comparison

Compare rare-first and common-first training. Check that all observed labels are admitted and that common-label distinctions learn.

Result

P057 · Latest full run · Limited

Seed 5701 on cpu: 18 of 18 test checks met. This experiment demonstrates a limitation. History: 12 of 12 full runs met every check, across 3 distinct seeds. History can include earlier source versions. The experiment code has changed since this run; these measurements describe its earlier version.

Comparison Latest measurement
Common-label coverage, rare first 100.0%
Common-label coverage, common first 100.0%

Storage grows and retains earlier labels in both orders. This single-consumer experiment does not test distributed encounter order.

Experiment details and code

Latest full run

Seed 5701, cpu, recorded 2026-09-20T21:19:28.373595+00:00. Outcome: met.

Source fingerprint: 6d252f9e843e80314f8edae8c89cb81ede2ff3c0783191b4f80fa13e221253bb. Python 3.12.10; Torch 2.12.0.

Measurement Value
baseline_rmse 0.999922
arms rare_first: steps: 300; initial_size: 1; allocated_size: 4; vocabulary: rare-left, rare-right, hot, cold; counts: rare-left: 5; rare-right: 5; hot: 9604; cold: 9586; common_coverage: 1; nrmse: 0.0313262; identity_swap_drift: 1.95628; common_first: steps: 300; initial_size: 1; allocated_size: 4; vocabulary: cold, hot, rare-left, rare-right; counts: cold: 9586; hot: 9604; rare-left: 5; rare-right: 5; common_coverage: 1; nrmse: 0.0387018; identity_swap_drift: 1.98372
Behavioral checks
Behavioral check Outcome
rare_first: all four labels are admitted Met
rare_first: first label IDs are preserved Met
rare_first: allocation holds all four labels Met
rare_first: storage grows automatically Met
rare_first: all labels have training exposures Met
rare_first: common-label coverage is complete Met
rare_first: common-label nRMSE below 0.15 Met
rare_first: common identities have distinct learned effects Met
rare_first: prediction does not change admission Met
common_first: all four labels are admitted Met
common_first: first label IDs are preserved Met
common_first: allocation holds all four labels Met
common_first: storage grows automatically Met
common_first: all labels have training exposures Met
common_first: common-label coverage is complete Met
common_first: common-label nRMSE below 0.15 Met
common_first: common identities have distinct learned effects Met
common_first: prediction does not change admission Met

Full run history

Runs may include earlier source versions. Repeating a seed is not an independent seed check.

Seed Outcome Recorded Code fingerprint
5701 met 2026-09-16T22:46:36.371605+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
5701 met 2026-09-16T22:48:37.432766+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
6501 met 2026-09-16T22:49:31.771999+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
6502 met 2026-09-16T22:50:24.570312+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
5701 met 2026-09-17T00:03:30.469559+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
5701 met 2026-09-17T01:02:01.853475+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
5701 met 2026-09-18T16:41:35.200168+00:00 48a0717a76b0e13f5fd2be0e8d3198c7877cb31d417e5e53470c1099038243cd
5701 met 2026-09-20T01:58:53.184848+00:00 7c262a64ce4a9e524a8a4da71f90b4b880e4a9465a29c857cb9e91a19a63ad1e
5701 met 2026-09-20T02:03:09.373288+00:00 7c262a64ce4a9e524a8a4da71f90b4b880e4a9465a29c857cb9e91a19a63ad1e
5701 met 2026-09-20T02:45:15.566324+00:00 958cf8cb6f4521ef1fbe60d9249ae32823a4a9ed0316f724bbd93d3350d8347f
5701 met 2026-09-20T02:51:59.249769+00:00 958cf8cb6f4521ef1fbe60d9249ae32823a4a9ed0316f724bbd93d3350d8347f
5701 met 2026-09-20T21:19:28.373595+00:00 b97c5a47e01eaac8421a129e31eee22c7b5fa75d2445bdc7fafcac02206a792f

Recorded results.

Run this experiment

Proof execution is optional and separate from building this site. Use a RelFlow build matching the recorded provenance; replace the wheel path below with that artifact. Run from this documentation repository’s root:

uv run --with /path/to/matching-relflow.whl python proofs/run.py P057

Or, from a prepared proof environment, run the self-contained script directly:

PYTHONPATH=proofs python proofs/vocabulary/capacity_admission_order.py

Add --accelerator gpu for CUDA or --seed 42 for another seeded experiment. --steps 2 checks execution with a short training budget; it is recorded as a smoke run.

Download the complete proof.

Data and model

A training pass has 4,096 rows: one of each rare label and 4,094 balanced common labels. The target is the label’s effect plus Gaussian noise with SD 0.02. For common-first, move the first cold and hot observations to the front without duplicating, deleting, or otherwise changing any record. Validation uses 512 independent observations. Test generation uses 2,050 independent observations and scores only its 2,048 common-label rows.

Training and controls

Each arm uses the same seed and 300 AdamW updates at learning rate 0.002, with shuffle=False, num_workers=0, and one device. Every arm must admit all four labels, retain the first two IDs, have common-label coverage of one, and achieve nRMSE below 0.15. Swapping cold and hot must change predictions by more than 1.5 on average, demonstrating use of their distinct identities. Both arms must grow to hold all four labels.

Scope of evidence

Earlier fixed-capacity runs used different gates. Assess this revision using a full run whose source fingerprint matches the current script; earlier scores do not establish its autoscaling behavior.

This checks one-device admission and common-label learning. The singleton rare labels establish admission and ID order, not reliable rare-label predictions. It does not establish worker/DDP ordering or optimizer-state preservation after growth later in training. Dedicated runtime tests cover those invariants separately, including two-rank DDP, persistent workers, gradient accumulation, and checkpoint resume.

Complete experiment code

Code
"""P057: vocabulary growth retains admission order and learnable distinctions."""

from collections.abc import Iterator
from functools import partial

import lightning.pytorch as lit
import numpy as np
import pyarrow as pa
from reporting import report

import relflow as rf

PROOF_ID = "P057"
BUDGET = 300
EFFECTS = {"rare-left": -1.0, "rare-right": 1.0, "cold": -1.0, "hot": 1.0}


def records(*, rows: int, seed: int, common_first: bool = False) -> Iterator[dict]:
    if rows < 4:
        raise ValueError("capacity proof requires at least four observations")
    rng = np.random.default_rng(seed)
    common = np.resize(np.asarray(["cold", "hot"]), rows - 2)
    rng.shuffle(common)
    labels = ["rare-left", "rare-right", *common.tolist()]
    noise = rng.normal(0.0, 0.02, rows)
    observations = [
        {"code": label, "target": float(EFFECTS[label] + deviation)}
        for label, deviation in zip(labels, noise, strict=True)
    ]
    order = list(range(rows))
    if common_first:
        front = [labels.index("cold"), labels.index("hot")]
        order = front + [index for index in order if index not in front]
    for index in order:
        yield observations[index]


def build() -> rf.Model:
    return rf.Model(
        d_model=32,
        n_layers=1,
        n_heads=4,
        dropout=0.0,
        batch_size=64,
        code=rf.Category(p_unavailable=0.0),
        target=rf.Number(mask=True, objective="mse"),
    )


def prediction(model: rf.Model, rows: list[dict]) -> np.ndarray:
    output = model.predict(pa.Table.from_pylist(rows))["predictions"].to_pylist()
    return np.asarray([row["target"]["content"] for row in output], dtype=np.float64)


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    test = [row for row in records(rows=2050, seed=seed + 3) if row["code"] in {"cold", "hot"}]
    train = [row for row in records(rows=4096, seed=seed + 1) if row["code"] in {"cold", "hot"}]
    actual = np.asarray([row["target"] for row in test])
    baseline = float(np.sqrt(np.mean((actual - np.mean([row["target"] for row in train])) ** 2)))
    metrics = {"baseline_rmse": baseline, "arms": {}}
    checks = {}
    for name, common_first in (("rare_first", False), ("common_first", True)):
        lit.seed_everything(seed, workers=True)
        model = build()
        initial_size = model.nodes["/code"].embedder.size
        model.optimizer = rf.adamw(learning_rate=0.002)
        data = rf.SyntheticDataModule(
            model=model,
            train=partial(records, rows=4096, seed=seed + 1, common_first=common_first),
            validate=partial(records, rows=512, seed=seed + 2),
            seed=seed,
            shuffle=False,
            num_workers=0,
        )
        trainer = lit.Trainer(
            accelerator=accelerator,
            devices=1,
            max_epochs=-1,
            max_steps=BUDGET if steps is None else min(steps, BUDGET),
            deterministic=True,
            logger=False,
            enable_checkpointing=False,
            enable_progress_bar=False,
            enable_model_summary=False,
            num_sanity_val_steps=0,
        )
        trainer.fit(model, datamodule=data)
        model.eval()
        vocabulary = rf.Category.vocabulary(model, "/code")
        predicted = prediction(model, test)
        renamed = [{**row, "code": "hot" if row["code"] == "cold" else "cold"} for row in test]
        measured = {
            "steps": trainer.global_step,
            "initial_size": initial_size,
            "allocated_size": model.nodes["/code"].embedder.size,
            "vocabulary": vocabulary,
            "counts": rf.Category.counts(model, "/code"),
            "common_coverage": float(np.mean([row["code"] in vocabulary for row in test])),
            "nrmse": float(np.sqrt(np.mean((actual - predicted) ** 2))) / baseline,
            "identity_swap_drift": float(np.mean(np.abs(predicted - prediction(model, renamed)))),
        }
        first = ("cold", "hot") if common_first else ("rare-left", "rare-right")
        checks[f"{name}: all four labels are admitted"] = len(vocabulary) == 4 and set(vocabulary) == set(EFFECTS)
        checks[f"{name}: first label IDs are preserved"] = vocabulary[:2] == first
        checks[f"{name}: allocation holds all four labels"] = measured["allocated_size"] == 4
        checks[f"{name}: storage grows automatically"] = measured["allocated_size"] > initial_size
        checks[f"{name}: all labels have training exposures"] = all(
            measured["counts"].get(label, 0) > 0 for label in EFFECTS
        )
        checks[f"{name}: common-label coverage is complete"] = measured["common_coverage"] == 1.0
        checks[f"{name}: common-label nRMSE below 0.15"] = measured["nrmse"] < 0.15
        checks[f"{name}: common identities have distinct learned effects"] = measured["identity_swap_drift"] > 1.5
        checks[f"{name}: prediction does not change admission"] = rf.Category.vocabulary(model, "/code") == vocabulary
        metrics["arms"][name] = measured
    return metrics, checks


if __name__ == "__main__":
    report(PROOF_ID, run, seed=5701)