Price cycles in the mode decision: 37 misses become 1, for 0.26 dB

The decoder has been CPU-bound since FINDINGS 28 while the mode decision
minimised D + lam*R -- distortion against BYTES. decide() now minimises
D + lam*bytes + mu*cycles, and ratectl bisects mu per frame against the
833,333-cycle budget with the lam bisection nested inside it. On the worst
sustained window:

  sasi  27.22 -> 26.95 dB, 109.5 -> 109.4 KB/s, 37/120 misses -> 1
  scsi  29.90 -> 29.27 dB, 280.0 -> 278.6 KB/s, 51/120 misses -> 1

Bitrate does not move: the byte controller still binds, and mu changes WHICH
modes are bought. V4 is what it stops buying -- 25.2 -> 20.3% of blocks at sasi
and 15.0 -> 5.3% at scsi, where RAW takes it. That is 28.8's inversion in
practice: RAW is dearer in bytes and cheaper in cycles, so only the byte-rich
profile can buy its way out of V4.

Three things worth knowing beyond the headline:

  - The one frame that still misses, at both profiles, is FRAME 0 -- no previous
    reconstruction, so 100% changed by definition, which is also what a scene
    cut is. It comes out at the all-V1 floor of 110.6% and is emitted late on
    purpose. Freezing a cut to make a deadline is the worse failure.
  - 28.7's "11 frames are impossible" was too pessimistic. That floor held the
    SKIP set fixed and asked how cheaply the drawn blocks could be drawn; the
    real decision can also MOVE a block to SKIP, which above ~90% non-SKIP is
    the only lever left.
  - SKIP's price depends on its neighbours (13.25 cycles clustered, 45 mixed),
    which a per-block lagrangian cannot see. The way out is that the two uses
    need not share a cost function: a ranking constant inside decide(), the
    exact clustered rule for the frame-level bisection. vq_hybrid.cycles() is
    now the one definition of that rule and 11_cpu_budget.py imports it.

Gated: 09_ratectl_drift.py runs both controllers, both 0/120 drifting frames.
The cost-aware container decodes pixel-exact on the 68000 (120 frames). ON by
default in encode.py; --no-cpu-fit restores session 7. check.sh ALL GREEN.

Still a model, not a measurement, for THIS container: FINDINGS 31's cycle
figures come from vq_hybrid.cycles (within 1 point of the 68000 on four frames
of the session-7 container). Timing this one on the machine is step 1 of the
next session -- it was started and killed for time, and it is slow.

