Can admitting a new input change predictions before learning?

Vocabulary
Introduce a new cohort of input identities while keeping output labels and hidden-answer selection fixed.

Example

entity: cohort-0-id-42
context: high
label: yes
hide: true

label is hidden where hide is true. Each new cohort has unrelated identity effects.

Model tree with record, entity, context, label. Amber cards are hidden prediction targets. hide chooses which label answers are hidden.

Model tree with record, entity, context, label. Amber cards are hidden prediction targets. hide chooses which label answers are hidden.

Figure 1: Amber cards are hidden prediction targets. hide chooses which label answers are hidden.

Comparison

Predict the same held-out rows just before and just after admitting independent training records, without an optimizer update.

Result

P066 · Latest full run · Failing

Seed 7801 on gpu: 17 of 21 test checks met. The experiment did not meet every criterion. History: 14 of 18 full runs met every check, across 10 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
Largest probability change, second cohort 0.4904
Average probability change, second cohort 0.2112

Admission can change predictions because a new Category slot activates untrained content. The preservation checks fail; removing that jump alone would not establish a better representation.

Experiment details and code

Latest full run

Seed 7801, gpu, recorded 2026-09-19T21:39:02.011470+00:00. Outcome: not met.

Source fingerprint: 34483eb7b896c1c3bdaf4aa3a02d267a99e32ee3a6995e26c8cf405b38f9907e. Python 3.12.6; Torch 2.12.0.

Measurement Value
0 steps: 400; learned: oracle_rmse: 0.136564; constant_rmse: 0.356539; nll: 0.415898; brier: 0.140823; shuffled: oracle_rmse: 0.400621; constant_rmse: 0.356539; nll: 0.867609; brier: 0.273076
1 cold: oracle_rmse: 0.139642; constant_rmse: 0.183277; nll: 0.677407; brier: 0.241065; admission_max_drift: 0.490442; admission_mean_drift: 0.211214; admitted: oracle_rmse: 0.246454; constant_rmse: 0.183277; nll: 0.846044; brier: 0.275318; before_validation: loss.state: 0; accuracy.state: 1; vocabulary.size: 2; targets.known: 4096; targets.unavailable: 0; coverage.content: 1; accuracy.content: 0.686035; accuracy.all: 0.686035; loss.content: 0.677407; nll.content: 0.677407; after_validation: loss.state: 3.91288e-06; accuracy.state: 1; vocabulary.size: 2; targets.known: 4096; targets.unavailable: 0; coverage.content: 1; accuracy.content: 0.57251; accuracy.all: 0.57251; loss.content: 0.846044; nll.content: 0.846044; steps: 400; learned: oracle_rmse: 0.0865546; constant_rmse: 0.358146; nll: 0.373694; brier: 0.127584; shuffled: oracle_rmse: 0.425198; constant_rmse: 0.358146; nll: 1.14356; brier: 0.304409
2 cold: oracle_rmse: 0.0605292; constant_rmse: 0.183277; nll: 0.639948; brier: 0.223486; admission_max_drift: 0.670028; admission_mean_drift: 0.253573; admitted: oracle_rmse: 0.263167; constant_rmse: 0.183277; nll: 0.995912; brier: 0.284861; before_validation: loss.state: 0; accuracy.state: 1; vocabulary.size: 2; targets.known: 4096; targets.unavailable: 0; coverage.content: 1; accuracy.content: 0.670898; accuracy.all: 0.670898; loss.content: 0.639948; nll.content: 0.639948; after_validation: loss.state: 3.42129e-05; accuracy.state: 1; vocabulary.size: 2; targets.known: 4096; targets.unavailable: 0; coverage.content: 1; accuracy.content: 0.601074; accuracy.all: 0.601074; loss.content: 0.995912; nll.content: 0.995912; steps: 400; learned: oracle_rmse: 0.0897513; constant_rmse: 0.356999; nll: 0.385007; brier: 0.127683; shuffled: oracle_rmse: 0.43774; constant_rmse: 0.356999; nll: 1.22421; brier: 0.320635
Behavioral checks
Behavioral check Outcome
0: learned probability RMSE below 0.16 Met
0: learned predictions beat constant baseline Met
0: shuffled identity worsens RMSE by more than 0.08 Met
1: cold evaluation preserves vocabulary Met
1: admission actually adds training identities Met
1: admission preserves predictions Not met
1: admission preserves reconstruction NLL Not met
1: output coverage remains complete Met
1: cold marginal RMSE below 0.15 Met
1: learned probability RMSE below 0.16 Met
1: learned predictions beat constant baseline Met
1: shuffled identity worsens RMSE by more than 0.08 Met
2: cold evaluation preserves vocabulary Met
2: admission actually adds training identities Met
2: admission preserves predictions Not met
2: admission preserves reconstruction NLL Not met
2: output coverage remains complete Met
2: cold marginal RMSE below 0.15 Met
2: learned probability RMSE below 0.16 Met
2: learned predictions beat constant baseline Met
2: shuffled identity worsens RMSE by more than 0.08 Met

