Files
daedalus-fourier/docs/k3_mc_phase2.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

110 lines
4.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: 2
status: closed 2026-05-18
date_opened: 2026-05-18
parent: k3_mc_phase1.md
---
# Cycle 3, Phase 2 — MC situation analysis
## 1. C reference
- **Source**: `external/ffmpeg-snapshot/libavcodec/vp9dsp_template.c`
(already vendored from cycle 1).
- **Function**: `put_8tap_regular_8h_c` generated by
`filter_fn_1d(8, h, mx, regular, FILTER_8TAP_REGULAR, put)`
expands to call `do_8tap_1d_c` with `ds=1` (horizontal) and the
REGULAR filter bank.
- **Underlying primitive**: `do_8tap_1d_c` iterates `h` rows;
per row, iterates `w=8` columns; per column, computes the
`FILTER_8TAP` macro: `clip((sum_{k=0..7} F[k] * src[x+k-3]
+ 64) >> 7, 0, 255)`.
- **Spec**: VP9 specification § 8.5.1 (subpel motion compensation).
## 2. NEON reference
- **Source**: `external/ffmpeg-snapshot/libavcodec/aarch64/vp9mc_neon.S`
(vendored 2026-05-18, FFmpeg n7.1.3, SHA-256
`6b1d50f9821742584fdd47758057f810644aff3a008faaa774ff5b9cac4d1fef`).
- **Symbol**: `ff_vp9_put_regular8_h_neon` (note: filter type baked
into name, width=8 baked in, h-direction baked in)
- **Signature** (VP9 `vp9_mc_func` typedef):
```c
void ff_vp9_put_regular8_h_neon(uint8_t *dst, ptrdiff_t dst_stride,
const uint8_t *src, ptrdiff_t src_stride,
int h, int mx, int my);
```
Registers: `x0=dst, x1=dst_stride, x2=src, x3=src_stride, w4=h, w5=mx, w6=my`.
- **Dependencies**:
- `libavutil/aarch64/asm.S` ✓ (already vendored)
- `ff_vp9_subpel_filters[3][16][8]` symbol — provided by
`external/ffmpeg-snapshot/libavcodec/vp9_subpel_filters_table.c`
(hand-extracted from `libavcodec/vp9dsp.c` of the same n7.1.3
pin; copying just the constant data avoids dragging in the
rest of `vp9dsp.c` which would require linking the entire VP9
decoder).
## 3. Workload model
Per 8×8 block output:
- 8 multiplies × 8 columns × 8 rows = **512 multiplies**
- 7 additions × 8 columns × 8 rows = 448 additions
- 1 round (+64), 1 shift (>>7), 1 clip per pixel × 64 = 192 ops
- Total ~1150 integer ops per block
Per-block memory (horizontal-only filter, 8-pixel-wide output):
- Read: 8 rows × (8 output cols + 7 tap overhang) = 8 × 15 = **120 source bytes**
- Write: 8 rows × 8 cols = **64 dst bytes**
- Total: **~184 bytes / block**
Per 1080p frame (32 400 8×8 blocks, worst case all-MC):
- ~5.9 MB total memory traffic
- ~37 Mops compute
- At GPU 4 GB/s share: 1.48 ms / frame = 675 FPS = 21.9 Mblock/s
- At V3D 92 GFLOPS theoretical scalar (SMUL24 throughput ≈ FP MUL): 0.4 ms compute / frame = 2500 FPS theoretical → **compute is NOT the bottleneck** at this shape
So MC is **bandwidth-bound on the QPU**, similar to LPF cycle 2.
## 4. Per-row workload diversity (vs cycle 1+2)
| | IDCT (k1) | LPF (k2) | MC (k3) |
|---|---|---|---|
| Per-block math | Heavy butterflies (~60 ops/block via separable transform) | Light: 0-30 ops per edge × 8 rows | 8-tap convolution: 1150 ops per block |
| Per-block memory | ~320 B in + 64 B out | ~64 B in + ~24 B out per edge | 120 B in + 64 B out |
| Compute / memory ratio | High | Low (memory-bound, lots of skipping) | Medium (compute-rich but bandwidth-bound at GPU) |
| Conditional? | No (always-execute) | Yes (fm/hev divergence per row) | No (deterministic per pixel) |
| QPU mult intensity | Q14 16b×16b mults | Light (compares, small clips) | 16b×8b mults (filter × pixel) |
MC is interesting because it's **compute-rich AND bandwidth-bound** —
the closest match in workload shape to a real-world GPU compute kernel
the V3D was designed for (graphics filtering).
## 5. Constraints carried from cycle 1+2
Same V3D 7.1 device profile (vulkaninfo unchanged). The relevant
specifics for MC:
- No DP4A → 8-tap convolution must be 8 separate SMUL24 + ADDs
(the typical GPU "dot4" packing is not available)
- shaderInt16 = false → filter coefficients widened to int32 in
registers; the filter table itself can be a uint16-storage SSBO
- shaderInt8 = false → source pixels widened to int32 in registers
- 1024-byte (16 KiB / 16) shared mem per WG is ample for MC source
staging if useful (15 cols × 8 rows × 1 byte per block-row × 32
blocks per WG = 3 840 B per row); for v1 we skip shared-mem
staging and let TMU handle reads directly
## 6. What Phase 2 does *not* close
- Per-block (block_y, block_x) layout / meta format. Phase 4 picks.
Likely same shape as cycle 2 (uvec4 per block: dst_offset,
src_offset, mx, _pad).
- Filter table residency: as SSBO load every row, push-constants
per dispatch (different mx per dispatch), or constant baked into
shader (one filter per shader = 16 specialised shaders for the 16
mx phases). Phase 4 picks; v1 likely SSBO for simplicity.
- Vertical / "hv" / "avg" / 4-pixel / 16-pixel / 32-pixel / 64-pixel
variants — out of cycle 3 scope; cycle 4+ if needed.
Phase 3 next: build `tests/bench_neon_mc.c`, capture M3'''.