# Form A Day-4 progress — v3+v2 stack lands byte-identical but regresses 12% at n=1M; daemon flips to v3

Status: **v4 kernel landed correctness-clean, throughput-regressed. Daemon default flips to v3 (`--window-w 4`), v3 stays canonical worker path.** v3 remains best-measured kernel; v4 stays in tree as future warp-scan launchpad.

## What shipped

`secp256k1-batch-mul.cu` now carries four kernel variants:

- `secp_mul_batch_kernel_v1` (Day-1) — per-thread Jacobian double-and-add, per-thread `_ModInv`.
- `v2_phase_a..d` (Day-2 batch-inv pipeline, binary ladder Phase A).
- `secp_mul_batch_kernel_v3<W>` (Day-3) — per-thread windowed-base ladder, per-thread `_ModInv`.
- `v4_phase_a<W>` (Day-4) — windowed-base ladder Phase A, **stacked with v2_phase_b/c/d for batch inversion**.

CLI matrix:

| flags                              | variant |
|-----------------------------------|---------|
| (none)                             | v1      |
| `--batch-inv`                      | v2      |
| `--window-w 4`                     | v3      |
| `--window-w 4 --batch-inv`         | v4      |

## Validation — byte-identical at every N

Coincurve oracle on 3090-ai, default seed 0xC0FFEE:

```
v4 known-small  n=8      PASS  [v4-window-w4+batch-inv]
v4 random n=32           PASS
v4 random n=1000         PASS
v4 random n=10000        PASS
v4 random n=100000       PASS
v4 random n=1000000      PASS
```

Math is correct across all four variants. The HARD RULE on byte-identity holds; v4 commits because the kernel ships clean.

## Throughput — v4 regresses 12% vs v3 at n=1M

3090-ai best-of-3, kernel ms (after one warm-up):

| n         | v1 ms  | v1 Mkeys/s | v3 ms  | v3 Mkeys/s | v4 ms  | v4 Mkeys/s | v4/v1 | v4/v3 |
|-----------|--------|------------|--------|------------|--------|------------|-------|-------|
|    10,000 |   2.53 |      3.95  |   4.64 |      2.16  |   5.59 |      1.79  | 0.45x | 0.83x |
|   100,000 |  13.45 |      7.44  |  10.84 |      9.22  |  12.42 |      8.05  | 1.08x | 0.87x |
| 1,000,000 | 127.29 |      7.86  |  72.32 |     13.83  |  82.27 |     12.16  | 1.55x | 0.88x |

v4 beats v1 at n >= 100k (1.55x at n=1M) but **regresses 12% vs v3 at n=1M**. HARD-RULE threshold was "v4 must beat v3 by >= 10%". v4 fails that threshold → no v4 promotion to daemon default.

## Why v4 lost to v3

Day-3 progress doc predicted: "Once v3 lands, the bottleneck shifts BACK toward scalar_mul's residual cost + per-thread inversion. The Day-2 batch-inv idea now becomes viable as a v3+v2 stacked variant." That prediction was half-right. The stacked v4 does shave the per-thread ModInv cost (~9 ms of work disappears at n=1M, because Phase B + C + D collectively run in ~10 ms vs v3's ~73 ms total). But v2's Phase B/D walks add ~20 ms of single-thread-per-block serial work that exceeds the inversion savings.

Concretely at n=1M with v2_block_size=256:
- Phase A (v4 windowed scalar_mul, all 1M threads parallel): ~62 ms — same as v3's ladder cost.
- Phase B (3,906 blocks, 1 thread each, per-block 256-mult serial): ~7-8 ms.
- Phase C (1 thread total, 3906-block walk + 1 ModInv): ~1 ms.
- Phase D (3,906 blocks, 1 thread each, per-block 256-mult + 256-affine-proj serial): ~12 ms.

Total ~82 ms vs v3's 72 ms. The single ModInv saved is roughly 0.3 ms (one ModInv on a 3090 takes microseconds when not blocking other threads). The per-block serial walks dwarf that saving.

This is exactly the failure mode Day-2 progress doc called out: "Phase B and Phase D use 1 thread per block. At n=1M that's 3906 active threads vs v1's 1M threads. 3090 has 82 SMs x 1536 max-resident-threads = ~125k concurrent threads; v2 leaves >97% of SM throughput idle in the prefix and suffix passes."

v3's per-thread ModInv saturates 1M parallel threads on those 82 SMs. The math wins. The threading model loses.

## What v4 needs to actually beat v3

The same Day-2 follow-up that was deferred then:

**Warp-level prefix scan inside Phase B + suffix walk in Phase D.** Kogge-Stone or Sklansky scan with 32 threads/warp. At n=1M with block_size=256, each warp would scan 8 elements — turn the serial 256-mult chain into 8 mults of warp-coordinated work. Estimated Phase B cost drops from ~7 ms to <1 ms; Phase D drops similarly. That should bring v4 to ~65 ms total, beating v3's 72 ms.

That refactor is multi-day work (cross-warp shuffles, careful boundary handling, lane-mask discipline). Defer to Day-5 if fox keeps the v4 frontier alive.

## Daemon flip — v3 wins, daemon default now `--window-w 4`

v3 vs v1 at n=1M = 1.73x. That meets the daemon-flip threshold. Daemon spawns now carry `--window-w 4`.

Two options were sketched in task brief:
- **Option A**: extend `start-daemon` arglist (minimal code change in lumbda).
- **Option B**: wrap binary in a `/tmp/secp-daemon-wrapper.sh` (per-host configurable).

Chose **Option A**: add a `secp-extra-args` parameter to `gpu-worker.lsp`, modify `maybe-register-daemon!` for the secp daemon to pass `'("--daemon" "--window-w" "4")`. Keeps all configuration inside the lumbda source — single source of truth.

## Files touched

- `examples/cuda-fanout/secp256k1-batch-mul.cu` — added `v4_phase_a<W>` kernel template + v4 dispatch branch in `process_one_bin` + variant label + usage string.
- `examples/cuda-fanout/gpu-worker.lsp` — added `start-daemon` arglist override, plumbed `--window-w 4` into the secp256k1 daemon spawn.
- `examples/cuda-fanout/CATALOG.md` — promoted form A throughput number from v3 13.60 Mkeys/s to v3-confirmed 13.83 Mkeys/s @ n=1M.
- `www/bend.html` — same throughput promotion.

## Rebuild + deploy

```
ssh 3090-ai.foxhop.net 'cd /home/fox/git/lumbda/examples/cuda-fanout && rm -f secp256k1-batch-mul && make secp256k1-batch-mul'
ssh ai.foxhop.net      'cd /home/fox/git/lumbda/examples/cuda-fanout && rm -f secp256k1-batch-mul && make secp256k1-batch-mul'
rsync gpu-worker.lsp -> both
bash /tmp/launch.sh on both
```
