perf: compact Stage-2 AR inference loop to active rows only
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 1m2s
CI / Lint (ruff check) (pull_request) Successful in 1m6s
CI / Format (ruff format) (pull_request) Successful in 1m6s
CI / Tests (pull_request) Successful in 2m46s
CI / Bump version, tag, and update changelog on merge to master (pull_request) Has been skipped
CI / Publish package to Gitea package registry (pull_request) Has been skipped
CI / Update README badges (version, test count) (pull_request) Has been skipped
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 1m2s
CI / Lint (ruff check) (pull_request) Successful in 1m6s
CI / Format (ruff format) (pull_request) Successful in 1m6s
CI / Tests (pull_request) Successful in 2m46s
CI / Bump version, tag, and update changelog on merge to master (pull_request) Has been skipped
CI / Publish package to Gitea package registry (pull_request) Has been skipped
CI / Update README badges (version, test count) (pull_request) Has been skipped
sample_secondaries_ar ran all k_max=15 slots for every row regardless of each row's own predicted secondary count, even though the baseline checkpoint's rollout measured only 0.382 secondaries/step — so ~97% of stage-2 model calls generated tokens sec_valid then masked away. Compact the loop to the still-active row set at each slot: drop a row the moment its n_sec_pred is exhausted (or, under n_sec.mode="stop_token", the moment its own stop logit fires), so slot k's model calls cost O(active rows) instead of O(B). Exact — rows are independent given their own history — verified by comparing the compacted path against a new full_length=True escape hatch that reproduces the original uncompacted behavior bit-for-bit under deterministic noise. full_length=True is required by _assemble_stage2_ar_inputs_scheduled's scheduled-sampling self-sample, whose training contract needs a real prediction at every slot up to k_max regardless of a row's own count, so training behavior is unchanged. AttentionHistory's KV cache and MarkovHistory's O(1) state are kept aligned to the shrinking active set via a new HistoryEncoder.select_cache / Stage2Autoregressive.select_history_cache. Also fixes a latent bug the refactor surfaced: derived_n_sec (stop-token mode) could be overwritten by a later spurious re-fire of the stop logit on a row that had already stopped; now tracked via an explicit `finished` mask so only the first stop slot is recorded, matching the documented contract. No architecture or checkpoint-format change — every existing v0.3.0 Stage2Autoregressive checkpoint (flow/wgan, markov/attention, head/stop_token) picks up the speedup automatically on its next `giant rollout`/`giant predict`, no retraining needed. Measured (CPU, hidden_dim=512/6 blocks, k_max=15, batch 512, mean n_sec≈0.38 matching the baseline checkpoint's own rollout): 17.6-22.9x fewer wall-clock seconds for the AR loop alone (attention/markov history respectively). Directional only — baseline.toml's GPU inference-cost comment is updated accordingly, flagged stale pending a real rollout re-measurement via eval_cost_per_step. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HPt7bVLZYFJe5cG6V7ahqC
This commit is contained in:
@@ -341,6 +341,87 @@ def test_sample_secondaries_ar_none_n_sec_pred_without_stop_head_raises():
|
||||
sample_secondaries_ar(decoder, cond_cont, cond_cat, stage1_out, None, steps=2)
|
||||
|
||||
|
||||
# ── Row compaction: full_length=False (default, inference) must agree with
|
||||
# full_length=True (the pre-compaction behaviour, still exercised by
|
||||
# _assemble_stage2_ar_inputs_scheduled's training-time self-sample) ────────
|
||||
|
||||
|
||||
def _zero_randn(*size, **kwargs):
|
||||
"""Drop-in replacement for `torch.randn` that returns zeros of the same
|
||||
shape — makes the ODE/WGAN noise deterministic so a compacted run and a
|
||||
full_length run can be compared row-for-row regardless of how many
|
||||
`torch.randn` calls each makes (compaction changes the batch size, and
|
||||
therefore the RNG stream position, at every slot)."""
|
||||
device = kwargs.get("device")
|
||||
dtype = kwargs.get("dtype")
|
||||
return torch.zeros(*size, device=device, dtype=dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("history", ["markov", "attention"])
|
||||
@pytest.mark.parametrize("generator", ["flow", "wgan"])
|
||||
def test_sample_secondaries_ar_compaction_matches_full_length_head_mode(generator, history, monkeypatch):
|
||||
B, k_max, emb_dim = 4, 5, 6
|
||||
decoder = _stage2_ar("physical", generator, emb_dim=emb_dim, k_max=k_max, history=history)
|
||||
cond_cont, cond_cat = _cond(B)
|
||||
stage1_out = torch.randn(B, X_DIM)
|
||||
n_sec_pred = torch.tensor([0, 1, 3, k_max])
|
||||
|
||||
monkeypatch.setattr(torch, "randn", _zero_randn)
|
||||
sec_cont_c, sec_type_c, sec_valid_c = sample_secondaries_ar(
|
||||
decoder, cond_cont, cond_cat, stage1_out, n_sec_pred, steps=2, full_length=False
|
||||
)
|
||||
sec_cont_f, sec_type_f, sec_valid_f = sample_secondaries_ar(
|
||||
decoder, cond_cont, cond_cat, stage1_out, n_sec_pred, steps=2, full_length=True
|
||||
)
|
||||
|
||||
assert torch.equal(sec_valid_c, sec_valid_f)
|
||||
assert torch.equal(sec_valid_c, torch.arange(k_max).unsqueeze(0) < n_sec_pred.unsqueeze(1))
|
||||
assert torch.allclose(sec_cont_c[sec_valid_c], sec_cont_f[sec_valid_f], atol=1e-4, rtol=1e-4)
|
||||
assert torch.allclose(sec_type_c[sec_valid_c], sec_type_f[sec_valid_f], atol=1e-4, rtol=1e-4)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("generator", ["flow", "wgan"])
|
||||
def test_sample_secondaries_ar_compaction_matches_full_length_stop_token(generator, monkeypatch):
|
||||
"""`n_sec_sampling="greedy"` keeps the stop decision itself deterministic
|
||||
(no `torch.rand` draw), so only `torch.randn` needs zeroing."""
|
||||
B, k_max = 6, 5
|
||||
decoder = _stage2_ar_stop_token("physical", generator, n_sec_sampling="greedy", k_max=k_max)
|
||||
cond_cont, cond_cat = _cond(B)
|
||||
stage1_out = torch.randn(B, X_DIM)
|
||||
|
||||
monkeypatch.setattr(torch, "randn", _zero_randn)
|
||||
sec_cont_c, sec_type_c, sec_valid_c = sample_secondaries_ar(
|
||||
decoder, cond_cont, cond_cat, stage1_out, None, steps=2, full_length=False
|
||||
)
|
||||
sec_cont_f, sec_type_f, sec_valid_f = sample_secondaries_ar(
|
||||
decoder, cond_cont, cond_cat, stage1_out, None, steps=2, full_length=True
|
||||
)
|
||||
|
||||
assert torch.equal(sec_valid_c, sec_valid_f)
|
||||
assert torch.allclose(sec_cont_c[sec_valid_c], sec_cont_f[sec_valid_f], atol=1e-4, rtol=1e-4)
|
||||
assert torch.allclose(sec_type_c[sec_valid_c], sec_type_f[sec_valid_f], atol=1e-4, rtol=1e-4)
|
||||
|
||||
|
||||
def test_sample_secondaries_ar_full_length_ignores_n_sec_pred_zero_rows():
|
||||
"""A row with n_sec_pred == 0 would be dropped from the active set at
|
||||
slot 0 under compaction (full_length=False) — full_length=True must
|
||||
still run the model for it at every slot (only masked by sec_valid at
|
||||
the end), matching _assemble_stage2_ar_inputs_scheduled's contract."""
|
||||
B, k_max, emb_dim = 3, 4, 6
|
||||
decoder = _stage2_ar("physical", "flow", emb_dim=emb_dim, k_max=k_max)
|
||||
cond_cont, cond_cat = _cond(B)
|
||||
stage1_out = torch.randn(B, X_DIM)
|
||||
n_sec_pred = torch.tensor([0, 0, 0])
|
||||
sec_cont, sec_type, sec_valid = sample_secondaries_ar(
|
||||
decoder, cond_cont, cond_cat, stage1_out, n_sec_pred, steps=2, full_length=True
|
||||
)
|
||||
assert not sec_valid.any()
|
||||
# every slot still ran the model (not left at the zero-init default) —
|
||||
# a real flow ODE output from randn-initialized noise is essentially
|
||||
# never exactly zero.
|
||||
assert not torch.allclose(sec_cont, torch.zeros_like(sec_cont))
|
||||
|
||||
|
||||
# ── resolve_n_sec: n_sec.mode = "head" sampling policy (gitea #86) ──────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user