FINDINGS 31. tools/analysis/13_cpu_ratectl.py.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-23 16:24:22 -07:00
parent 29eb78a599
commit 06b98d4b47
10 changed files with 586 additions and 186 deletions
+101 -10
View File
@@ -81,6 +81,27 @@ PROFILES = {
# instead (FINDINGS 26.2). The old ladder ran to lam=2e5, 250x past shippable.
LAM_CLIFF = 800.0
# Ceiling on the CYCLE search. mu prices a cycle in the same units lam prices a
# byte, so the scale that matters is set by their ratio: at the `sasi` floor of
# lam=60, mu=0.2 makes a V1 block's 300 cycles cost what its 1 payload byte
# costs. MU_CLIFF=100 is three decades past that: a V1 block priced at 30,000
# distortion units.
#
# It does NOT freeze the picture, and that is the point. At MU_CLIFF a block
# only becomes SKIP if holding the previous reconstruction costs less than
# 28,665 units of distortion, so a frame with nothing on screen to hold -- the
# first frame of a stream, or a scene cut -- stays fully coded and comes out at
# the all-V1 floor of 110.6% (FINDINGS 28.5). Such a frame is emitted LATE on
# purpose, exactly as a frame that will not fit at LAM_CLIFF is emitted over
# budget. Freezing a cut to make the deadline would be the worse failure.
MU_CLIFF = 100.0
MU_FLOOR = 1e-4 # bisection is geometric, so lo must be > 0
# The hard per-frame decode budget. NOT a bucket: bytes can be banked in the
# player's ring buffer, but there is no double buffer to decode ahead into, so
# a frame that misses its deadline is simply late. FINDINGS 28.
FRAME_CYCLES = 10_000_000 / 12.0
AUDIO_KBPS = 7.8 # MSM6258 ADPCM 15.6kHz mono -- comes out of the same budget
@@ -89,7 +110,7 @@ def frame_budget(kbps, fps=12, audio=AUDIO_KBPS):
return (kbps - audio) * 1024.0 / fps
def _search_lam(ctx, allow, lam_lo, lam_hi, iters=12):
def _search_lam(ctx, allow, lam_lo, lam_hi, iters=12, mu=0.0):
"""Smallest lam (=> best quality) whose frame fits `allow` bytes.
Payload size is non-increasing in lam -- raising lam can only move a block
@@ -100,17 +121,17 @@ def _search_lam(ctx, allow, lam_lo, lam_hi, iters=12):
not fit: that frame is emitted over budget on purpose. Past the FINDINGS 15
cliff a frame is not rate-controlled, it is destroyed, so a visible overrun
is the better failure (FINDINGS 26.2)."""
mode, sz = H.decide(ctx, lam_lo)
mode, sz = H.decide(ctx, lam_lo, mu)
if sz <= allow:
return lam_lo, mode, sz, False
mode_hi, sz_hi = H.decide(ctx, lam_hi)
mode_hi, sz_hi = H.decide(ctx, lam_hi, mu)
if sz_hi > allow:
return lam_hi, mode_hi, sz_hi, True
lo, hi = lam_lo, lam_hi # lo does not fit, hi does
best = (lam_hi, mode_hi, sz_hi)
for _ in range(iters):
mid = float(np.sqrt(lo * hi))
mode_m, sz_m = H.decide(ctx, mid)
mode_m, sz_m = H.decide(ctx, mid, mu)
if sz_m <= allow:
hi = mid; best = (mid, mode_m, sz_m)
else:
@@ -118,9 +139,55 @@ def _search_lam(ctx, allow, lam_lo, lam_hi, iters=12):
return best[0], best[1], best[2], False
def _search_mu(ctx, allow, lam_lo, lam_hi, cyc_budget, iters=10):
"""Smallest mu whose frame fits BOTH budgets: `allow` bytes and
`cyc_budget` 68000 cycles.
Two controllers, one nested inside the other, because the constraints are
not separable. Raising mu moves blocks to cheaper-to-DECODE modes, which
usually also shrinks the frame -- but not always: RAW is 400 cycles against
V4's 448 and 16 bytes against 4, so mu can buy cycles by SPENDING bytes
(FINDINGS 28.8). So every mu step re-runs the lam bisection and the byte
budget is enforced at the mu that is actually chosen.
Cost is scored with H.cycles(), the exact clustered rule, NOT with the
per-block ranking constant the decision uses -- see vq_hybrid's note on
SKIP. The controller therefore converges on what the 68000 will really do.
Monotonicity: at a fixed lam, raising mu can only move a block to a mode
that costs no more cycles, and it can only ADD to a SKIP cluster, so frame
cycles are non-increasing in mu. The nested lam re-search can perturb that
at the margin (a smaller frame permits a smaller lam, which buys quality
back and can cost a few cycles), so the bisection keeps the best FEASIBLE
point it has actually seen rather than trusting the invariant.
Returns (mu, lam, mode, size, cyc, over_bytes, over_cycles)."""
lam, mode, sz, ovr = _search_lam(ctx, allow, lam_lo, lam_hi, mu=0.0)
cyc = H.cycles(mode)
if cyc <= cyc_budget:
return 0.0, lam, mode, sz, cyc, ovr, False
lam_h, mode_h, sz_h, ovr_h = _search_lam(ctx, allow, lam_lo, lam_hi, mu=MU_CLIFF)
cyc_h = H.cycles(mode_h)
if cyc_h > cyc_budget: # cannot fit even frozen: emit late
return MU_CLIFF, lam_h, mode_h, sz_h, cyc_h, ovr_h, True
lo, hi = MU_FLOOR, MU_CLIFF # lo overruns, hi fits
best = (MU_CLIFF, lam_h, mode_h, sz_h, cyc_h, ovr_h)
for _ in range(iters):
mid = float(np.sqrt(lo * hi))
lam_m, mode_m, sz_m, ovr_m = _search_lam(ctx, allow, lam_lo, lam_hi, mu=mid)
cyc_m = H.cycles(mode_m)
if cyc_m <= cyc_budget:
hi = mid; best = (mid, lam_m, mode_m, sz_m, cyc_m, ovr_m)
else:
lo = mid
return (*best, False)
def encode_rate_controlled(m, target_kbps, fps=12, bucket_frames=8,
lam_lo=1.0, lam_hi=LAM_CLIFF, prefill=0.0,
steps=None, verbose=False):
steps=None, verbose=False, cycle_budget=None):
"""Per-frame lam search under a leaky bucket, driving the encoder ONE FRAME
AT A TIME and feeding back the frame actually emitted.
@@ -149,6 +216,12 @@ def encode_rate_controlled(m, target_kbps, fps=12, bucket_frames=8,
109.5 to 116.3 KB/s against a 110 ceiling, and on a 14-frame clip it
disables rate control entirely because the bucket is larger than the clip.
`cycle_budget` adds the SECOND controller (session 8): a hard per-frame
68000 decode ceiling, bisected on `mu` inside the lam search. None (the
default) leaves it off and reproduces session 6 exactly, which is what
keeps tools/analysis/09_ratectl_drift.py comparable. Pass
FRAME_CYCLES for the 12fps stock-68000 budget.
`steps` is accepted and ignored -- there is no ladder any more.
"""
if steps is not None and verbose:
@@ -156,25 +229,35 @@ def encode_rate_controlled(m, target_kbps, fps=12, bucket_frames=8,
budget = frame_budget(target_kbps, fps)
cap = bucket_frames * budget
bucket = prefill * cap # banked bytes; bounded by the player's buffer both ways
out = dict(recon=[], modes=[], sizes=[], lam=[], l1=[], l4g=[], overrun=[])
out = dict(recon=[], modes=[], sizes=[], lam=[], l1=[], l4g=[], overrun=[],
mu=[], cycles=[], late=[])
prev = None
for f in range(len(m["idx"])):
ctx = H.frame_ctx(m, f, prev)
allow = budget + bucket
lam, mode, sz, ovr = _search_lam(ctx, allow, lam_lo, lam_hi)
if cycle_budget is None:
lam, mode, sz, ovr = _search_lam(ctx, allow, lam_lo, lam_hi)
mu, cyc, late = 0.0, H.cycles(mode), False
else:
mu, lam, mode, sz, cyc, ovr, late = _search_mu(
ctx, allow, lam_lo, lam_hi, cycle_budget)
rec = H.paint(m, ctx, mode)
bucket = float(np.clip(bucket + budget - sz, -cap, cap))
out["recon"].append(rec); out["modes"].append(mode)
out["sizes"].append(sz); out["lam"].append(lam); out["overrun"].append(ovr)
out["mu"].append(mu); out["cycles"].append(cyc); out["late"].append(late)
out["l1"].append(ctx["sym"]["l1"]); out["l4g"].append(ctx["sym"]["l4g"])
prev = rec
if verbose:
print(f" f{f:04d} lam={lam:8.2f} {sz:7.0f} B "
f"(allow {allow:7.0f}){' OVER' if ovr else ''}")
print(f" f{f:04d} lam={lam:8.2f} mu={mu:8.4f} {sz:7.0f} B "
f"(allow {allow:7.0f}) {100*cyc/FRAME_CYCLES:5.1f}% cpu"
f"{' OVER' if ovr else ''}{' LATE' if late else ''}")
return dict(recon=out["recon"], modes=out["modes"],
sizes=np.array(out["sizes"]), lam=np.array(out["lam"]),
l1=out["l1"], l4g=out["l4g"], overrun=np.array(out["overrun"]),
nb=m["nb"], budget=budget, cap=cap)
mu=np.array(out["mu"]), cycles=np.array(out["cycles"]),
late=np.array(out["late"]),
nb=m["nb"], budget=budget, cap=cap, cycle_budget=cycle_budget)
def summarise(m, enc, target_kbps, fps=12):
@@ -192,6 +275,14 @@ def summarise(m, enc, target_kbps, fps=12):
over=100.0 * np.mean(sz > enc.get("budget", np.inf)),
skip=100 * (mo == 0).mean(), v1=100 * (mo == 1).mean(),
v4=100 * (mo == 2).mean(), raw=100 * (mo == 3).mean())
if "cycles" in enc:
cy = np.asarray(enc["cycles"])
d.update(cyc_med=float(np.median(cy)), cyc_max=float(cy.max()),
cyc_p90=float(np.percentile(cy, 90)),
cpu_miss=int((cy > FRAME_CYCLES).sum()),
mu_med=float(np.median(enc["mu"])),
mu_max=float(np.asarray(enc["mu"]).max()),
late=int(np.asarray(enc.get("late", [])).sum()))
if "lam" in enc:
lam = enc["lam"]
d.update(lam_med=float(np.median(lam)), lam_max=float(lam.max()),