Handoff: rate control is next, and it is unsound as written

Session 5 handoff. The user has chosen rate control as the next session's work,
so this reads ratectl.py properly before that session starts rather than
discovering the problem mid-implementation.

FINDINGS 26: encode_rate_controlled() is not sound. H.encode() is temporally
recursive -- SKIP blocks copy the previous RECONSTRUCTION -- but rate control
builds a ladder of independent whole-sequence encodes and picks each frame from
whichever rung fits the budget. Frames then reference reconstructions the
decoder never saw. Measured on the Singe window: 67 rung switches, 111 of 120
frames drift, worst frame 43.4% of pixels, reported PSNR overstated by 0.36 dB.
It would have wired up cleanly and reported a plausible wrong answer.

Two further defects in the same function: the lam ladder runs to 2e5, 250x past
the FINDINGS 15 cliff, so a frame that only fits up there is destroyed rather
than rate-controlled; and with 5 rungs only two are ever chosen, straddling the
operating point by 7.5x. The docstring describes a per-frame binary search,
which is the right design -- the implementation is a fixed ladder. The leaky
bucket does work and should be kept: 109.1 KB/s against a 110 target.

tools/analysis/09_ratectl_drift.py is the regression test and the acceptance
criterion: it exits non-zero until zero frames drift.

Also corrected the stale 38% blit figure in ratectl.py's profile commentary,
which session 5 measured at 53.6% (FINDINGS 24), and recorded the pgrep -f
self-kill trap again -- four times across three sessions now.

check.sh ALL GREEN.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-23 14:10:07 -07:00
parent e00264a058
commit 145753c0bf
5 changed files with 218 additions and 16 deletions
+70
View File
@@ -1003,3 +1003,73 @@ One 10 s window of one stream, at fixed lam, with `_paint` still a Python loop.
The full-disc survey is still not done, and the numbers above are the *worst*
window rather than a distribution over content. What has changed is that the
worst case is now a measurement rather than a worry.
---
## 26. Rate control is unsound as written — found before wiring it up (session 5)
FINDINGS 25.3 promoted rate control from insurance to a requirement. Reading
`ratectl.py` before wiring it into `encode.py` turned up a correctness bug that
would have produced exactly the kind of plausible-looking wrong result this
project keeps catching (FINDINGS 4, 9, 14, 18).
### 26.1 The lam ladder desynchronises the encoder from the decoder
`H.encode()` is **temporally recursive**: SKIP blocks are copied from the
previous *reconstruction*, and `prev = out` closes the loop
(`vq_hybrid.py:84-109`). A frame's output therefore depends on every frame
before it in that same run.
`encode_rate_controlled()` runs `H.encode()` once per lam over the **whole
sequence**, building a ladder of independent temporal chains, then picks each
frame from whichever rung fits the budget. When frame *f* comes from rung *i*
and frame *f-1* was emitted from rung *j != i*, the SKIP blocks in *f* reference
a reconstruction **the decoder never saw**.
Measured on the Singe window (`tools/analysis/09_ratectl_drift.py`, 120 frames,
5 rungs, target 110 KB/s):
| | |
|---|---|
| rung switches | **67** over 120 frames |
| frames whose emitted output differs from what the encoder recorded | **111 / 120** |
| worst frame | **21,339 px = 43.4% of the frame** |
| encoder-vs-decoder agreement, worst frame | 27.1 dB |
| reported PSNR overstatement | **0.36 dB** |
The 0.36 dB is the least interesting number here. The encoder is reporting
quality for a reconstruction that will never exist, and 43% of a frame differing
is a visible artefact whatever the mean says.
**The fix is structural, not a tuning change:** `H.encode()` must become
frame-drivable — take `prev` and one lam, return one frame — so rate control can
feed back the frame it actually emitted. The current whole-sequence signature is
what makes the ladder tempting in the first place.
### 26.2 The ladder spans 250x past the shippable range
`lam_hi=2e5`, but FINDINGS 15 puts the quality cliff between lam=800 and
lam=2000 and says do not ship past lam~800. Every rung above ~800 is
unshippable, so a frame that only fits at lam=9457 has not been rate-controlled,
it has been destroyed. Cap `lam_hi` at 800 and let a frame that cannot fit
overrun the bucket — a visible overrun is a better failure than silent garbage.
### 26.3 The ladder is far too coarse where it matters
With `steps=5` the geomspace lands on 1 / 21 / 447 / 9457 / 200000, and **only
two rungs were ever chosen**. The budget is 8,721 B/frame; the two straddling
rungs deliver 23,183 B (lam=21) and 3,071 B (lam=447) — a **7.5x** gap across
the operating point. Rate control cannot land near a target it has to jump over.
The module docstring already describes the right approach — *"per frame we
binary-search lam to land inside a byte budget"* — but the implementation is a
fixed precomputed ladder. Doc and code disagree; the doc is correct.
### 26.4 What does work
The leaky bucket lands the mean where it should: **109.1 KB/s against a 110
target**, with 32% of frames over the per-frame budget and banked by the bucket.
That mechanism is sound and worth keeping. It is the per-frame lam *selection*
underneath it that needs rebuilding, not the bucket.
### 26.5 Cost note before starting
Each rung is a full-sequence encode and `_paint` is still a Python per-block
loop, so a 5-rung run over 120 frames takes minutes. **Vectorise `_paint`
first** — it is already on the list for the full-disc survey and it makes the
rate-control work practical rather than merely faster.