bend: cuda-sim-ops-bin handler + per-call CPU/GPU telemetry

gpu-worker.lsp gains a cuda-sim-ops-bin op handler that spawns
demo_ops from www.foxhop.net/ecdsa/cuda via spawn-process-stdio,
drains stdout, & parses our (cuda-sim-result ...) portal back.

Each call now emits two log lines:
;;; bend RECV cuda-sim-ops-bin ops=PATH n-batches=N t-ms=...
;;; bend DONE cuda-sim-ops-bin n-batches=N wall-ms=W cpu-ms=C gpu-ms=G mismatches=0 gpu/cpu=R

so we can tell how fast bend jobs run on CPU vs GPU per call.

CLAUDE.md & www/index.html mention this integration is now live
end-to-end across our fleet.
This commit is contained in:
russell@unturf.com 2026-06-05 10:18:11 -04:00
parent 2919a2255d
commit 78ff89fb8b
No known key found for this signature in database
3 changed files with 118 additions and 0 deletions

View file

@ -44,6 +44,29 @@ Then ask fox about our mission.
| GNU asm | `asm/` | `make asm-build` | `make asm-test` | `make asm-repl` |
| All | — | — | `make test-all` | — |
## Bend — GPU dispatch primitive
`examples/cuda-fanout/` ships `(bend ...)` — runtime decides per call
whether to evaluate locally or ship to a CUDA worker over our wire
protocol. Two wire modes:
- **S-expression mode** (text) — for small payloads. Slow above ~1k
inputs because parser cost dominates.
- **Binary mode** (magic `BSHK` + raw bytes) — for huge payloads. 150x
faster than S-exp at 1M inputs; bends past host hashlib by 12x.
Workers run on any tier (`make gpu-worker LUMBDA={c,python,asm}`).
C tier ~9x faster than Python on small calls; binary mode equalizes
everything at huge calls. Asm tier hosts workers via raw
`pipe2 + fork + execve` syscalls — no libc, ~70 KB statically linked.
The integration with `www.foxhop.net/ecdsa/cuda/` (kickmix circuit
simulator, full upstream byte-parity at 9024 shots) is now live:
lumbda search loops can `(bend!-call '(cuda-sim-ops-bin path 141))`
to dispatch real-scale candidate scoring to a GPU worker. The Phase B
1-8 secp256k1 arithmetic landed on the foxhop side this session, so
the substrate has every piece it needs.
## Test Suites
- Python unit/integration: `tests.py` (571 tests)

View file

