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