Files
daedalus-fourier/docs/k3_mc_phase4.md
T
marfrit 356e446a49 Cycle 3 (MC interpolation) closure: M1'''=100%, R'''=0.067 RED, M4=-19.5%
Third daedalus-fourier kernel — VP9 8-tap regular subpel filter,
horizontal direction, 8-wide output. Multiply-heavy by design to
stress V3D's no-DP4A deficit. Full cycle Phase 1-7 + M4'''.

Phase 5''' second-model review delivered cleanly — caught 1 RED
bug pre-implementation (src_off off-by-3 indexing convention) and
2 YELLOW gaps (assert MUST language, shaderdb filter-LUT gate).
Without the review, M1''' would have failed silently on first run
with cryptic "high-index source pixels wrong" symptoms.

Phase 6 v1 first-light: M1''' 100.0000% bit-exact (65536/65536
blocks across all 16 mx phases). Phase 5''' filter-LUT prediction
materialised exactly: 197 uniforms (gate was 144), 2 threads (down
from cycle-2's 4 due to register pressure).

Performance:

  M2''' = 1.413 Mblock/s     (707.9 ns/block)
  M3''' = 20.997 Mblock/s    (NEON baseline phase3)
  R'''  = 0.067              (RED band — structural mismatch)
  shaderdb: 488 inst, 2 threads, 197 uniforms, 25 max-temps, 0 spills

M4''' concurrent matrix (8s windows):

  NEON 1-core           14.479 Mblock/s
  NEON 4-core           15.248 Mblock/s   <- baseline (compute-bound,
                                              not bandwidth-saturated
                                              like cycles 1+2!)
  QPU only               1.380 Mblock/s
  MIXED NEON-3 + QPU    12.277 Mblock/s   <- -19.5% (FAIL gate)
  MIXED NEON-4 + QPU    12.158 Mblock/s   <- -20.3%

NEW cross-cycle finding (Phase 9 lesson 2): compute-bound CPU
workloads make the QPU-offload story collapse. Cycles 1+2 were
bandwidth-saturated (4-core scaling 0.56-0.82x of 1-core), so
freeing a CPU core via QPU offload added throughput. Cycle 3 MC
is compute-bound (4-core scaling 1.05x of 1-core — near-linear),
no free cycles to free. QPU contribution (0.45 Mblock/s in
contention) doesn't compensate for losing 1 NEON core delivering
~3.8 Mblock/s.

But 30fps@1080p floor: PASS in every config (1.4x to 15.7x
isolation margin). Per project_30fps_floor_is_fine.md, user-facing
test never fails — daily YouTube playback works fine on any CPU/QPU
split.

DEPLOYMENT RECIPE for higgs (cycle 3 confirmed split):

  IDCT (k1)  -> QPU   (R=0.92, +7% mixed, frees CPU core)
  LPF  (k2)  -> QPU   (R=0.41, +7% mixed, frees CPU core)
  MC   (k3)  -> CPU   (R=0.067, -19.5% mixed — stays on CPU)
  Entropy    -> CPU   (structurally serial)

Mixed-substrate deployment, not "QPU does everything". Realistic for
higgs: entropy + MC on 2-3 ARM cores; IDCT + LPF dispatched to QPU
concurrently; 1-2 ARM cores left for vscode etc.

New artifacts:
- src/v3d_mc_8h.comp               — GLSL kernel
- tests/vp9_mc_ref.c               — standalone C ref (REGULAR filter
                                     embedded; clean transcription)
- tests/bench_neon_mc.c            — M1'''_c + M3''' bench
- tests/bench_v3d_mc.c             — M1''' + M2''' bench with contract
                                     asserts + 30fps margin display
- tests/bench_concurrent_mc.c      — M4''' pthread bench
- external/ffmpeg-snapshot/libavcodec/aarch64/vp9mc_neon.S    (vendored)
- external/ffmpeg-snapshot/libavcodec/vp9_subpel_filters_table.c
                                     (hand-extracted; provides
                                      ff_vp9_subpel_filters symbol
                                      without dragging in full vp9dsp.c)
- docs/k3_mc_phase{1,2,3,4,5,7}.md — full cycle documentation

Memory updates: project_30fps_floor_is_fine.md (user's 30fps target
recalibration), MEMORY.md index updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:51:43 +00:00

208 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
cycle: 3
phase: 4
status: open (awaiting Phase 5''' review)
date_opened: 2026-05-18
parent: k3_mc_phase3.md
template: phase4.md (cycle 1) + k2_deblock_phase4.md (cycle 2) — same constraints, same patterns
---
# Cycle 3, Phase 4 — Plan QPU MC kernel
Compact plan. Cycle 1+2 phase4 docs cover the constraint matrix
(C1-C10) and the dev-discipline patterns. Phase 4''' references
them rather than re-deriving.
## 1. Constraints (carried)
Same V3D 7.1 device. New for MC specifically:
- SMUL24 covers 16-bit filter × 8-bit pixel mults (max ~32K product, fits)
- Sum of 8 products fits in int32 trivially
- No DP4A — must use 8 separate scalar muls per output pixel
- 16 filter phases × 8 taps × 2 B = 256 B — too big for push constants
(max 128 B), small enough for one const array in shader
## 2. Workload model
Per 8×8 block:
- 512 SMUL24 (8 mults × 64 output pixels)
- 448 ADD (7 adds × 64 output pixels)
- 64 round (+64 → >>7) operations
- 64 clip-to-[0,255]
- ≈ 1150 ALU ops per block
- 120 B read + 64 B write = 184 B per block
Per 1080p frame (32 400 blocks):
- ~37 Mops compute → 1.8 ms at v3d 23 % sustained (compute-bound estimate)
- ~5.9 MB traffic → 1.48 ms at 4 GB/s GPU share (bandwidth-bound estimate)
## 3. Workgroup geometry
Bake in the v4 lesson and the cycle-2 single-WG-size-from-start:
- `local_size_x = 256` (16 subgroups × 16 lanes)
- 8 lanes per block (1 lane per row r=0..7), 2 blocks per subgroup
- **32 blocks per WG**
- 1080p: 1 013 WGs
Same lane decomposition as cycle 2 LPF:
```
edge_slot = lane_in_sg >> 3 // 0 or 1 — "which block in this subgroup"
row = lane_in_sg & 7 // 0..7
block_local = sg_in_wg * 2 + edge_slot
block_idx = wg_id * 32 + block_local
oob = block_idx >= n_blocks
```
No barrier needed, no shared mem. Safe early-return on oob.
## 4. Per-thread algorithm
```glsl
if (block_idx >= pc.n_blocks) return;
uvec4 m = u_meta.meta[block_idx];
uint dst_off = m.x;
uint src_off = m.y;
uint mx = m.z & 15u;
// Read 15 source pixels for this row.
uint src_row_addr = src_off + row * pc.src_stride_u8;
int s0 = int(u_src.src[src_row_addr + 0u]);
int s1 = int(u_src.src[src_row_addr + 1u]);
int s2 = int(u_src.src[src_row_addr + 2u]);
int s3 = int(u_src.src[src_row_addr + 3u]);
int s4 = int(u_src.src[src_row_addr + 4u]);
int s5 = int(u_src.src[src_row_addr + 5u]);
int s6 = int(u_src.src[src_row_addr + 6u]);
int s7 = int(u_src.src[src_row_addr + 7u]);
int s8 = int(u_src.src[src_row_addr + 8u]);
int s9 = int(u_src.src[src_row_addr + 9u]);
int s10 = int(u_src.src[src_row_addr + 10u]);
int s11 = int(u_src.src[src_row_addr + 11u]);
int s12 = int(u_src.src[src_row_addr + 12u]);
int s13 = int(u_src.src[src_row_addr + 13u]);
int s14 = int(u_src.src[src_row_addr + 14u]);
// Filter coefficients — const REGULAR table, indexed by mx.
int F0 = FILTER_REGULAR[mx][0]; ... int F7 = FILTER_REGULAR[mx][7];
// 8 output pixels (each = 8-tap convolution of 8 consecutive source).
uint dst_row_addr = dst_off + row * pc.dst_stride_u8;
int o0 = F0*s0 + F1*s1 + F2*s2 + F3*s3 + F4*s4 + F5*s5 + F6*s6 + F7*s7;
int o1 = F0*s1 + F1*s2 + F2*s3 + F3*s4 + F4*s5 + F5*s6 + F6*s7 + F7*s8;
int o2 = F0*s2 + F1*s3 + F2*s4 + F3*s5 + F4*s6 + F5*s7 + F6*s8 + F7*s9;
int o3 = F0*s3 + F1*s4 + F2*s5 + F3*s6 + F4*s7 + F5*s8 + F6*s9 + F7*s10;
int o4 = F0*s4 + F1*s5 + F2*s6 + F3*s7 + F4*s8 + F5*s9 + F6*s10+ F7*s11;
int o5 = F0*s5 + F1*s6 + F2*s7 + F3*s8 + F4*s9 + F5*s10+ F6*s11+ F7*s12;
int o6 = F0*s6 + F1*s7 + F2*s8 + F3*s9 + F4*s10+ F5*s11+ F6*s12+ F7*s13;
int o7 = F0*s7 + F1*s8 + F2*s9 + F3*s10+ F4*s11+ F5*s12+ F6*s13+ F7*s14;
u_dst.dst[dst_row_addr + 0u] = uint8_t(clamp((o0 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 1u] = uint8_t(clamp((o1 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 2u] = uint8_t(clamp((o2 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 3u] = uint8_t(clamp((o3 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 4u] = uint8_t(clamp((o4 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 5u] = uint8_t(clamp((o5 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 6u] = uint8_t(clamp((o6 + 64) >> 7, 0, 255));
u_dst.dst[dst_row_addr + 7u] = uint8_t(clamp((o7 + 64) >> 7, 0, 255));
```
Mirrors `tests/vp9_mc_ref.c` directly.
## 5. SSBOs / push constants
| binding | name | type | usage |
|---|---|---|---|
| 0 | `meta` | `readonly uvec4[]` | per-block (dst_off, src_off, mx, _pad) |
| 1 | `dst` | `uint8_t[]` | output pixels |
| 2 | `src` | `readonly uint8_t[]` | input pixels |
Push constants (16 B):
```
n_blocks, dst_stride_u8, src_stride_u8, _pad
```
Filter table: hard-coded in shader as
`const int FILTER_REGULAR[16][8] = { ... };` — 128 const ints.
**Race safety:** lane r writes `dst[dst_off + r*dst_stride + 0..7]`
(8 contiguous bytes). For rows r and r+1, writes are `r*stride + 7`
and `(r+1)*stride + 0`. Disjoint iff `dst_stride ≥ 8`.
**Contracts (revised per phase5''' findings 4 + 6):**
1. `dst_stride_u8 ≥ 8` (race-safety lower bound)
2. `src_stride_u8 ≥ 15` (per-row read span)
3. `dst_off + 7 + (r_max)*dst_stride < dst_buffer_size`
4. `src_off + 14 + (r_max)*src_stride < src_buffer_size`
5. **`src_off` is the byte offset of the FIRST byte of the source
block's row 0 in the SSBO buffer — NOT shifted by +3.** The
C bench's `src + 3` C-caller convention does not carry into
the SSBO offset. Shader reads `s[k] = u_src.src[src_off +
row*stride + k]` for k=0..14, which equals
`master_src[block_base + row*stride + k]`, matching the C ref's
per-row read of `master_src[block_base + row*stride + (x..x+7)]`
for output col x ∈ 0..7.
**Phase 6 MUST** add `assert(dst_stride_u8 >= 8 && src_stride_u8 >= 15)`
in `bench_v3d_mc.c`'s meta-construction loop. **Phase 6 MUST** also
run `V3D_DEBUG=shaderdb` after first compile and record uniform
count. If uniform count > ~144 (a fall-out indicator that the
filter LUT inflated unfavorably), escalate filter to a dedicated
SSBO binding 3.
## 6. Predicted M2''' / R'''
From Phase 3:
- Compute envelope: 17.5 Mblock/s
- Bandwidth envelope: 22.0 Mblock/s
- min ≈ 17.5 Mblock/s
- R''' isolation = 17.5 / 20.997 ≈ **0.83** (YELLOW, near GREEN)
Honest lower bound R''' = 0.5-0.6 if SMUL24-vs-DP4A penalty bites
harder. Phase 7''' measures.
## 7. WILL / WILL NOT touch
WILL (Phase 6 creates):
- `src/v3d_mc_8h.comp` — GLSL shader
- `tests/bench_v3d_mc.c` — harness with contract asserts
- CMake updates
WILL NOT touch:
- Cycle 1/2 artifacts (frozen Phase 3 baselines)
- `external/ffmpeg-snapshot/` (frozen vendored sources, including
the just-added `vp9_subpel_filters_table.c`)
- `src/v3d_runner.{c,h}` (reusable as-is)
## 8. Phase 5''' review prompts
Specific high-risk decisions:
1. **Orientation / arithmetic correctness** — the 8 `o0..o7`
expressions in §4 are stencil-aligned. Verify the off-by-one
in `F[k] * s[c+k]` matches `F[k] * src[x+k-3]` after the
`src+3` indexing shift used by the bench.
2. **Filter table residency** — hard-coded const array vs SSBO
vs push constants. Const is simplest but may cause v3d_compiler
to generate a large constant LUT. Worth verifying via shaderdb.
3. **Race safety** — same shape as cycle 2 (different rows of
same block disjoint iff stride ≥ row-width). Verify
`dst_stride ≥ 8` contract.
4. **`src+3` index shift** — the bench's source layout puts the
"row-0 col-0 source pixel" at `src + 3` (so src has -3..+12
reachable). Make sure the QPU shader applies this offset
consistently to its `src_off` meta value.
**RESOLVED (phase5''' finding 4, RED):** `src_off` is the raw
block-base offset (NOT +3-shifted). See §5 contract 5.
5. **Anything missing.**
## 9. Phase 6 execution order
1. Write shader, get glslang to accept (likely 0 spills, ≥2 threads)
2. Write bench with asserts + meta layout
3. Run M1''' bit-exact (gate)
4. Run M2''' (throughput)
5. If R''' < 1.0 → M4''' concurrent
6. Phase 7''' verdict