diff --git a/CMakeLists.txt b/CMakeLists.txt index e24bea2..a3f11ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,6 +293,21 @@ if (DAEDALUS_BUILD_VULKAN) add_dependencies(bench_concurrent_lpf8 daedalus_shaders) target_link_libraries(bench_concurrent_lpf8 PRIVATE v3d_runner Vulkan::Vulkan pthread) target_compile_options(bench_concurrent_lpf8 PRIVATE -O3 -march=armv8-a+simd) + + # Issue 003 — mixed-kernel M4 bench (NEON-N kernel A + QPU kernel B). + # Links all FFmpeg + dav1d NEON sources we have. + add_executable(bench_concurrent_mixed + tests/bench_concurrent_mixed.c + ${FFASM_SOURCES} + ${FFASM_LPF_SOURCES} + ${FFASM_MC_SOURCES} + ${FFC_MC_SOURCES} + ${DAV1D_CDEF_ASM_SOURCES} + ${DAV1D_CDEF_C_SOURCES} + ) + add_dependencies(bench_concurrent_mixed daedalus_shaders) + target_link_libraries(bench_concurrent_mixed PRIVATE v3d_runner Vulkan::Vulkan pthread) + target_compile_options(bench_concurrent_mixed PRIVATE -O3 -march=armv8-a+simd) endif() # ---- Summary ---------------------------------------------------------------- diff --git a/docs/issues/003-mixed-kernel-m4-bench.md b/docs/issues/003-mixed-kernel-m4-bench.md index fa6c2f1..7b28b37 100644 --- a/docs/issues/003-mixed-kernel-m4-bench.md +++ b/docs/issues/003-mixed-kernel-m4-bench.md @@ -1,87 +1,148 @@ # Issue 003 — Mixed-kernel M4 bench (closes cycle 3/5 deployment verdict) -**Status**: open, blocks Phase 8 deployment plumbing for cycles 3+5 +**Status**: **CLOSED 2026-05-18** (partial — real QPU CDEF still deferred to cycle 5 Phase 6, but enough data to update deployment recipe) **Type**: measurement gap; methodology fix -**Predicted verdict**: cycle 3 MC + cycle 5 CDEF may flip from - "CPU only" to "opportunistic QPU helper" -**Priority**: medium (changes deployment recipe; doesn't block other cycles) +**Verdict shift**: cycle 3 MC verdict stands (CPU only); cycle 5 CDEF deserves "opportunistic helper" caveat; cycle 1+2+4 deployment recipe **validated by V4 result**. **Filed**: 2026-05-18 +**Bench**: `tests/bench_concurrent_mixed.c` (built `bench_concurrent_mixed`) ## Background Cycles 3 (MC) and 5 (CDEF, partial) were verdict'd "stay on CPU" based on M4 measurements showing mixed NEON-3 + QPU running the -**same kernel** ran SLOWER than pure NEON-4. Specifically: +**same kernel** ran SLOWER than pure NEON-4. The user-flagged +calibration (2026-05-18): the M4 "same-kernel" test sets the bar +too high. A "different-kernel" test would more accurately reflect +deployment. -| | NEON-4 | NEON-3 + QPU | delta | +## Measurement results (hertz, 2026-05-18) + +`bench_concurrent_mixed` matrix, 6-second windows, NEON-3 pinned +to cores 0-2, QPU/fallback worker on core 3: + +| # | CPU side | QPU side | CPU agg | QPU contrib | +|---|---------------------------|--------------------------------|-------------|--------------| +|V1 | MC NEON-3 | CDEF (NEON fallback, core 3) | 24.49 Mblock/s | 1.75 Mblock/s CDEF | +|V2 | LPF4 NEON-3 | CDEF (NEON fallback, core 3) | 27.28 Medge/s | 1.70 Mblock/s CDEF | +|V3 | MC NEON-3 (**control**) | MC (real QPU dispatch) | 22.64 Mblock/s | 0.39 Mblock/s MC | +|V4 | MC NEON-3 | LPF4 (real QPU dispatch) | 27.87 Mblock/s | 12.74 Medge/s LPF4 | +|V5 | LPF4 NEON-3 | MC (real QPU dispatch) | 30.82 Medge/s | 0.37 Mblock/s MC | + +The "QPU side" cell records the substrate actually used. +**V1 and V2 use NEON-on-core-3** as a proxy for QPU CDEF because +cycle 5 Phase 6 (real QPU CDEF shader) is not yet implemented; +the proxy gives a lower bound on the "QPU helper" question. + +## Cross-variant deltas + +**Effect on CPU MC throughput when the QPU runs a different kernel:** + +| QPU kernel | CPU MC agg | delta vs V3 | per-core delta | |---|---|---|---| -| Cycle 3 MC | 15.25 Mblock/s | 12.28 | **−19.5 %** | -| Cycle 5 CDEF (predicted) | ~ 12-15 | ~ 10-12 | negative | +| MC (V3, same-kernel) | 22.64 Mblock/s | — | baseline | +| CDEF NEON fallback (V1) | 24.49 Mblock/s | +8.2 % | +0.6 Mblock/s/core | +| LPF4 real QPU (V4) | 27.87 Mblock/s | **+23.1 %** | +1.7 Mblock/s/core | -But this is the **worst-case contention scenario**: both substrates -competing for the same memory bus with the same access pattern. +Switching the QPU off MC (the same kernel) onto LPF4 (a different +bandwidth-bound kernel) gave the CPU MC side a **23 % per-core +throughput uplift** — because the QPU stopped contending for the +shared memory channel with the same access pattern. -**Real decoder pipeline shape**: CPU runs entropy + MC + LR + other -work concurrently; QPU runs IDCT + LPF (currently) + (potentially) -CDEF/MC. Different kernels on different substrates contend -*less* than same-kernel-on-both. +## Headline finding — V4 is the validated deployment shape -The user-flagged calibration (2026-05-18): the M4 "same-kernel" -test sets the bar too high. A "different-kernel" test would more -accurately reflect deployment. +**V4 = NEON-3 doing MC + QPU doing LPF4** is precisely the +daedalus-fourier deployment recipe (CPU runs cycle 3 MC; QPU runs +cycle 2 LPF4 via the GREEN-band offload). The measurement: -## What to measure +- CPU MC: 27.87 Mblock/s (per-core 8.3-10.0) +- QPU LPF4: 12.74 Medge/s (65 % of QPU LPF4 isolation throughput, + 19.6 Medge/s from cycle 2; bandwidth contention is real but + doesn't kill the offload) +- **Both substrates productive concurrently.** -A new bench harness `tests/bench_concurrent_mixed.c` that runs: +This is the experiment that should have run *first*; the +same-kernel M4 was the wrong comparison. The user was right. -| Variant | CPU side (NEON-3 pinned) | QPU side (1 core) | Captures | -|---|---|---|---| -| A | LPF wd=4 (bandwidth-bound, like real LPF stage) | CDEF | CDEF helper throughput; CPU LPF throughput drop | -| B | MC (compute-bound, like real MC stage) | CDEF | CDEF helper throughput; CPU MC throughput drop | -| C | MC | MC | (cycle 3 M4 control) | -| D | LPF wd=4 + MC alternating (proxy for "CPU doing mixed real work") | CDEF | Real-pipeline approximation | +## V3 vs V4 — why same-kernel M4 was pessimistic -Compute "QPU helper value" = (mixed total throughput in the relevant -kernel) − (CPU-only baseline) for each variant. +V3 (cycle 3 same-kernel rerun in this bench): 22.64 CPU MC + 0.39 +QPU MC = 23.03 total Mblock/s. The QPU substrate is a poor +substitute for a 4th NEON core when both are doing the same +kernel (QPU contributes 0.39 vs ~9.0 a 4th NEON core would add). -If variant A or B shows the QPU adds positive CDEF throughput -without significantly reducing the CPU kernel's throughput, then -CDEF deserves an "opportunistic helper" verdict instead of -"CPU only". +V4 (different-kernel deployment): 27.87 CPU MC + 12.74 QPU LPF4. +The QPU is "free" — it's not stealing throughput from the CPU +side (CPU MC is *higher* than in V3), and it's adding real LPF4 +work that the CPU would otherwise have to do. -## Expected outcome +**Conclusion**: the same-kernel M4 in cycles 1-5 was a +worst-case contention bound. The real deployment shape (V4) +performs *better* than same-kernel M4 suggested. -Per the user's "5 % CPU drop / 50 % bored QPU" framing: -- Variant A (bandwidth+bandwidth): QPU contention with bandwidth- - heavy LPF is real; QPU contribution likely ~70 % of isolation -- Variant B (compute+CDEF): MC is the worst-saturated case from - cycle 3; QPU likely under-contributes, CPU MC may drop. Net - result ~ cycle 3 M4 (−19.5 % rerun) -- Variant D (mixed): probably the closest-to-deployment number. - Best estimate of "additional QPU helper" value. +## V1, V2 — CDEF as opportunistic helper -## Acceptance criteria +V1/V2 use NEON-on-core-3 (not real QPU) as a proxy because cycle +5 Phase 6 isn't built. The proxy results: -- `tests/bench_concurrent_mixed.c` lands, 4 variants measurable -- Verdict per variant: "+X.X %" CDEF throughput vs pure CPU baseline -- Cycle 3 and cycle 5 deployment recipes updated either way -- `docs/k3_mc_phase7.md §"M4 methodology caveat"` updated with - results +- V1: NEON-core-3 CDEF adds **1.75 Mblock/s** while NEON-3 MC + delivers 24.49 Mblock/s (slightly *higher* than V3 control's + 22.64, because CDEF is compute-bound so it contends little on + the memory bus). +- V2: NEON-core-3 CDEF adds **1.70 Mblock/s** while NEON-3 LPF4 + delivers 27.28 Medge/s (close to NEON-4 LPF4 isolation 29.47). -## Why deferred +So **the 4th core CAN run CDEF concurrently** without crushing +the other 3 cores' MC or LPF work. Whether the actual *QPU* +(after cycle 5 Phase 6 lands) does likewise is unknown: -User-directed cycle 5 was CDEF; M4 methodology calibration only -surfaced AFTER cycle 5 close. The fix is its own ~half-day bench -work, separable from any cycle's kernel implementation. +- QPU CDEF predicted R₅ = 0.02-0.05 → at best 0.05 × 3.9 + ≈ 0.2 Mblock/s of CDEF helper. That's an order of magnitude + *below* the NEON-fallback proxy. +- But the QPU substrate would contend on the QPU side of the + memory hierarchy; the CPU MC side may be *less* affected than + V1's 24.49 (which had NEON contention). -## Related +The conservative read: **CDEF stays on CPU as primary path; QPU +CDEF dispatch path should exist in the V4L2 wrapper but only used +when no IDCT/LPF queue is pending**. Re-measure after cycle 5 +Phase 6 closes. -- `docs/k3_mc_phase7.md §"M4 methodology caveat"` (the calibration - doc with the user's contribution) -- `docs/k5_cdef_phase3_partial.md §"Deployment recommendation"` - (softened verdict pending this issue) -- `tests/bench_concurrent_mc.c` (cycle 3 same-kernel bench; - template for the mixed-kernel variant) -- `tests/bench_concurrent_lpf.c` + `bench_concurrent_lpf8.c` - (cycle 2/4 bench templates) -- Memory: `feedback_m4_same_kernel_worst_case.md` +## V5 — LPF on CPU side with QPU MC + +V5 inverts V4: NEON-3 does LPF4, QPU does MC. CPU LPF agg = +30.82 Medge/s (essentially NEON-4 isolation), QPU MC adds 0.37 +Mblock/s. This is the **wrong deployment** — QPU has no comparative +advantage for MC, and the LPF kernel that *should* go to QPU +stays on CPU. Confirms that cycle 2 LPF belongs on QPU, not the +other way around. + +## Updated deployment recipe + +| Cycle | Kernel | Primary substrate | QPU dispatch path | Notes | +|---|---|---|---|---| +| 1 IDCT 8×8 | QPU | yes | M4 +7.2 % validated | +| 2 LPF wd=4 | QPU | yes | M4 +6.9 % validated; **V4 confirms under MC contention** | +| 3 MC 8h | **CPU** | optional / unused | QPU MC contributes 0.39 Mblock/s under any contention scenario — keep dispatch path but don't enqueue | +| 4 LPF wd=8 | QPU | yes | M4 +4.1 % validated | +| 5 CDEF | **CPU** | opportunistic only | Cycle 5 Phase 6 deferred; real QPU CDEF measurement still owed | + +## What changes in repo state + +- `tests/bench_concurrent_mixed.c` lands (~470 LOC). +- `CMakeLists.txt` builds `bench_concurrent_mixed` target with all + the FFmpeg + dav1d NEON sources. +- `docs/k3_mc_phase7.md` § "M4 methodology caveat" updated with V3 + vs V4 deltas. +- `docs/k5_cdef_phase3_partial.md` § "Deployment recommendation" + updated with V1/V2 fallback-proxy results. +- Memory `feedback_m4_same_kernel_worst_case.md` annotated with + closing numbers. + +## What's still open after this issue + +- Real QPU CDEF measurement (depends on cycle 5 Phase 6 landing). +- Variant D (mixed LPF+MC alternating CPU work) skipped — the V1 + vs V4 contrast already answers the deployment question. +- Phase 8 V4L2 wrapper should follow the recipe table above: + dispatch paths for ALL kernels exist; the scheduler chooses + per-kernel based on the validated recipe. diff --git a/docs/k3_mc_phase7.md b/docs/k3_mc_phase7.md index df22a83..4c78798 100644 --- a/docs/k3_mc_phase7.md +++ b/docs/k3_mc_phase7.md @@ -122,6 +122,27 @@ NEON-3 on kernel-A + QPU on kernel-B concurrently would close the question. ~½ day of additional bench work; would update the deployment recipe for cycles 3 + 5 if the result is positive. +### Issue 003 results (2026-05-18, closed) + +`bench_concurrent_mixed` matrix in `docs/issues/003-mixed-kernel-m4-bench.md` +confirms the methodology critique: + +| QPU side | CPU MC agg | per-core MC | QPU contribution | +|---|---|---|---| +| MC (V3 control, same kernel) | 22.64 Mblock/s | 7.5 avg | 0.39 Mblock/s MC | +| LPF4 real QPU (V4) | **27.87 Mblock/s** | **9.3 avg** | **12.74 Medge/s LPF4** | + +Switching QPU off MC (same kernel) onto LPF4 (a different +bandwidth-bound kernel) gave CPU MC **+23 % per-core uplift**. +V4 = the actual daedalus-fourier deployment shape (CPU MC + QPU +LPF4), and both substrates were productive concurrently. + +**Cycle 3 MC verdict unchanged**: QPU MC contributes ~0.4 +Mblock/s under any contention scenario (V3, V5). The 4 NEON cores +do MC dramatically better. **MC stays on CPU.** But the +*deployment recipe overall* (cycle 1+2+4 on QPU, 3 on CPU) is +validated by V4 as a positive-sum arrangement. + ## Decision per Phase 1 rules + 30fps-floor calibration | Rule | Result | Status | diff --git a/docs/k5_cdef_phase3_partial.md b/docs/k5_cdef_phase3_partial.md index e62cf55..c2c4c2b 100644 --- a/docs/k5_cdef_phase3_partial.md +++ b/docs/k5_cdef_phase3_partial.md @@ -95,18 +95,29 @@ chasing two layout issues simultaneously). - 30fps floor: still PASS on isolation+mixed since NEON 4-core baseline likely 12+ Mblock/s, comfortably above 0.972 -**Deployment recommendation** (provisional, pending Phase 4-7 + -Issue 003 mixed-kernel M4): **CDEF baseline = CPU, QPU offload -viable as opportunistic helper, not measured**. +**Deployment recommendation** (updated 2026-05-18 after Issue 003 +closed; Phase 4-7 still deferred): **CDEF baseline = CPU, QPU +offload path should exist in V4L2 wrapper but only enqueue when +IDCT+LPF queue is empty**. -Same caveat as cycle 3 MC (see `k3_mc_phase7.md §"M4 methodology -caveat"`): our M4 measures same-kernel concurrent contention, which -is the worst case. In a real decoder pipeline where CPU is doing -entropy + MC + other work, taking CDEF off the CPU's plate could -plausibly add throughput even at R = 0.05-ish — because the QPU is -otherwise idle, the contention is across different kernels (less -collision than same-kernel), and the lost-CPU-core-cost shrinks -when the CPU has other work to fill in. +`bench_concurrent_mixed` V1 (NEON-3 MC + NEON-core-3 CDEF +fallback) and V2 (NEON-3 LPF4 + NEON-core-3 CDEF fallback) +results: + +| Variant | CPU side | CPU agg | NEON-core-3 CDEF | +|---|---|---|---| +| V1 | MC NEON-3 | 24.49 Mblock/s | 1.75 Mblock/s | +| V2 | LPF4 NEON-3 | 27.28 Medge/s | 1.70 Mblock/s | + +The proxy (NEON-on-core-3 doing CDEF) adds 1.7-1.75 Mblock/s of +CDEF work without crushing the other 3 cores' main work. CPU +aggregate stays close to single-kernel 4-core levels. Real QPU +CDEF (when cycle 5 Phase 6 lands) would substitute the QPU for +core 3; the QPU contribution is predicted R₅ = 0.02-0.05 → +~0.2 Mblock/s (much less than the NEON-fallback proxy). + +The opportunistic-helper hypothesis is **plausible but not +fully validated** for the actual QPU substrate. Conservative read: The **bandwidth-bound vs compute-bound classification rule** still holds at the kernel level, but its mapping to deployment is more diff --git a/tests/bench_concurrent_mixed.c b/tests/bench_concurrent_mixed.c new file mode 100644 index 0000000..49a362a --- /dev/null +++ b/tests/bench_concurrent_mixed.c @@ -0,0 +1,520 @@ +/* + * Issue 003 — Mixed-kernel M4 bench. + * + * Runs N NEON pthread workers (pinned 0..N-1) doing CPU kernel A, + * plus one QPU worker doing kernel B concurrently. Tests the + * "opportunistic QPU helper" hypothesis flagged by the user + * 2026-05-18 (feedback_m4_same_kernel_worst_case.md): does the QPU + * add meaningful throughput when the CPU is busy with a DIFFERENT + * kernel than the QPU is doing? + * + * CLI: + * --cpu-kernel mc|lpf4|lpf8 (default: mc) + * --qpu-kernel cdef|mc|lpf4|lpf8|idct (default: cdef) + * --neon-threads N (default: 3) + * --duration SECS (default: 8) + * + * Interpretation: compare mixed-mode throughput (sum of CPU side + * and QPU side, normalised) against the cycle-N M4 same-kernel + * baseline for the relevant kernel. If the QPU adds meaningful + * helper throughput without crushing the CPU side, the cycle + * 3+5 "CPU only" verdicts can be softened to "opportunistic + * QPU helper". + * + * License: BSD-2-Clause; links FFmpeg LGPL-2.1+ snapshot (MC, LPF) + * and dav1d BSD-2-Clause snapshot (CDEF). + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "v3d_runner.h" + +/* External NEON refs (vendored FFmpeg + dav1d). */ +extern 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); +extern void ff_vp9_loop_filter_h_4_8_neon(uint8_t *dst, ptrdiff_t stride, + int E, int I, int H); +extern void ff_vp9_loop_filter_h_8_8_neon(uint8_t *dst, ptrdiff_t stride, + int E, int I, int H); +extern void ff_vp9_idct_idct_8x8_add_neon(uint8_t *dst, ptrdiff_t stride, + int16_t *block, int eob); +extern void dav1d_cdef_filter8_8bpc_neon(uint8_t *dst, ptrdiff_t dst_stride, + const uint16_t *tmp, int pri_strength, int sec_strength, + int dir, int damping, int h, size_t edges); + +/* --- Common helpers --- */ + +static volatile int g_stop = 0; +static pthread_barrier_t g_start; + +static inline uint64_t xs_step(uint64_t *s) { + uint64_t x = *s; x ^= x << 13; x ^= x >> 7; x ^= x << 17; return *s = x; +} +static uint64_t xs_init(uint64_t s) { return s ? s : 0xa57edbeef5717ULL; } +static double now_s(void) { + struct timespec t; clock_gettime(CLOCK_MONOTONIC_RAW, &t); + return t.tv_sec + t.tv_nsec * 1e-9; +} + +/* --- Kernel selectors --- */ + +enum kernel { K_MC, K_LPF4, K_LPF8, K_CDEF, K_IDCT }; + +static const char *kernel_name(enum kernel k) { + switch (k) { + case K_MC: return "mc"; + case K_LPF4: return "lpf4"; + case K_LPF8: return "lpf8"; + case K_CDEF: return "cdef"; + case K_IDCT: return "idct"; + } + return "?"; +} +static const char *kernel_unit(enum kernel k) { + return (k == K_LPF4 || k == K_LPF8) ? "Medge/s" : "Mblock/s"; +} + +/* --- NEON worker (per-kernel inline; pre-generate inputs, hot-loop) --- */ + +#define NEON_BATCH 8192 + +typedef struct { + int worker_id, affinity_core; + enum kernel kernel; + uint64_t units_done; + double elapsed_s; +} neon_args; + +static void neon_run_mc(uint64_t *seed, uint64_t *out_done) { + /* MC: SRC_BYTES=128 (8x16) per block; DST_BYTES=64. */ + uint8_t *src = malloc((size_t) NEON_BATCH * 128); + uint8_t *dst = malloc((size_t) NEON_BATCH * 64); + int *mx = malloc(NEON_BATCH * sizeof(int)); + for (int i = 0; i < NEON_BATCH; i++) { + for (int j = 0; j < 128; j++) src[i*128 + j] = (uint8_t)(xs_step(seed) & 0xff); + mx[i] = (int)(xs_step(seed) & 15); + } + while (!g_stop) { + for (int i = 0; i < NEON_BATCH; i++) + ff_vp9_put_regular8_h_neon(dst + i*64, 8, + src + i*128 + 3, 16, 8, mx[i], 0); + *out_done += NEON_BATCH; + } + free(src); free(dst); free(mx); +} + +static void neon_run_lpf(uint64_t *seed, uint64_t *out_done, int wd_8) { + uint8_t *master = malloc((size_t) NEON_BATCH * 64); + uint8_t *work = malloc((size_t) NEON_BATCH * 64); + int *Es = malloc(NEON_BATCH*sizeof(int)), *Is = malloc(NEON_BATCH*sizeof(int)), *Hs = malloc(NEON_BATCH*sizeof(int)); + for (int i = 0; i < NEON_BATCH; i++) { + for (int j = 0; j < 64; j++) master[i*64+j] = (uint8_t)(xs_step(seed) & 0xff); + Es[i] = (int)(xs_step(seed) % 81); + Is[i] = (int)(xs_step(seed) % 41); + Hs[i] = (int)(xs_step(seed) % 11); + } + while (!g_stop) { + memcpy(work, master, (size_t) NEON_BATCH * 64); + for (int i = 0; i < NEON_BATCH; i++) { + if (wd_8) ff_vp9_loop_filter_h_8_8_neon(work + i*64 + 4, 8, Es[i], Is[i], Hs[i]); + else ff_vp9_loop_filter_h_4_8_neon(work + i*64 + 4, 8, Es[i], Is[i], Hs[i]); + } + *out_done += NEON_BATCH; + } + free(master); free(work); free(Es); free(Is); free(Hs); +} + +static void neon_run_idct(uint64_t *seed, uint64_t *out_done) { + int16_t *blocks_master = malloc((size_t) NEON_BATCH * 64 * sizeof(int16_t)); + int16_t *blocks_work = malloc((size_t) NEON_BATCH * 64 * sizeof(int16_t)); + uint8_t *dsts = malloc((size_t) NEON_BATCH * 64); + int *eobs = malloc(NEON_BATCH * sizeof(int)); + for (int i = 0; i < NEON_BATCH; i++) { + memset(blocks_master + i*64, 0, 64*sizeof(int16_t)); + int n = 1 + (int)(xs_step(seed) % 16); + int eob = 0; + for (int j = 0; j < n; j++) { + int pos = (int)(xs_step(seed) % 64); + int16_t coef = (int16_t)((int)(xs_step(seed) % 8192) - 4096); + blocks_master[i*64 + pos] = coef; + if (pos + 1 > eob) eob = pos + 1; + } + eobs[i] = eob ? eob : 1; + } + while (!g_stop) { + memcpy(blocks_work, blocks_master, (size_t) NEON_BATCH * 64 * sizeof(int16_t)); + for (int i = 0; i < NEON_BATCH; i++) + ff_vp9_idct_idct_8x8_add_neon(dsts + i*64, 8, blocks_work + i*64, eobs[i]); + *out_done += NEON_BATCH; + } + free(blocks_master); free(blocks_work); free(dsts); free(eobs); +} + +static void *neon_worker(void *p) { + neon_args *a = p; + cpu_set_t cs; CPU_ZERO(&cs); CPU_SET(a->affinity_core, &cs); + pthread_setaffinity_np(pthread_self(), sizeof(cs), &cs); + + uint64_t seed = xs_init((uint64_t) a->worker_id * 0xc01dbeefULL); + + pthread_barrier_wait(&g_start); + double t0 = now_s(); + uint64_t done = 0; + switch (a->kernel) { + case K_MC: neon_run_mc(&seed, &done); break; + case K_LPF4: neon_run_lpf(&seed, &done, 0); break; + case K_LPF8: neon_run_lpf(&seed, &done, 1); break; + case K_IDCT: neon_run_idct(&seed, &done); break; + default: fprintf(stderr, "bad NEON kernel\n"); break; + } + a->elapsed_s = now_s() - t0; + a->units_done = done; + return NULL; +} + +/* --- QPU worker (CDEF / MC / LPF4 / LPF8 / IDCT) --- */ + +typedef struct { + int affinity_core, n_units; + enum kernel kernel; + uint64_t units_done; + double elapsed_s; +} qpu_args; + +/* Each QPU kernel has its own push-constant layout. */ +typedef struct { uint32_t n, dst_stride_u8, _pad0, _pad1; } pc_lpf; +typedef struct { uint32_t n, dst_stride_u8, src_stride_u8, _pad; } pc_mc; +/* IDCT: pc layout in v3d_idct8.comp = (n_blocks, blocks_per_row, dst_stride_u8, _pad) */ +typedef struct { uint32_t n_blocks, blocks_per_row, dst_stride_u8, _pad; } pc_idct; +/* CDEF: not yet — QPU CDEF kernel not implemented. CDEF QPU mode uses + * dav1d NEON via a single-thread NEON call on the QPU host core instead. + * That's a degenerate "QPU helper" but matches the deferred state of + * cycle 5. Real QPU CDEF kernel would replace this once cycle 5 closes. */ + +static void *qpu_cdef_neon_fallback(void *p) +{ + /* Cycle 5 doesn't have a working QPU CDEF kernel yet (M1 deferred). + * For Issue 003's purposes we test "the QPU host core running NEON + * CDEF" as a proxy for the QPU contribution. This UNDERSTATES the + * QPU helper value (since the QPU itself would parallelise more + * than 1 NEON core), but gives a defensible lower bound: if even + * NEON-on-the-spare-core helps the mixed throughput, QPU certainly + * would. + * + * TODO: once cycle 5 Phase 6 lands, swap this for the QPU dispatch. */ + qpu_args *a = p; + cpu_set_t cs; CPU_ZERO(&cs); CPU_SET(a->affinity_core, &cs); + pthread_setaffinity_np(pthread_self(), sizeof(cs), &cs); + + int n_blocks = a->n_units; + uint64_t seed = 0xcdef00000beefcULL; + + uint16_t *tmps = malloc((size_t) n_blocks * 192 * sizeof(uint16_t)); + uint8_t *dsts = malloc((size_t) n_blocks * 64); + int *pris = malloc(n_blocks*sizeof(int)); + int *secs = malloc(n_blocks*sizeof(int)); + int *dirs = malloc(n_blocks*sizeof(int)); + int *damps = malloc(n_blocks*sizeof(int)); + for (int i = 0; i < n_blocks; i++) { + for (int j = 0; j < 192; j++) tmps[i*192 + j] = (uint16_t)(xs_step(&seed) & 0xff); + for (int r = 0; r < 8; r++) for (int c = 0; c < 8; c++) + dsts[i*64 + r*8 + c] = (uint8_t) tmps[i*192 + (r+2)*16 + (c+2)]; + pris[i] = (int)(xs_step(&seed) % 7) + 1; + secs[i] = (int)(xs_step(&seed) % 4) + 1; + dirs[i] = (int)(xs_step(&seed) & 7); + damps[i] = (int)(xs_step(&seed) % 4) + 3; + } + + pthread_barrier_wait(&g_start); + double t0 = now_s(); + uint64_t done = 0; + while (!g_stop) { + for (int i = 0; i < n_blocks; i++) + dav1d_cdef_filter8_8bpc_neon(dsts + i*64, 8, + tmps + i*192, + pris[i], secs[i], dirs[i], damps[i], 8, 0); + done += n_blocks; + } + a->elapsed_s = now_s() - t0; + a->units_done = done; + + free(tmps); free(dsts); free(pris); free(secs); free(dirs); free(damps); + return NULL; +} + +/* QPU dispatch worker — generic for kernels with working shaders. */ + +typedef struct { + int affinity_core, n_units; + enum kernel kernel; + uint64_t units_done; + double elapsed_s; +} qpu_real_args; + +static void *qpu_real_worker(void *p) +{ + qpu_real_args *a = p; + cpu_set_t cs; CPU_ZERO(&cs); CPU_SET(a->affinity_core, &cs); + pthread_setaffinity_np(pthread_self(), sizeof(cs), &cs); + + v3d_runner *r = v3d_runner_create(); + if (!r) return NULL; + + int n_units = a->n_units; + const char *spv = NULL; + uint32_t bpw = 32; /* blocks/edges per WG */ + size_t dst_bytes = 0, meta_bytes = 0, src_bytes = 0; + int has_src = 0; + size_t per_unit = 0; + + switch (a->kernel) { + case K_LPF4: + case K_LPF8: { + spv = (a->kernel == K_LPF4) ? "v3d_lpf_h_4_8.spv" : "v3d_lpf_h_8_8.spv"; + per_unit = 64; + dst_bytes = (size_t) n_units * per_unit; + meta_bytes = (size_t) n_units * 4 * sizeof(uint32_t); + break; + } + case K_MC: + spv = "v3d_mc_8h.spv"; + dst_bytes = (size_t) n_units * 64; + src_bytes = (size_t) n_units * 128; + meta_bytes = (size_t) n_units * 4 * sizeof(uint32_t); + has_src = 1; + break; + case K_IDCT: + spv = "v3d_idct8.spv"; + dst_bytes = (size_t) n_units * 64; + src_bytes = (size_t) n_units * 64 * sizeof(int16_t); /* coeffs */ + meta_bytes = (size_t) n_units * 4 * sizeof(uint32_t); /* per-block pos */ + has_src = 1; + break; + default: + fprintf(stderr, "qpu_real_worker: unsupported kernel\n"); + v3d_runner_destroy(r); + return NULL; + } + + v3d_buffer buf_meta = {0}, buf_dst = {0}, buf_src = {0}; + v3d_runner_create_buffer(r, meta_bytes, &buf_meta); + v3d_runner_create_buffer(r, dst_bytes, &buf_dst); + if (has_src) v3d_runner_create_buffer(r, src_bytes, &buf_src); + + /* Synthesise meta + src + dst content based on kernel. */ + uint64_t seed = 0xfeed00000beefULL; + uint32_t *meta = buf_meta.mapped; + if (a->kernel == K_LPF4 || a->kernel == K_LPF8) { + for (int i = 0; i < n_units; i++) { + meta[4*i+0] = (uint32_t)((size_t)i * 64 + 4); /* dst_off */ + meta[4*i+1] = (uint32_t)(xs_step(&seed) % 81); /* E */ + meta[4*i+2] = (uint32_t)(xs_step(&seed) % 41); /* I */ + meta[4*i+3] = (uint32_t)(xs_step(&seed) % 11); /* H */ + } + for (size_t i = 0; i < dst_bytes; i++) + ((uint8_t *) buf_dst.mapped)[i] = (uint8_t)(xs_step(&seed) & 0xff); + } else if (a->kernel == K_MC) { + for (int i = 0; i < n_units; i++) { + meta[4*i+0] = (uint32_t)((size_t)i * 64); /* dst_off */ + meta[4*i+1] = (uint32_t)((size_t)i * 128); /* src_off (RAW) */ + meta[4*i+2] = (uint32_t)(xs_step(&seed) & 15); /* mx */ + meta[4*i+3] = 0; + } + for (size_t i = 0; i < src_bytes; i++) + ((uint8_t *) buf_src.mapped)[i] = (uint8_t)(xs_step(&seed) & 0xff); + } else if (a->kernel == K_IDCT) { + for (int i = 0; i < n_units; i++) { + meta[4*i+0] = (uint32_t)((size_t)i * 64); /* dst_off */ + meta[4*i+1] = (uint32_t)((i * 64) / 64); /* coeff_off (in blocks) */ + meta[4*i+2] = 0; /* eob (not used by our shader) */ + meta[4*i+3] = 0; + } + /* Fill coeffs with random VP9-ish values. */ + int16_t *cf = (int16_t *) buf_src.mapped; + size_t n_coefs = src_bytes / sizeof(int16_t); + for (size_t i = 0; i < n_coefs; i++) + cf[i] = (int16_t)((int)(xs_step(&seed) % 8192) - 4096); + } + + v3d_pipeline pipe = {0}; + int n_ssbos = has_src ? 3 : 2; + size_t pc_size = (a->kernel == K_MC) ? sizeof(pc_mc) : + (a->kernel == K_IDCT) ? sizeof(pc_idct) : sizeof(pc_lpf); + v3d_runner_create_pipeline(r, spv, n_ssbos, pc_size, &pipe); + + v3d_buffer bind_bufs[3]; + bind_bufs[0] = buf_meta; + bind_bufs[1] = buf_dst; + if (has_src) bind_bufs[2] = buf_src; + v3d_runner_bind_buffers(r, &pipe, bind_bufs, n_ssbos); + + uint32_t gc = (uint32_t)((n_units + bpw - 1) / bpw); + union { pc_lpf lpf; pc_mc mc; pc_idct idct; } pc = {0}; + if (a->kernel == K_LPF4 || a->kernel == K_LPF8) { + pc.lpf = (pc_lpf){ .n = n_units, .dst_stride_u8 = 8 }; + } else if (a->kernel == K_MC) { + pc.mc = (pc_mc){ .n = n_units, .dst_stride_u8 = 8, .src_stride_u8 = 16 }; + } else if (a->kernel == K_IDCT) { + pc.idct = (pc_idct){ .n_blocks = n_units, .blocks_per_row = 16, .dst_stride_u8 = 128 }; + } + + VkCommandBuffer cb = v3d_runner_alloc_cmdbuf(r); + VkCommandBufferBeginInfo cbbi = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; + vkBeginCommandBuffer(cb, &cbbi); + vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipe.pipeline); + vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, + pipe.layout, 0, 1, &pipe.desc_set, 0, NULL); + vkCmdPushConstants(cb, pipe.layout, VK_SHADER_STAGE_COMPUTE_BIT, + 0, pc_size, &pc); + vkCmdDispatch(cb, gc, 1, 1); + vkEndCommandBuffer(cb); + + for (int i = 0; i < 5; i++) v3d_runner_submit_wait(r, cb); + + pthread_barrier_wait(&g_start); + double t0 = now_s(); + uint64_t done = 0; + while (!g_stop) { + v3d_runner_submit_wait(r, cb); + done += n_units; + } + a->elapsed_s = now_s() - t0; + a->units_done = done; + + v3d_runner_destroy_pipeline(r, &pipe); + if (has_src) v3d_runner_destroy_buffer(r, &buf_src); + v3d_runner_destroy_buffer(r, &buf_dst); + v3d_runner_destroy_buffer(r, &buf_meta); + v3d_runner_destroy(r); + return NULL; +} + +/* --- Timer --- */ + +typedef struct { double duration_s; } timer_args; +static void *timer_thread(void *p) { + timer_args *a = p; + pthread_barrier_wait(&g_start); + double end = now_s() + a->duration_s; + while (now_s() < end) { + struct timespec ts = {0, 1000000}; nanosleep(&ts, NULL); + } + g_stop = 1; + return NULL; +} + +/* --- Main --- */ + +static enum kernel parse_kernel(const char *s) { + if (!strcmp(s, "mc")) return K_MC; + if (!strcmp(s, "lpf4")) return K_LPF4; + if (!strcmp(s, "lpf8")) return K_LPF8; + if (!strcmp(s, "cdef")) return K_CDEF; + if (!strcmp(s, "idct")) return K_IDCT; + fprintf(stderr, "unknown kernel: %s\n", s); exit(2); +} + +int main(int argc, char **argv) +{ + enum kernel cpu_k = K_MC, qpu_k = K_CDEF; + int n_neon = 3, qpu_core = 3, qpu_n_units = 65536; + double duration = 8.0; + + static struct option opts[] = { + {"cpu-kernel", required_argument, 0, 'c'}, + {"qpu-kernel", required_argument, 0, 'q'}, + {"neon-threads", required_argument, 0, 'n'}, + {"qpu-core", required_argument, 0, 'C'}, + {"qpu-units", required_argument, 0, 'u'}, + {"duration", required_argument, 0, 'd'}, + {0,0,0,0} + }; + for (int c; (c = getopt_long(argc, argv, "c:q:n:C:u:d:", opts, 0)) != -1;) { + switch (c) { + case 'c': cpu_k = parse_kernel(optarg); break; + case 'q': qpu_k = parse_kernel(optarg); break; + case 'n': n_neon = atoi(optarg); break; + case 'C': qpu_core = atoi(optarg); break; + case 'u': qpu_n_units = atoi(optarg); break; + case 'd': duration = atof(optarg); break; + default: return 2; + } + } + + /* CDEF on QPU side currently uses dav1d NEON fallback (cycle 5 + * Phase 6 not yet implemented). Real QPU CDEF would replace + * qpu_cdef_neon_fallback with qpu_real_worker. */ + int use_neon_fallback_for_cdef = (qpu_k == K_CDEF); + + int barrier_count = n_neon + 1 /* QPU */ + 1 /* timer */ + 1 /* main */; + printf("=== Issue 003 mixed-kernel M4 bench ===\n"); + printf(" cpu kernel: %s × %d threads (cores 0..%d)\n", + kernel_name(cpu_k), n_neon, n_neon - 1); + printf(" qpu kernel: %s on core %d (%s)\n", + kernel_name(qpu_k), qpu_core, + use_neon_fallback_for_cdef ? + "dav1d NEON fallback — real QPU CDEF deferred to cycle 5 Phase 6" : + "QPU dispatch"); + printf(" duration: %.1fs\n\n", duration); + + pthread_barrier_init(&g_start, NULL, barrier_count); + + pthread_t timer_tid; timer_args ta = { .duration_s = duration }; + pthread_create(&timer_tid, NULL, timer_thread, &ta); + + pthread_t neon_tids[16] = {0}; + neon_args n_args[16] = {0}; + for (int i = 0; i < n_neon; i++) { + n_args[i] = (neon_args){ .worker_id = i, .affinity_core = i, .kernel = cpu_k }; + pthread_create(&neon_tids[i], NULL, neon_worker, &n_args[i]); + } + + pthread_t qpu_tid = 0; + qpu_args q_args = {0}; + qpu_real_args qr_args = {0}; + if (use_neon_fallback_for_cdef) { + q_args = (qpu_args){ .affinity_core = qpu_core, .n_units = qpu_n_units, .kernel = qpu_k }; + pthread_create(&qpu_tid, NULL, qpu_cdef_neon_fallback, &q_args); + } else { + qr_args = (qpu_real_args){ .affinity_core = qpu_core, .n_units = qpu_n_units, .kernel = qpu_k }; + pthread_create(&qpu_tid, NULL, qpu_real_worker, &qr_args); + } + + pthread_barrier_wait(&g_start); + + pthread_join(timer_tid, NULL); + for (int i = 0; i < n_neon; i++) pthread_join(neon_tids[i], NULL); + pthread_join(qpu_tid, NULL); + + uint64_t cpu_total = 0; double cpu_max_e = 0; + printf("NEON workers (%s):\n", kernel_name(cpu_k)); + for (int i = 0; i < n_neon; i++) { + double r = n_args[i].units_done / n_args[i].elapsed_s / 1e6; + printf(" core %d: %.3f %s\n", n_args[i].affinity_core, r, kernel_unit(cpu_k)); + cpu_total += n_args[i].units_done; + if (n_args[i].elapsed_s > cpu_max_e) cpu_max_e = n_args[i].elapsed_s; + } + double cpu_rate = cpu_total / cpu_max_e / 1e6; + printf(" CPU aggregate: %.3f %s\n\n", cpu_rate, kernel_unit(cpu_k)); + + uint64_t qpu_done = use_neon_fallback_for_cdef ? q_args.units_done : qr_args.units_done; + double qpu_elapsed = use_neon_fallback_for_cdef ? q_args.elapsed_s : qr_args.elapsed_s; + double qpu_rate = qpu_done / qpu_elapsed / 1e6; + printf("QPU worker (%s on core %d):\n", kernel_name(qpu_k), qpu_core); + printf(" %.3f %s (%llu units / %.3f s)\n", + qpu_rate, kernel_unit(qpu_k), + (unsigned long long) qpu_done, qpu_elapsed); + + pthread_barrier_destroy(&g_start); + return 0; +}