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
+105
View File
@@ -1678,3 +1678,108 @@ re-priced `sasi` stream buys 8773 spans across 120 frames, a mean of 73 a frame,
and each one's three-instruction dispatch is inside the fitted 43.7 — but the
mode-map walk that decides a span exists is not. `decode.s` does not implement
spans yet.
## 31. The mode decision can see cycles now, and it costs 0.26 dB (session 8)
FINDINGS 28 left the decoder missing 31% of frames at `sasi` and 42% at `scsi`
while the mode decision minimised `D + lam*R` — distortion against BYTES — on a
machine whose binding budget is CYCLES. This is the second controller.
`vq_hybrid.decide(ctx, lam, mu)` now minimises `D + lam*bytes + mu*cycles`, and
`ratectl.encode_rate_controlled(cycle_budget=...)` bisects `mu` per frame
against 833,333 cycles with the `lam` bisection nested inside it.
`tools/analysis/13_cpu_ratectl.py` measures what it costs.
### 31.1 The result
Worst sustained window, 120 frames, same targets, same quality floors:
| | PSNR | KB/s | CPU median | CPU max | frames missing |
|---|---:|---:|---:|---:|---:|
| `sasi` bytes only | 27.22 dB | 109.5 | 74.4% | 136.2% | **37/120** |
| `sasi` + cycle ceiling | **26.95 dB** | 109.4 | 81.5% | 110.6% | **1/120** |
| `scsi` bytes only | 29.90 dB | 280.0 | 94.9% | 146.6% | **51/120** |
| `scsi` + cycle ceiling | **29.27 dB** | 278.6 | 99.6% | 110.6% | **1/120** |
**36 of 37 misses at `sasi` for 0.26 dB, 50 of 51 at `scsi` for 0.62 dB.** The
bitrate does not move: the byte controller still binds, and mu changes *which*
modes are bought rather than how many bytes.
`sasi` pays less quality than `scsi` because it had less to give up: it was
already short of bytes, so the cycle-cheap directions it takes (V4 -> V1, and
blocks it can afford to hold) were near where the byte lagrangian already sat.
28.8 predicted the shape of this and got the sign right.
Mode mix, `sasi`, bytes-only -> with the ceiling: SKIP 46.4 -> 47.1%,
V1 19.8 -> 23.0%, **V4 25.2 -> 20.3%**, RAW 8.5 -> 9.6%. At `scsi` the V4
collapse is dramatic — **15.0 -> 5.3%**, with RAW taking it at 41.3 -> 43.2%,
which is 28.8's inversion happening in practice: RAW is dearer in bytes and
cheaper in cycles, so a byte-rich profile buys its way out of V4.
Only **46 of 120 frames need any mu at all** at `sasi`; the median frame is
decided at mu=0 and is unchanged from session 6.
### 31.2 The one frame that cannot fit is the intra frame, not a hard case
Both profiles miss exactly one frame, both at 110.6% — the all-V1 floor of
FINDINGS 28.5 — and in both it is **frame 0**. It has no previous
reconstruction, so every block must be coded, which is the definition of a
100%-changed frame. A scene cut mid-stream is the same thing.
That is the correct behaviour rather than a failure, and it is worth being
explicit about why: at `MU_CLIFF` a block only becomes SKIP if holding the
previous reconstruction costs less than ~28,665 units of distortion. A frame
with nothing on screen worth holding stays fully coded and 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 a deadline is the worse failure.
### 31.3 28.7 was too pessimistic, and the reason is instructive
28.7 estimated that only ~three quarters of the misses were the encoder's to
fix — 26 of 37 at `sasi` — because re-coding every non-SKIP block as V1 still
missed 11 frames. Measured, the controller fixes **36 of 37**.
The gap is that 28.7's floor held the SKIP set fixed and asked "how cheap can
the blocks we already decided to draw be?". The real decision can also **move a
block to SKIP**, paying distortion for it, and above ~90% non-SKIP that is the
only lever left. So 28.7's floor was a floor for a fixed SKIP set, not for the
mode decision. Two conclusions of 28.7 stand: the profiles are an I/O axis and
both must fit the same 10 MHz budget.
### 31.4 SKIP is not a constant, and the way out is two cost functions
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 in a mixed byte, so its price
depends on its neighbours — which a per-block lagrangian cannot see. Picking one
number is a real trade: 45 overcharges clustered SKIPs and pushes the encoder
away from the mode that saves the most cycles, 13.25 undercharges isolated ones
and lets frames overrun.
The resolution is that **the budget check does not have to use the same cost
function as the mode decision**. `decide()` uses 13.25 purely to *rank* modes
within a block, where the choice only scales the incentive (the V1-SKIP gap
moves 12% between the two candidates). The controller scores whole frames with
`vq_hybrid.cycles()`, the exact clustered rule, validated to 1 point against the
68000 — so the bisection converges on what the machine will really do, whatever
the ranking constant was. That function is now defined once and imported by
`11_cpu_budget.py`, rather than living in two places that can drift apart.
### 31.5 Both controllers are gated against decoder drift
The mu controller varies the mode map frame to frame exactly as the lam
controller does, so it is exposed to the FINDINGS 26.1 failure — an encoder
reporting a reconstruction the decoder will never produce. `09_ratectl_drift.py`
now runs **both** configurations and both report 0/120 drifting frames, 0.00 dB
overstatement. The CPU ceiling is on by default in `encode.py`
(`--no-cpu-fit` restores session 7 behaviour).
### 31.6 With spans on top, the window fits completely
Re-running the span pricing of FINDINGS 30 against a cost-aware container —
lever B first, then lever A on what it leaves:
| `sasi` | bytes only | + cycle ceiling | + ceiling + spans |
|---|---:|---:|---:|
| median frame | 74.4% | 81.5% | **56.8%** |
| worst frame | 136.2% | 110.6% | **91.5%** |
| frames missing | 37/120 | 1/120 | **0/120** |
| bitrate | 101.7 KB/s | 101.6 | 449.3 KB/s |
The intra frame lands at 91.5% — spans are what make a full redraw fit, which is
30.6's arithmetic arriving in a real container. That row is still a **model** of
a bitstream nothing implements; the two levers have never run on the 68000
together, and the ring-buffer question of 30.7 gets sharper at 449 KB/s.