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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user