Full run history

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

Seed Outcome Recorded Code fingerprint
7801 met 2026-09-18T16:33:29.036700+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 not met 2026-09-18T16:35:31.576108+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 met 2026-09-18T16:37:22.603318+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 met 2026-09-18T16:37:29.078312+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7802 met 2026-09-18T16:37:49.457525+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7803 met 2026-09-18T16:38:19.000975+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7804 met 2026-09-18T16:38:47.670629+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7805 met 2026-09-18T16:39:19.502594+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7806 met 2026-09-18T16:39:49.311495+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7807 met 2026-09-18T16:40:19.737737+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7808 met 2026-09-18T16:40:51.198710+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7809 met 2026-09-18T16:41:26.270542+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7810 met 2026-09-18T16:41:57.003007+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 met 2026-09-18T16:42:51.631369+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 met 2026-09-18T16:46:21.373292+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 not met 2026-09-18T16:49:06.519554+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 not met 2026-09-18T16:55:41.129537+00:00 07a124bdc51216cb1d516bab1ca9b30c641a3817195a045df43c2437ce9314e8
7801 not met 2026-09-19T21:39:02.011470+00:00 5ee10e7dc3a0b67637dbb4ddf94872dad0b9dcd32dfc00e19e50d62f60ff6a51

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 P066

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

PYTHONPATH=proofs python proofs/vocabulary/rolling_input_admission.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.

Training and controls

Each stage has 8,192 training rows, 1,024 entity-disjoint validation rows, 4,096 independent held-out rows, and 400 AdamW updates. Optimizer state is restarted between fits; this tests staged onboarding, not uninterrupted optimizer/worker scheduling. A separate worker test checks admission itself. No admission uses validation or test observations. The latent identity effect is available only to the evaluator, never as a model field.

Subsequent cohorts must have admission drift below 1e-6 and identical count-normalized reconstruction NLL. After learning, probability RMSE must beat the constant baseline and remain below 0.16; shuffling identities must worsen it by at least 0.08. Cold marginal RMSE must remain below 0.15. These quality gates are provisional; the admission invariant is structural. The same generator and gates run unchanged against the pre-fix baseline.

Complete experiment code

Code
"""P066: admission stability, cold probabilities, and subsequent identity learning."""

from collections.abc import Iterator

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

import relflow as rf

PROOF_ID = "P066"
BUDGET = 400
IDENTITIES = 256


def records(*, rows: int, seed: int, cohort: int = 0) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    identities = rng.integers(IDENTITIES, size=rows)
    effects = np.random.default_rng(918 + cohort).choice([-2.5, 2.5], IDENTITIES)
    context = rng.choice([-2.0, 2.0], rows)
    probabilities = 1 / (1 + np.exp(-(context + effects[identities])))
    outcomes = rng.random(rows) < probabilities
    for i, (identity, x, outcome) in enumerate(zip(identities, context, outcomes, strict=True)):
        yield {
            "entity": f"cohort-{cohort}-id-{identity}",
            "context": "low" if x < 0 else "high",
            "label": "yes" if outcome else "no",
            "hide": i % 4 != 0,
        }


def build() -> rf.Model:
    model = rf.Model(
        d_model=32,
        n_layers=1,
        n_heads=4,
        batch_size=128,
        dropout=0.0,
        entity=rf.Category(p_unavailable=0.2),
        context=rf.Category(p_unavailable=0.0),
        label=rf.Category(p_unavailable=0.0, mask=rf.Mask(query="hide", reconstruct=True)),
    )
    model.optimizer = rf.adamw(learning_rate=0.002)
    return model


def probability(model: rf.Model, rows: list[dict]) -> np.ndarray:
    predictions = model.predict(pa.Table.from_pylist(rows))["predictions"].to_pylist()
    content = [row["label"]["content"] for row in predictions]
    return np.asarray([item["probability"] if item["value"] == "yes" else 1 - item["probability"] for item in content])


