lumbda/examples/cuda-fanout/plans/form-A-secp256k1-batched-mul.md
russell@unturf.com 4c5e04b07f
bend: planning docs for forms A, B, D (CATALOG.md build runway)
Three implementation plans land per CATALOG.md "Build order —
recommended 3-form runway":

  form-B-bignum-cgbn.md (3-5 dev-days, fewest unknowns):
    direct NVlabs CGBN consumption. 9-op dispatcher
    (mod-add/sub/mul/sqr/inv/exp/reduce + add-no-mod +
    mul-no-mod-2x-width). BSHK/BSHR mirrors shake256-fanout.
    15k crossover (wire overhead vs raw kernel). Risks:
    CGBN maintenance freshness + sm_86/sm_89 verification.

  form-A-secp256k1-batched-mul.md (6 dev-days):
    FixedPaul/VanitySearch-Bitcrack picked (AGPL-3, active
    2025-05-16) over JeanLucPons (GPL-3, stale 2022-04-04).
    Vendor 3 headers ~2400 LoC + write secp_mul_batch_kernel.
    BSPK/BSPR wire mirrors BSHK. Kernel extraction is the
    dominant risk.

  form-D-clifford-stabilizer.md (10 dev-days):
    STABSim & Qimax not open-source; clean-room
    Aaronson-Gottesman tableau recommended. Reuse QECCOPS1
    ops.bin over BSHK op_id=0x02 — no .stim translation.
    Toffoli option (b): score symbolically, simulate
    Clifford-only — gated on first measuring Toffoli fraction
    (point-add suspected ~50%, Amdahl eats gains if worse).

Plans live as actionable instructions in second-person voice;
each cites URLs for every claim. CATALOG.md & bend.html
untouched here — plans inform future surveys without inventing
numbers.
2026-06-05 13:25:37 -04:00

12 KiB
Raw Permalink Blame History

Form A — cuda-secp256k1-batched-mul implementation plan

Status: design only. No binary built. No remote GPU touched.

Reference catalog entry: ../CATALOG.md § A. Target: a secp256k1-batch-mul daemon binary that speaks the BSHK binary wire protocol the worker already drives for shake256-fanout, sitting next to it in examples/cuda-fanout/.

1. Upstream selection — pick FixedPaul/VanitySearch-Bitcrack

