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:
+101
-10
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user