lumbda/examples/cuda-fanout/DESIGN-ecdsa-integration.md
russell@unturf.com aa428cdeb2
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
2026-06-04 20:08:40 -04:00

4.6 KiB
Raw Blame History

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):

;; 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

;; 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:

(define (eval-variant variant-id cases)
  (let ((circ (emit-variant variant-id ...)))
    (simulate circ)        ; in-process lumbda sim.lsp
    ...))

With bend:

(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.