def score(p: np.ndarray, rows: list[dict], cohort: int, *, cold: bool = False) -> dict[str, float]:
    x = np.asarray([-2.0 if row["context"] == "low" else 2.0 for row in rows])
    if cold:
        oracle = (1 / (1 + np.exp(-(x - 2.5))) + 1 / (1 + np.exp(-(x + 2.5)))) / 2
    else:
        effects = np.random.default_rng(918 + cohort).choice([-2.5, 2.5], IDENTITIES)
        identities = [int(row["entity"].split("-")[-1]) for row in rows]
        oracle = 1 / (1 + np.exp(-(x + effects[identities])))
    y = np.asarray([row["label"] == "yes" for row in rows], dtype=float)
    clipped = p.clip(1e-7, 1 - 1e-7)
    return {
        "oracle_rmse": float(np.sqrt(np.mean((p - oracle) ** 2))),
        "constant_rmse": float(np.sqrt(np.mean((0.5 - oracle) ** 2))),
        "nll": float(np.mean(-y * np.log(clipped) - (1 - y) * np.log1p(-clipped))),
        "brier": float(np.mean((p - y) ** 2)),
    }


def evaluate(trainer: lit.Trainer, model: rf.Model, rows: list[dict]) -> dict[str, float]:
    data = rf.SyntheticDataModule(model=model, validate=lambda: iter(rows), seed=0)
    result = trainer.validate(model, datamodule=data, verbose=False)[0]
    prefix = ".label/validate."
    return {name.removeprefix(prefix): float(value) for name, value in result.items() if name.startswith(prefix)}


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    lit.seed_everything(seed, workers=True)
    model = build()
    metrics, checks = {}, {}
    trainer = None
    for cohort in range(3):
        rows = [{**row, "hide": True} for row in records(rows=4096, seed=seed + 100 + cohort, cohort=cohort)]
        training = list(records(rows=8192, seed=seed + 1 + cohort, cohort=cohort))
        arm = {}
        if trainer is not None:
            vocabulary = rf.Category.vocabulary(model, "/entity")
            before = probability(model, rows)
            before_metrics = evaluate(trainer, model, rows)
            checks[f"{cohort}: cold evaluation preserves vocabulary"] = (
                rf.Category.vocabulary(model, "/entity") == vocabulary
            )
            arm["cold"] = score(before, rows, cohort, cold=True)
            model.encode(pa.Table.from_pylist(training), strata="train")
            after = probability(model, rows)
            after_metrics = evaluate(trainer, model, rows)
            arm["admission_max_drift"] = float(np.max(np.abs(after - before)))
            arm["admission_mean_drift"] = float(np.mean(np.abs(after - before)))
            arm["admitted"] = score(after, rows, cohort, cold=True)
            arm["before_validation"] = before_metrics
            arm["after_validation"] = after_metrics
            checks.update(
                {
                    f"{cohort}: admission actually adds training identities": len(
                        rf.Category.vocabulary(model, "/entity")
                    )
                    == (cohort + 1) * IDENTITIES,
                    f"{cohort}: admission preserves predictions": arm["admission_max_drift"] < 1e-6,
                    f"{cohort}: admission preserves reconstruction NLL": abs(
                        before_metrics["nll.content"] - after_metrics["nll.content"]
                    )
                    < 1e-6,
                    f"{cohort}: output coverage remains complete": before_metrics["coverage.content"]
                    == after_metrics["coverage.content"]
                    == 1.0,
                    f"{cohort}: cold marginal RMSE below 0.15": arm["cold"]["oracle_rmse"] < 0.15,
                }
            )
        trainer = lit.Trainer(
            accelerator=accelerator,
            devices=1,
            max_epochs=-1,
            max_steps=BUDGET if steps is None else min(steps, BUDGET),
            logger=False,
            enable_checkpointing=False,
            enable_progress_bar=False,
            enable_model_summary=False,
            deterministic=True,
            num_sanity_val_steps=0,
        )
        validation = list(records(rows=1024, seed=seed + 20 + cohort, cohort=cohort + 1))
        data = rf.SyntheticDataModule(
            model=model, train=lambda: iter(training), validate=lambda: iter(validation), seed=seed
        )
        # Lightning does not reset eval mode when fitting the same model again.
        model.train()
        trainer.fit(model, datamodule=data)
        model.eval()
        arm["steps"] = trainer.global_step
        arm["learned"] = score(probability(model, rows), rows, cohort)
        permutation = np.random.default_rng(seed + 500).permutation(len(rows))
        shuffled = [{**row, "entity": rows[index]["entity"]} for row, index in zip(rows, permutation, strict=True)]
        arm["shuffled"] = score(probability(model, shuffled), rows, cohort)
        checks.update(
            {
                f"{cohort}: learned probability RMSE below 0.16": arm["learned"]["oracle_rmse"] < 0.16,
                f"{cohort}: learned predictions beat constant baseline": arm["learned"]["oracle_rmse"]
                < arm["learned"]["constant_rmse"],
                f"{cohort}: shuffled identity worsens RMSE by more than 0.08": arm["shuffled"]["oracle_rmse"]
                - arm["learned"]["oracle_rmse"]
                > 0.08,
            }
        )
        metrics[str(cohort)] = arm
    return metrics, checks


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