@ -29,6 +29,13 @@
;; Override via env or per host.
"./shake256-fanout")
;; demo_ops lives in the foxhop ecdsa repo and runs upstream-format
;; ops.bin against the CPU+GPU simulators, writing (cuda-sim-result …)
;; portals via --portal. No daemon mode: spawn-per-call.
(define *binary-demo-ops*
(or (get-environment-variable "DEMO_OPS")
"/home/fox/git/www.foxhop.net/ecdsa/cuda/demo_ops"))
(define (parse-port-arg args)
(let loop ((rest args))
(cond
@ -140,6 +147,91 @@
(delete-file in-path) (delete-file out-path)
(list 'error (cdr status)))))))
;;; -- op handler -- cuda-sim-ops-bin -----------------------------
;;;
;;; Request: (cuda-sim-ops-bin <ops-bin-path> <n-batches>)
;;; Spawns demo_ops <path> <n-batches> --portal <tmp>, drains its
;;; stdout to EOF (process exit), reads the portal, returns the
;;; parsed (cuda-sim-result ...) form unchanged so callers can pull
;;; out gates / phase / timings.
(define (drain-to-eof port)
;; Read until read-line returns #f (or eof-object). Discard lines.
(let loop ()
(let ((line (read-line port)))
(cond
((eq? line #f) #t)
((eof-object? line) #t)
(else (loop))))))
;; Pull a numeric value out of a parsed portal expression for telemetry.
;; Walks (a b c ...) looking for (name <number>); returns #f if missing.
(define (portal-find-number form name)
(let loop ((rest (cdr form)))
(cond
((null? rest) #f)
((and (pair? (car rest)) (eq? (car (car rest)) name)
(pair? (cdr (car rest))) (number? (car (cdr (car rest)))))
(car (cdr (car rest))))
(else (loop (cdr rest))))))
;; Walk through (timing-ms (cpu-total X) (gpu-kernel Y)) shape.
(define (portal-timing form which)
(let loop ((rest (cdr form)))
(cond
((null? rest) #f)
((and (pair? (car rest)) (eq? (car (car rest)) 'timing-ms))
(portal-find-number (car rest) which))
(else (loop (cdr rest))))))
(define (handle-cuda-sim-ops-bin args)
(let* ((ops-path (car args))
(n-batches (car (cdr args)))
(portal-path (gensym-path "/tmp/bend-sim-ops" ".portal"))
(t-start (current-time-ms)))
(cond
((not (file-exists? ops-path))
(display ";;; bend ERROR cuda-sim-ops-bin ops-bin-missing ")
(display ops-path) (newline)
(list 'error (list 'ops-bin-missing ops-path)))
(else
(display ";;; bend RECV cuda-sim-ops-bin ops=")
(display ops-path)
(display " n-batches=") (display n-batches)
(display " t-ms=") (display t-start) (newline)
(let* ((argv (list ops-path
(number->string n-batches)
"--portal" portal-path))
(pair (spawn-process-stdio *binary-demo-ops* argv)))
;; demo_ops does not read stdin; drain stdout until exit.
(drain-to-eof (cdr pair))
(close-port (car pair))
(close-port (cdr pair))
(let ((wall-ms (- (current-time-ms) t-start)))
(cond
((file-exists? portal-path)
(let* ((result (read-from-string (file->string portal-path)))
(cpu-ms (portal-timing result 'cpu-total))
(gpu-ms (portal-timing result 'gpu-kernel))
(mismatches (portal-find-number result 'mismatches)))
(delete-file portal-path)
(display ";;; bend DONE cuda-sim-ops-bin")
(display " n-batches=") (display n-batches)
(display " wall-ms=") (display wall-ms)
(display " cpu-ms=") (display (or cpu-ms 'NA))
(display " gpu-ms=") (display (or gpu-ms 'NA))
(display " mismatches=") (display (or mismatches 'NA))
(cond
((and (number? cpu-ms) (number? gpu-ms) (> gpu-ms 0))
(display " gpu/cpu=")
(display (/ cpu-ms gpu-ms))))
(newline)
(list 'ok result)))
(else
(display ";;; bend FAIL cuda-sim-ops-bin no-portal wall-ms=")
(display wall-ms) (newline)
(list 'error (list 'no-portal portal-path))))))))))
;;; -- dispatch --------------------------------------------------
(define (handle-request sexp)
@ -149,6 +241,7 @@
(let ((op (car sexp)) (args (cdr sexp)))
(cond
((eq? op 'cuda-shake-fanout) (handle-cuda-shake-fanout args))
((eq? op 'cuda-sim-ops-bin) (handle-cuda-sim-ops-bin args))
((eq? op 'ping) (list 'ok 'pong))
(else (list 'error (list 'unknown-op op))))))))

View file

@ -106,6 +106,8 @@ make gpu-worker LUMBDA=asm # smallest footprint</code></pre>
<p>Three tiers, three operating points (S-expression mode): C tier wins at small &amp; medium scales (8&times; faster than Python); asm tier hits 0.21 ms at very small inputs (~30% behind C, 6&times; faster than Python, 70 KB statically linked, zero libc); Python tier scales linearly (~22 µs per input) all the way through 1 M inputs but runs slowly on a single core. The S-exp CLIFFs at 10k (C) and 1k (asm) are tier-internal reader limits — binary mode bypasses them entirely.</p>
<p>The CUDA toolchain stays isolated to the leaf binary the worker spawns. No tier links libcudart; no tier requires nvcc at build time. Asm tier hosts workers through hand-written <code>pipe2 + fork + execve</code> syscalls — no libc anywhere on the chain.</p>
<p>See <a href="https://git.unturf.com/engineering/unturf/lumbda/-/blob/master/examples/cuda-fanout/README.md">examples/cuda-fanout/</a> for the wire contract, daemon protocol, bench data, and per-tier integration sketch.</p>
<h3>Real workload — ecdsafail search</h3>
<p>Beyond hash fan-out, bend now dispatches quantum-reversible circuit scoring for our secp256k1 point-addition challenge work at <a href="https://www.foxhop.net/ecdsa.html">foxhop.net/ecdsa</a>. Lumbda emits an upstream-format <code>ops.bin</code> from a Phase B Roetteler 12-step circuit, calls <code>(bend!-call '(cuda-sim-ops-bin path 141))</code>, & receives Σ Clifford / Σ Toffoli totals back from a GPU worker over our binary wire — same cross-tier validation, same byte-identical portal contract that the hash demo proves. Search loops on any host tier ship candidate scoring to whichever fleet node holds a warm CUDA context.</p>
</section>
<section id="proof">