You would fork FixedPaul/VanitySearch-Bitcrack, not JeanLucPons. Reasons:

  • License. FixedPaul ships AGPL-3.0 (https://github.com/FixedPaul/VanitySearch-Bitcrack), matching shake256-fanout.cu's AGPLv3 header. JeanLucPons is GPL-3.0 (https://github.com/JeanLucPons/VanitySearch). AGPL ↔ AGPL stays a clean derivative.
  • Activity. FixedPaul last touched master 2025-05-16; JeanLucPons last commit 2022-04-04 (commits pages on each repo). Three years stale vs actively maintained.
  • Speed. FixedPaul publishes 6.9 Gkeys/s on 4090, 8.8 Gkeys/s on 5090 (repo README). Their "optimized CUDA modular math" patch over upstream is the win we want.

No 3090 number published; you would measure on 3090-ai.foxhop.net once a binary exists. Expect ~3 Gkeys/s extrapolating from 3080's 2.65 Gkeys/s in our catalog.

2. Kernel extraction — files you need

Upstream layout (https://github.com/JeanLucPons/VanitySearch/tree/master/GPU; fork preserves shape):

  • GPU/GPUMath.h (~900 lines) — __device__ modular arithmetic with inline PTX: _ModMult, _ModSqr, ModSub256, _ModInv, and crucially _ModInvGrouped (Montgomery batched inversion: one inverse per GRP_SIZE/2+1 points). Keep verbatim.
  • GPU/GPUGroup.h (~1024 lines) — precomputed Gx[][], Gy[][] for G, 2G, … 512G plus _2Gnx/_2Gny. Windowed-mul ladder. Keep verbatim.
  • GPU/GPUCompute.h (~450 lines) — ComputeKeys() Shamir-trick loop (P+iG and P-iG share one delta-x). Keep field arithmetic; strip SHA256 + RIPEMD160 + base58 + address-prefix branches.
  • GPU/GPUEngine.cu (~920 lines, 5 __global__ kernels: comp_keys, comp_keys_p2sh, comp_keys_comp, comp_keys_pattern, comp_keys_p2sh_pattern) — drop entirely. Write one new secp_mul_batch_kernel(scalars[N], base, points[N]) calling _ModInvGrouped.

Drop: main.cpp, Vanity.cpp, Base58.cpp, Bech32.cpp, Wildcard.cpp, hash/* (SSE host hashes), GPUBase58.h, GPUHash.h, GPUWildcard.h, GPUGenerate.cpp — 19 .cpp files per upstream Makefile, zero kept.

License: copy each file's header verbatim, add SPDX line, drop a LICENSE-vanitysearch-bitcrack beside our existing license note.

3. Wire protocol — BSHK request, BSHR response

Mirror the existing shake protocol (shake256-fanout.cu lines 286-292):

Request, little-endian throughout, no alignment padding:

magic       u8[4]   = "BSHK"
op_id       u32     = 0x01 (secp_mul_batch)
n           u32     = number of scalars
base_x      u8[32]  base point X (big-endian field element, secp256k1 native)
base_y      u8[32]  base point Y
scalars     u8[n*32]  big-endian 256-bit ints

Response:

magic       u8[4]   = "BSHR" on success, "BERR" on failure
status      u32     = 0 ok, nonzero error code (1 bad-magic, 2 bad-n,
                     3 cuda-launch-fail, 4 truncated-input)
n           u32     = echoed scalar count
points      u8[n*64]  N points, each (X32 || Y32) big-endian; affine,
                     infinity encoded as all-zero

Endianness: LE u32 headers (matches shake binary at shake256-fanout.cu:286), BE field elements (matches secp256k1 native; ships straight to openssl/python-ecdsa for validation). Worker's wire-send-raw treats payload as opaque (gpu-worker.lsp:264), no change there.

4. gpu-worker.lsp handler sketch

You add one handler analogous to handle-binary-shake at gpu-worker.lsp:253. The daemon stays warm for the whole worker lifetime — same pattern, same daemon-process helper:

(define *binary-secp-mul* "./secp256k1-batch-mul")

;; in main: register at boot, paying CUDA init once.
(register-daemon! 'cuda-secp-mul-batch *binary-secp-mul*)

;; new binary dispatcher: payload[0..4] = "BSPK" magic to disambiguate
;; from "BSHK" shake requests.
(define (handle-binary-secp-mul client payload)
  (let* ((daemon  (cdr (assoc 'cuda-secp-mul-batch *daemons*)))
         (in-path  (gensym-path "/tmp/bend-secp-in"  ".bin"))
         (out-path (gensym-path "/tmp/bend-secp-out" ".bin"))
         (blob (substring payload 4 (string-length payload))))
    (write-binary-file in-path blob)
    (let ((status (daemon-process daemon in-path out-path #t)))
      (cond
        ((eq? status 'ok)
         (let ((result (read-binary-file out-path)))
           (delete-file in-path) (delete-file out-path)
           (wire-send-raw client (string-append "BSPR" result))))
        (else
         (delete-file in-path) (delete-file out-path)
         (wire-send-raw client (string-append "BERR" (cdr status))))))))

Extend handle-one (gpu-worker.lsp:271) to branch on first 4 bytes: BSHK → shake, BSPK → secp-mul. S-expression path stays for ping and any text-mode ops. CUDA context init runs twice at startup, never per request.

5. Daemon protocol — match shake256-fanout exactly

secp256k1-batch-mul --daemon reads stdin one cmd per line, emits one response per line, identical to shake256-fanout.cu:226-269:

ready                      ; once at startup after cudaFree(0)
process-bin <in> <out>     ; binary request
done <out>                 ; success
error <reason>             ; failure
quit / bye                 ; shutdown

daemon-process in gpu-worker.lsp:71 already writes process-bin <in> <out>\n & expects done/error — zero handler-side change. start-daemon (line 62) waits for ready before serving, so CUDA init blocks worker readiness deterministically.

CLI fallback: secp256k1-batch-mul --binary <in> <out> for one-shot test (matches shake256-fanout.cu:272).

6. Build recipe

Two options:

  • A — submodule. git submodule add https://github.com/FixedPaul/VanitySearch-Bitcrack third_party/vanitysearch-bitcrack. Pro: trivial upstream-bump. Con: ~12 MB of unused .cpp / .vcxproj noise.
  • B — vendor three headers. Copy GPUMath.h, GPUGroup.h, GPUCompute.h into examples/cuda-fanout/vendor/secp256k1-vanitysearch/ with AGPL-3.0 + SPDX preserved, add a PROVENANCE.md carrying source commit SHA. Pro: lean, audit-friendly. Con: manual sync.

Pick B — ~2400 lines, mirrors how shake256-fanout.cu carries its tiny-sha3 distillation inline.

Makefile additions:

NVCCFLAGS_SECP ?= -O3 -arch=sm_86 -Xcompiler="-O3 -Wall" \
                  -maxrregcount=0 --ptxas-options=-v

all: shake256-fanout secp256k1-batch-mul

secp256k1-batch-mul: secp256k1-batch-mul.cu \
                     vendor/secp256k1-vanitysearch/GPUMath.h \
                     vendor/secp256k1-vanitysearch/GPUGroup.h \
                     vendor/secp256k1-vanitysearch/GPUCompute.h
	$(NVCC) $(NVCCFLAGS_SECP) -Ivendor/secp256k1-vanitysearch -o $@ $<

test-secp: secp256k1-batch-mul test_secp_roundtrip.py
	./secp256k1-batch-mul --binary /tmp/secp-in.bin /tmp/secp-out.bin
	python3 test_secp_roundtrip.py /tmp/secp-in.bin /tmp/secp-out.bin

bench-secp: secp256k1-batch-mul bench_secp.py
	python3 bench_secp.py ./secp256k1-batch-mul

sm_86 covers 3090; sm_89 covers 4090. Build a fat binary with -gencode arch=compute_86,code=sm_86 -gencode arch=compute_89,code=sm_89 for both fleet hosts.

Worker auto-registration: one line, (register-daemon! 'cuda-secp-mul-batch *binary-secp-mul*) next to the shake registration at gpu-worker.lsp:299.

7. Validation plan

Correctness. test_secp_roundtrip.py runs N=100 known scalars (1, 2, 3, …, plus edge cases: curve order n → infinity, n-1, scalars near 2^128, 2^256-1). Computes k*G on CPU via coincurve (or ecdsa.SECP256k1.generator * k), byte-compares GPU output. Halt on first mismatch. Cross-check at N=10 against openssl EC_POINT_mul to catch a coincurve defect masking a kernel defect.

Performance. bench_secp.py sweeps N ∈ {1k, 10k, 100k, 1M}, records wire_ms, kernel_ms (extend response to carry u32 kernel_us after n), gpu/cpu ratio. CPU baseline: in-process coincurve loop. Median over 20 runs. Format mirrors bench_tiers.py:174 so the summary table renders unchanged.

Expected: 3090 ~3 Gkeys/s, 4090 ~6.9 Gkeys/s. CPU baseline ~50 kkeys/s single-thread coincurve → ~60,000× per core, ~5,000× vs 12-thread. Below 1,000× signals a defect — most likely PCIe transfer saturating (1M × 64 B = 64 MB out, ~2 ms at PCIe 4.0).

8. Risks & open questions

  • License audit. FixedPaul declares AGPL-3.0 at repo level but GPU/*.h headers may still carry GPLv3 inherited from JeanLucPons. Audit each vendored file before commit. If a file is GPLv3-only, our binary becomes GPLv3 (still compatible with our AGPLv3 worker via §7 of GPLv3).
  • Endomorphism in fork. FixedPaul claims "optimized CUDA modular math" but lists no specifics. If the speedup is a private GPUMath.h patch, vendoring upstream headers alone loses it. Diff FixedPaul/GPU/GPUMath.h vs JeanLucPons/GPU/GPUMath.h first; take FixedPaul's.
  • Host RAM at N=1M. 1M × 64 B = 64 MB out, 1M × 32 B = 32 MB in. Worker holds ~3 copies (in-portal + daemon + out-portal): peak ~300 MB transient. Bounded on 24 GB / 80 GB hosts.
  • GRP_SIZE preservation. Upstream's GRP_SIZE=1024 means each thread does 1024 keys sequentially sharing one _ModInvGrouped call. A naive one-thread-per-scalar kernel loses the amortisation — ~50× worse. New kernel launches ceil(N/1024) threads, each handling a 1024-scalar group; N rounds up to multiple of 1024 with sentinel scalar = 1, pad outputs discarded.
  • Device memory. 1M-point request allocates ~96 MB device-side (in/out + scratch). cudaFree between requests to avoid fragmentation over 10k+ requests. Add explicit free in process_one_bin.

9. Effort estimate

Experienced CUDA dev, no fleet-side work counted:

step days
vendor headers, audit licenses, write PROVENANCE.md 0.5
write secp_mul_batch_kernel reusing _ModInvGrouped group loop 1.5
daemon shell (stdin loop, --binary, --daemon modes) — copy shake 0.5
binary I/O, BSHK/BSHR parse + emit, host-side scalar layout 0.5
Makefile, smoke test on local nvcc (host has no GPU — compile only) 0.5
test_secp_roundtrip.py + coincurve oracle + openssl cross-check 1.0
bench_secp.py + sweeps + first 3090/4090 measurement 1.0
gpu-worker.lsp handler + dispatch branch + integration test 0.5
catalog promotion (live row, measured speedup) 0.0 (fox's call)
total 6 days

Concentration: ~40% kernel-extraction (steps 1+2), ~25% validation, ~15% bench harness, ~10% daemon/wire, ~10% handler. Kernel-extraction risk dominates; everything else mirrors shake256-fanout.

URLs cited