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
+60 -10
View File
@@ -42,6 +42,49 @@ LUMA = VQ.LUMA
_HDR_BYTES_PER_BLOCK = 2 / 8.0
RAW_BYTES = 16.0 # literal palette bytes, never indices
# ---------------------------------------------------------------------------
# CYCLE cost of each mode, per block, MEASURED on the 68000 (FINDINGS 28.2,
# tools/bench/decode.lua). This is the other axis: `lam` prices bytes, `mu`
# prices cycles, and the two are not proportional -- V4 is 4x a V1 block in
# bytes and 1.49x in cycles.
#
# SKIP IS NOT A CONSTANT, and it is the one trap in here. A SKIP block costs
# 13.25 cycles when all four blocks sharing its header byte are SKIP (one
# `tst.b` clears the group) and ~45 when it sits in a mixed byte -- so its
# price depends on its NEIGHBOURS, which a per-block lagrangian cannot see.
# The way out is that the two uses do not need the same number:
# * `decide` uses C_SKIP_RANK purely to RANK modes within a block. SKIP is
# the cheapest mode either way, so the choice only scales the incentive:
# the V1-SKIP gap moves 12% between the two candidates.
# * `cycles()` scores a WHOLE frame with the exact clustered rule, and that
# is what the rate controller bisects against. Nothing downstream of the
# mode decision uses the ranking constant.
C_V1, C_V4, C_RAW = 299.9, 448.2, 400.4
C_SKIP_CLUSTERED = 53.0 / 4 # all-SKIP header byte: one tst.b for four
C_SKIP_MIXED = 45.0 # a SKIP block inside a mixed byte
C_SKIP_RANK = C_SKIP_CLUSTERED # ranking only -- see above
MODE_CYCLES = np.array([C_SKIP_RANK, C_V1, C_V4, C_RAW], dtype=np.float64)
FRAME_CYCLES_12FPS = 10_000_000 / 12.0 # 833,333, x68k.cpp:1133
def cycles(mode):
"""Exact decode cost of one frame's mode map, in 68000 cycles.
Single source of truth: tools/analysis/11_cpu_budget.py imports this, and
it reproduces the four frames timed on the 68000 to within 1 point
(FINDINGS 28.2). Instruction cycles against zero-wait-state memory, so a
LOWER BOUND like every 68000 figure since FINDINGS 24."""
g = np.asarray(mode).reshape(-1, 4) # one header byte = four blocks
allskip = (g == 0).all(1)
c = allskip.sum() * 4 * C_SKIP_CLUSTERED
mm = g[~allskip]
c += (mm == 0).sum() * C_SKIP_MIXED
c += (mm == 1).sum() * C_V1
c += (mm == 2).sum() * C_V4
c += (mm == 3).sum() * C_RAW
return float(c)
def blocks_of(idx, pal, bw, bh):
return VQ.blockify(idx, pal, bw, bh)
@@ -149,17 +192,24 @@ def frame_ctx(m, f, prev, idx_bytes=None):
idx_bytes=default_idx_bytes(m) if idx_bytes is None else idx_bytes)
def decide(ctx, lam):
"""Lagrangian mode decision at one lam. Returns (mode, payload bytes).
def decide(ctx, lam, mu=0.0):
"""Lagrangian mode decision at one lam and one mu. Returns (mode, bytes).
Cheap by design: no painting, no image-sized work. A lam search calls this
a dozen times per frame and paints once."""
Minimises `distortion + lam*bytes + mu*cycles` per block. `mu=0` is the
byte-only decision every session before 8 made; the machine's binding
budget is cycles, and bytes and cycles do not rank the modes the same way
(V4 is 4x V1 in bytes, 1.49x in cycles; RAW is dearer than V4 in bytes and
CHEAPER in cycles, so mu inverts that preference -- FINDINGS 28.8).
Cheap by design: no painting, no image-sized work. A search calls this a
dozen times per lam step and paints once."""
ib = ctx["idx_bytes"]
s = ctx["sym"]
cost = np.stack([ctx["eS"],
s["e1"] + lam * (1.0 * ib),
s["e4"] + lam * (4.0 * ib),
np.full(ctx["nb"], lam * RAW_BYTES)])
mc = mu * MODE_CYCLES
cost = np.stack([ctx["eS"] + mc[0],
s["e1"] + lam * (1.0 * ib) + mc[1],
s["e4"] + lam * (4.0 * ib) + mc[2],
np.full(ctx["nb"], lam * RAW_BYTES + mc[3])])
mode = np.argmin(cost, axis=0).astype(np.uint8)
return mode, frame_bytes(mode, ctx["nb"], ib)
@@ -193,10 +243,10 @@ def paint(m, ctx, mode):
return from_blocks(ob, nbx, nby)
def encode_frame(m, f, prev, lam, idx_bytes=None):
def encode_frame(m, f, prev, lam, idx_bytes=None, mu=0.0):
"""One frame at one lam against one previous reconstruction."""
ctx = frame_ctx(m, f, prev, idx_bytes)
mode, sz = decide(ctx, lam)
mode, sz = decide(ctx, lam, mu)
return dict(recon=paint(m, ctx, mode), mode=mode, size=sz,
l1=ctx["sym"]["l1"], l4g=ctx["sym"]["l4g"], ctx=ctx)