From 78ff89fb8b4fc12fbc259ec4acfe7f1012b0c953 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 5 Jun 2026 10:18:11 -0400 Subject: [PATCH] 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. --- CLAUDE.md | 23 +++++++ examples/cuda-fanout/gpu-worker.lsp | 93 +++++++++++++++++++++++++++++ www/index.html | 2 + 3 files changed, 118 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b0cbdc2..e80cf4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/examples/cuda-fanout/gpu-worker.lsp b/examples/cuda-fanout/gpu-worker.lsp index f051f97..13ec103 100644 --- a/examples/cuda-fanout/gpu-worker.lsp +++ b/examples/cuda-fanout/gpu-worker.lsp @@ -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 ) +;;; Spawns demo_ops --portal , 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 ); 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)))))))) diff --git a/www/index.html b/www/index.html index 5341faa..7d8346d 100644 --- a/www/index.html +++ b/www/index.html @@ -106,6 +106,8 @@ make gpu-worker LUMBDA=asm # smallest footprint

Three tiers, three operating points (S-expression mode): C tier wins at small & medium scales (8× faster than Python); asm tier hits 0.21 ms at very small inputs (~30% behind C, 6× 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.

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 pipe2 + fork + execve syscalls — no libc anywhere on the chain.

See examples/cuda-fanout/ for the wire contract, daemon protocol, bench data, and per-tier integration sketch.

+

Real workload — ecdsafail search

+

Beyond hash fan-out, bend now dispatches quantum-reversible circuit scoring for our secp256k1 point-addition challenge work at foxhop.net/ecdsa. Lumbda emits an upstream-format ops.bin from a Phase B Roetteler 12-step circuit, calls (bend!-call '(cuda-sim-ops-bin path 141)), & 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.