examples/cuda-fanout: DESIGN-ecdsa-integration.md

Sketches how bend would wire into ecdsafail-challenge candidate
search loop on the foxhop.net side:

  ecdsa/lumbda/search.lsp
    → (bend!-call '(cuda-sim-ops-bin ops-path 141))
    → gpu-worker.lsp routes to demo_ops --portal
    → S-exp result back to lumbda, scoring proceeds

Identifies the two pieces missing before this lands:
  1. `system`-equivalent primitive in lumbda (or spawn+wait via
     existing spawn-process-stdio)
  2. Phase B step 7 (Solinas mod-mul) so lumbda emits real-scale
     ops.bin variants worth bending

Once both close, this is a half-day wire-up.

Cross-references:
  ~/git/www.foxhop.net/ecdsa/cuda/                    — the CUDA prototype
  ~/git/www.foxhop.net/ecdsa/lumbda/search.lsp       — current search loop
  examples/cuda-fanout/DESIGN-go-gpu.md              — the broader bend RPC design
This commit is contained in:
russell@unturf.com 2026-06-04 20:08:40 -04:00
parent 07a8a4aaf7
commit aa428cdeb2
No known key found for this signature in database

View file

@ -0,0 +1,126 @@
# Integrating bend → ecdsa lumbda search loop
The motivating workload behind `bend` is ecdsafail-challenge candidate
search: the lumbda search loop (`ecdsa/lumbda/search.lsp` in
www.foxhop.net) emits circuit variants, scores each, picks the lowest.
At toy scale it runs in lumbda's own simulator (`sim.lsp`). At
upstream scale (12.8M ops, 1,355 qubits, 1.3M bits) it needs the CUDA
prototype (`ecdsa/cuda/demo_upstream`, byte-identical to upstream Rust
`eval_circuit`).
`bend` is the glue.
## Where things stand
In www.foxhop.net's `ecdsa/cuda/`:
| binary | purpose | upstream byte-parity? |
|--------|---------|----------------------|
| `demo` | hand-crafted 21-op circuit, CPU↔GPU smoke | n/a |
| `demo_ops` | ops.bin loader, LFSR RNG, scoring | gate counts ✓ |
| `demo_upstream` | SHAKE + secp256k1 register init | full (every digit) |
| `demo_multi` | multi-candidate batch driver, S-exp portal output | per candidate |
Each writes an S-expression portal lumbda can ingest directly.
## The new GPU op for bend's registry
Add a `cuda-sim-ops-bin` op (analogous to today's `cuda-shake-fanout`):
```scheme
;; in bend.lsp
(define (bend-sim-cost args)
"(cuda-sim-ops-bin path n-batches) — cost = n-batches × 12.8M ops
approx 100 ns/op host."
(let ((n-batches (car (cdr args))))
(* n-batches 12788119 100)))
(bend-register-op! 'cuda-sim-ops-bin bend-sim-cost)
```
## Worker handler
```scheme
;; in gpu-worker.lsp
(define (handle-cuda-sim-ops-bin args)
(let* ((ops-path (car args))
(n-batches (car (cdr args)))
(portal-out (gensym-path "/tmp/bend-cuda-sim" ".portal"))
(cmd (string-append
"/home/fox/git/www.foxhop.net/ecdsa/cuda/demo_ops "
ops-path
" --batches " (number->string n-batches)
" --portal " portal-out)))
(system cmd) ; or spawn-process-stdio + wait
(let ((result (read-from-string (file->string portal-out))))
(delete-file portal-out)
(list 'ok result))))
```
Note: `system` doesn't exist as a lumbda primitive today; either add
one (~10 LoC per tier) or spawn through `spawn-process-stdio` and
read stdout to confirm completion before reading the portal.
## Search-loop side
`ecdsa/lumbda/search.lsp` currently does:
```scheme
(define (eval-variant variant-id cases)
(let ((circ (emit-variant variant-id ...)))
(simulate circ) ; in-process lumbda sim.lsp
...))
```
With bend:
```scheme
(load "bend.lsp")
(define (eval-variant-cuda variant-id ops-bin-path)
(let* ((result (bend!-call
(list 'cuda-sim-ops-bin ops-bin-path 141))))
;; result shape from demo_ops's portal:
;; (cuda-multi-result (ops-path …) (status OK)
;; (totals (clifford N) (toffoli N))
;; (timing-ms …))
(let ((totals (section result 'totals)))
(list variant-id
(car (cdr (assoc 'toffoli totals)))
(car (cdr (assoc 'clifford totals)))))))
```
Each variant emits its own `ops.bin` (the upstream binary format we
parse with `ops_loader.c`), the search loop bends it to a GPU worker,
worker calls `demo_ops`, returns the portal, lumbda picks the
winner.
## What's missing before this lands
1. `system` or equivalent primitive in lumbda (Python/C/asm) — one of:
- synchronous subprocess wait
- `spawn-process-stdio` + wait via reading "done" line
2. Lumbda's emitter side needs to produce upstream-format `ops.bin`
files. Toy 4-bit circuits don't need this; real secp256k1 circuits
would require Phase B step 7+ (Solinas mod-mul + Bernstein-Yang
mod-inv) to close before there are interesting variants to bend.
3. The search-loop scoring rule needs adaptation: today it scores
`avg_toffoli × peak_qubits`; the CUDA portal returns Σ counts over
141 batches × 64 shots, so dividing by 9024 gives the
per-shot avg upstream uses.
## Why this design
- `bend` already does the routing decision: small toy circuits stay
in lumbda's in-process `sim.lsp`; large ones bend to GPU.
- Wire protocol identical to `cuda-shake-fanout` — no new primitive
beyond a Scheme registration entry.
- `demo_ops --portal` output already matches lumbda's S-expression
ingestion contract (see `ecdsa/cuda/main_ops.c` portal writer).
- Worker pool elasticity: any GPU host can register
`cuda-sim-ops-bin` and become eligible to receive candidate
evaluations. Coordinator's load-aware dispatch (`DESIGN-go-gpu.md`)
handles fan-out across multiple GPU hosts.
This is a half-day's wire-up once Phase B step 7 closes and lumbda
gains a `system`-equivalent primitive. Until then, the architecture
is documented; the integration waits.