Cycle 5 closed: CDEF QPU R5=0.116 ORANGE, opportunistic helper

Phase 4 plan with 3 Phase-5 REDs applied inline:
  - meta layout: m.z=tmp_off, m.w=dir
  - sec_shift clamped to >=0 (NEON uqsub semantics)
  - directions table as const ivec2[14], not OR-packed

Phase 6 deliverable: v3d_cdef.comp (387 inst, 2 threads, no spills).
3-way M1 (QPU vs C ref vs NEON) PASS 4096/4096.

M2: 0.443 Mblock/s -> R5 = 0.116 ORANGE (predicted 0.02-0.05 RED).
M4 same-kernel: NEON-3+QPU 8.46 < NEON-4 alone ~10 (negative).
M4 mixed (NEON-3 MC + QPU CDEF): CPU 34.17 Mblock/s MC,
  QPU 0.42 Mblock/s CDEF helper. CPU side higher than the
  Issue 003 NEON-fallback proxy suggested - cross-substrate
  contention is gentler than same-side NEON contention.

Verdict: CDEF stays on CPU; QPU dispatch path exists for
opportunistic use. Deployment recipe table updated for all 5
cycles. Phase 9 lessons: linear extrapolation across cycles is
too pessimistic; CDEF is bandwidth-bound on NEON despite high
per-block ns; real-substrate-cross contention < NEON-proxy
contention.

- src/v3d_cdef.comp: cycle 5 QPU shader
- tests/bench_v3d_cdef.c: 3-way M1, M2 bench
- tests/bench_concurrent_mixed.c: K_CDEF on both sides
- tests/cdef_ref.c + bench_neon_cdef.c: sec_shift clamp +
  expanded damping range to exercise the edge case
- CMakeLists.txt: v3d_cdef.spv + bench_v3d_cdef wiring
- docs/k5_cdef_phase4.md updated with Phase 5 review applied
- docs/k5_cdef_phase7.md: closure doc with full verdict matrix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 13:52:46 +00:00
parent 1740e7c165
commit 5223d3cb3f
8 changed files with 849 additions and 36 deletions
+64 -13
View File
@@ -134,6 +134,31 @@ static void neon_run_lpf(uint64_t *seed, uint64_t *out_done, int wd_8) {
free(master); free(work); free(Es); free(Is); free(Hs);
}
static void neon_run_cdef(uint64_t *seed, uint64_t *out_done) {
int n = NEON_BATCH;
uint16_t *tmps = malloc((size_t) n * 192 * sizeof(uint16_t));
uint8_t *dsts = malloc((size_t) n * 64);
int *pris = malloc(n*sizeof(int)), *secs = malloc(n*sizeof(int));
int *dirs = malloc(n*sizeof(int)), *damps = malloc(n*sizeof(int));
for (int i = 0; i < n; 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) % 6) + 1;
}
while (!g_stop) {
for (int i = 0; i < n; i++)
dav1d_cdef_filter8_8bpc_neon(dsts + i*64, 8,
tmps + i*192 + (2*16+2),
pris[i], secs[i], dirs[i], damps[i], 8, 0);
*out_done += n;
}
free(tmps); free(dsts); free(pris); free(secs); free(dirs); free(damps);
}
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));
@@ -175,6 +200,7 @@ static void *neon_worker(void *p) {
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;
case K_CDEF: neon_run_cdef(&seed, &done); break;
default: fprintf(stderr, "bad NEON kernel\n"); break;
}
a->elapsed_s = now_s() - t0;
@@ -194,8 +220,8 @@ typedef struct {
/* 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;
typedef struct { uint32_t n_blocks, tmp_stride_u16, dst_stride_u8, _pad; } pc_cdef;
/* 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
@@ -296,8 +322,16 @@ static void *qpu_real_worker(void *p)
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 */
src_bytes = (size_t) n_units * 64 * sizeof(int16_t);
meta_bytes = (size_t) n_units * 4 * sizeof(uint32_t);
has_src = 1;
break;
case K_CDEF:
spv = "v3d_cdef.spv";
bpw = 4;
dst_bytes = (size_t) n_units * 64;
src_bytes = (size_t) n_units * 192 * sizeof(uint16_t);
meta_bytes = (size_t) n_units * 4 * sizeof(uint32_t);
has_src = 1;
break;
default:
@@ -334,22 +368,37 @@ static void *qpu_real_worker(void *p)
((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+0] = (uint32_t)((size_t)i * 64);
meta[4*i+1] = (uint32_t)((i * 64) / 64);
meta[4*i+2] = 0;
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);
} else if (a->kernel == K_CDEF) {
uint16_t *tmps = (uint16_t *) buf_src.mapped;
for (int i = 0; i < n_units; i++) {
uint32_t pri = (uint32_t)((xs_step(&seed) % 7) + 1);
uint32_t sec = (uint32_t)((xs_step(&seed) % 4) + 1);
uint32_t damping = (uint32_t)((xs_step(&seed) % 6) + 1);
meta[4*i+0] = (uint32_t)((size_t)i * 64);
meta[4*i+1] = pri | (sec << 8) | (damping << 16);
meta[4*i+2] = (uint32_t)((size_t)i * 192 + (2*16 + 2));
meta[4*i+3] = (uint32_t)(xs_step(&seed) & 7);
for (int j = 0; j < 192; j++)
tmps[(size_t)i * 192 + j] = (uint16_t)(xs_step(&seed) & 0xff);
}
for (size_t i = 0; i < dst_bytes; i++)
((uint8_t *) buf_dst.mapped)[i] = (uint8_t)(xs_step(&seed) & 0xff);
}
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);
(a->kernel == K_IDCT) ? sizeof(pc_idct) :
(a->kernel == K_CDEF) ? sizeof(pc_cdef) : sizeof(pc_lpf);
v3d_runner_create_pipeline(r, spv, n_ssbos, pc_size, &pipe);
v3d_buffer bind_bufs[3];
@@ -359,13 +408,15 @@ static void *qpu_real_worker(void *p)
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};
union { pc_lpf lpf; pc_mc mc; pc_idct idct; pc_cdef cdef; } 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 };
} else if (a->kernel == K_CDEF) {
pc.cdef = (pc_cdef){ .n_blocks = n_units, .tmp_stride_u16 = 16, .dst_stride_u8 = 8 };
}
VkCommandBuffer cb = v3d_runner_alloc_cmdbuf(r);
@@ -451,10 +502,10 @@ int main(int argc, char **argv)
}
}
/* 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);
/* Cycle 5 Phase 6 landed — v3d_cdef.spv is M1-PASS. Use real
* QPU dispatch for CDEF too. The NEON-fallback worker remains
* compiled but is unselected. */
int use_neon_fallback_for_cdef = 0;
int barrier_count = n_neon + 1 /* QPU */ + 1 /* timer */ + 1 /* main */;
printf("=== Issue 003 mixed-kernel M4 bench ===\n");