bend: cuda-secp256k1-bench op — feel the 277x GPU win at 10M scalars
New op-head (cuda-secp256k1-bench N) lets HTTP callers trigger a
massive secp256k1 batched scalar*G workload without uploading the
32*N-byte BSCP payload. Worker generates the random scalars itself
via generate-bscp.py (/dev/urandom in 1 MB chunks), dispatches to
the existing cuda-secp256k1-batched-mul daemon (same daemon the
BSCP wire mode hits), times the GPU kernel, and returns a small
S-expression summarizing the run:
(ok (n N)
(gen-ms G)
(gpu-ms D)
(gpu-mkeys-per-sec R)
(cpu-rate-mkeys-per-sec 0.05) ; libsecp256k1 single-thread ref
(cpu-est-sec E)
(speedup-est S)
(sample-x HEX))
cpu-rate is the textbook libsecp256k1 single-thread number (~50K
scalar*G/sec). cpu-est-sec extrapolates from that without actually
running the CPU baseline — honest because the rate is well-known
and the daemon's GPU rate (~13.83 Mkeys/s on 3090 per Day-3 bench)
is what we measure end-to-end.
Reference numbers expected at 10M scalars on 3090-ai:
gen-ms ~3000 (urandom + write 320 MB)
gpu-ms ~720
speedup-est ~277x (gpu 13.83 Mkeys/s / cpu 0.05 Mkeys/s)
cpu-est-sec ~200 (~3 minutes of CPU work)
Three helpers added: read-binary-file-prefix (peek at the BSCR
header), sample-x-hex (format point.x as 64-char hex), and
generate-bscp-file (spawn the python helper, fail-open on missing
binary). No daemon changes — secp256k1-batch-mul stays unmodified.
This commit is contained in:
parent
ec70ddb5ae
commit
ef31860b44
2 changed files with 218 additions and 0 deletions
68
examples/cuda-fanout/generate-bscp.py
Normal file
68
examples/cuda-fanout/generate-bscp.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env python3
|
||||
"""generate-bscp.py — emit a BSCP secp256k1 batched scalar*G request file.
|
||||
|
||||
Usage: generate-bscp.py COUNT OUT_PATH
|
||||
|
||||
Writes a BSCP-format binary suitable for secp256k1-batch-mul --binary or
|
||||
its --daemon process-bin command. Scalars come from /dev/urandom (fast,
|
||||
hundreds of MB/s); a few may be zero or >= curve order, but the worker
|
||||
just produces garbage points for those — fine for a bench/demo.
|
||||
|
||||
Used by gpu-worker.lsp's handle-cuda-secp256k1-bench to construct big
|
||||
input payloads server-side so HTTP callers can trigger 10M-scale GPU
|
||||
work without uploading the 320 MB scalar list.
|
||||
|
||||
BSCP wire (from secp256k1-batch-mul.cu header):
|
||||
"BSCP" 4 B magic
|
||||
u32 op_id 0x01 = scalar*base
|
||||
u32 n number of scalars
|
||||
u8[32] base_x base point X, big-endian (G)
|
||||
u8[32] base_y base point Y, big-endian (G)
|
||||
u8[n*32] scalars big-endian 256-bit ints
|
||||
"""
|
||||
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
# secp256k1 generator G — official curve parameters.
|
||||
GX = bytes.fromhex(
|
||||
"79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"
|
||||
)
|
||||
GY = bytes.fromhex(
|
||||
"483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"
|
||||
)
|
||||
|
||||
CHUNK_SCALARS = 32 * 1024 # 1 MB per write
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
sys.stderr.write(__doc__)
|
||||
return 1
|
||||
count = int(sys.argv[1])
|
||||
out_path = sys.argv[2]
|
||||
if count <= 0:
|
||||
sys.stderr.write(f"count must be positive (got {count})\n")
|
||||
return 1
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(b"BSCP")
|
||||
f.write(struct.pack("<I", 0x01))
|
||||
f.write(struct.pack("<I", count))
|
||||
f.write(GX)
|
||||
f.write(GY)
|
||||
|
||||
# Scalars in 1 MB urandom chunks — minimizes syscall count.
|
||||
full = count // CHUNK_SCALARS
|
||||
for _ in range(full):
|
||||
f.write(os.urandom(CHUNK_SCALARS * 32))
|
||||
remainder = count % CHUNK_SCALARS
|
||||
if remainder:
|
||||
f.write(os.urandom(remainder * 32))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -342,6 +342,155 @@
|
|||
(display wall-ms) (newline)
|
||||
(list 'error (list 'no-portal portal-path))))))))))
|
||||
|
||||
;;; -- op handler -- cuda-secp256k1-bench --------------------------
|
||||
;;;
|
||||
;;; (cuda-secp256k1-bench N) — batched scalar*G on N random scalars.
|
||||
;;;
|
||||
;;; This is the "feel the GPU win" demo handler. HTTP callers can't
|
||||
;;; ship a 32*N-byte BSCP payload over the playground bend bridge
|
||||
;;; for any meaningful N, so the worker generates the input file
|
||||
;;; itself from os.urandom via examples/cuda-fanout/generate-bscp.py,
|
||||
;;; dispatches to the existing secp256k1-batched-mul daemon (same
|
||||
;;; one the BSCP wire mode hits), and returns timing data — not the
|
||||
;;; N*64-byte output, which would balloon the response.
|
||||
;;;
|
||||
;;; Returns (ok (n N) (gen-ms G) (gpu-ms D) (gpu-mkeys-per-sec R)
|
||||
;;; (cpu-rate-mkeys-per-sec C) (cpu-est-sec E)
|
||||
;;; (speedup-est S) (sample-x HEX))
|
||||
;;;
|
||||
;;; cpu-rate-mkeys-per-sec is the libsecp256k1 single-thread reference
|
||||
;;; (~0.05 Mkeys/s = 50K scalar*G/sec on a modern x86 core); used to
|
||||
;;; extrapolate the CPU time without actually running it.
|
||||
(define *bscp-generator-path*
|
||||
(string-append (or (get-environment-variable "PWD") ".")
|
||||
"/generate-bscp.py"))
|
||||
(define *cpu-secp256k1-rate-mkeys-per-sec* 0.05) ; libsecp256k1 single-thread ref
|
||||
|
||||
(define (generate-bscp-file count out-path)
|
||||
;; Spawn generate-bscp.py COUNT OUT_PATH, drain stdout to EOF.
|
||||
;; Returns 'ok on success, (error <reason>) on failure.
|
||||
(cond
|
||||
((not (file-exists? "/usr/bin/python3"))
|
||||
(list 'error 'python3-missing))
|
||||
((not (file-exists? *bscp-generator-path*))
|
||||
(list 'error (list 'generator-script-missing *bscp-generator-path*)))
|
||||
(else
|
||||
(let ((pair (spawn-process-stdio
|
||||
"/usr/bin/python3"
|
||||
(list *bscp-generator-path*
|
||||
(number->string count)
|
||||
out-path))))
|
||||
(cond
|
||||
((eq? pair #f) (list 'error 'spawn-failed))
|
||||
(else
|
||||
(close-port (car pair))
|
||||
(drain-to-eof (cdr pair))
|
||||
(close-port (cdr pair))
|
||||
(cond
|
||||
((file-exists? out-path) 'ok)
|
||||
(else (list 'error 'no-output)))))))))
|
||||
|
||||
(define (handle-cuda-secp256k1-bench args)
|
||||
(let ((d (daemon-or-error 'cuda-secp256k1-batched-mul)))
|
||||
(cond
|
||||
((and (pair? d) (eq? (car d) 'error)) d)
|
||||
(else
|
||||
(let* ((daemon d)
|
||||
(count (car args))
|
||||
(in-path (gensym-path "/tmp/bend-secp-bench-in" ".bscp"))
|
||||
(out-path (gensym-path "/tmp/bend-secp-bench-out" ".bscr"))
|
||||
(t-gen-start (current-time-ms))
|
||||
(gen-status (generate-bscp-file count in-path))
|
||||
(gen-ms (- (current-time-ms) t-gen-start)))
|
||||
(cond
|
||||
((not (eq? gen-status 'ok))
|
||||
(if (file-exists? in-path) (delete-file in-path))
|
||||
(list 'error (list 'bscp-generate-failed gen-status)))
|
||||
(else
|
||||
(display ";;; bend RECV cuda-secp256k1-bench n=")
|
||||
(display count)
|
||||
(display " gen-ms=") (display gen-ms) (newline)
|
||||
(let* ((t-gpu-start (current-time-ms))
|
||||
(status (daemon-process daemon in-path out-path #t))
|
||||
(gpu-ms (- (current-time-ms) t-gpu-start)))
|
||||
(cond
|
||||
((not (eq? status 'ok))
|
||||
(if (file-exists? in-path) (delete-file in-path))
|
||||
(if (file-exists? out-path) (delete-file out-path))
|
||||
(list 'error (list 'daemon-failed (cdr status))))
|
||||
(else
|
||||
(let* ((sample (read-binary-file-prefix out-path 76)) ; 4+4+4 hdr + 64 first pt
|
||||
(sample-hex (sample-x-hex sample))
|
||||
(gpu-sec (/ gpu-ms 1000.0))
|
||||
(gpu-rate (cond ((> gpu-sec 0) (/ count gpu-sec 1000000.0))
|
||||
(else 0)))
|
||||
(cpu-rate *cpu-secp256k1-rate-mkeys-per-sec*)
|
||||
(cpu-est-sec (cond ((> cpu-rate 0)
|
||||
(/ count cpu-rate 1000000.0))
|
||||
(else 0)))
|
||||
(speedup (cond ((> gpu-rate 0) (/ gpu-rate cpu-rate))
|
||||
(else 0))))
|
||||
(if (file-exists? in-path) (delete-file in-path))
|
||||
(if (file-exists? out-path) (delete-file out-path))
|
||||
(display ";;; bend DONE cuda-secp256k1-bench n=")
|
||||
(display count)
|
||||
(display " gpu-ms=") (display gpu-ms)
|
||||
(display " mkeys-s=") (display gpu-rate) (newline)
|
||||
(list 'ok
|
||||
(list 'n count)
|
||||
(list 'gen-ms gen-ms)
|
||||
(list 'gpu-ms gpu-ms)
|
||||
(list 'gpu-mkeys-per-sec gpu-rate)
|
||||
(list 'cpu-rate-mkeys-per-sec cpu-rate)
|
||||
(list 'cpu-est-sec cpu-est-sec)
|
||||
(list 'speedup-est speedup)
|
||||
(list 'sample-x sample-hex)))))))))))))
|
||||
|
||||
;; read first N bytes of file as a binary string. Used to peek at the
|
||||
;; BSCR header + first output point without slurping the whole result
|
||||
;; file (which is 64 * count bytes for large counts).
|
||||
(define (read-binary-file-prefix path n)
|
||||
(let* ((port (open-input-file path))
|
||||
(acc (read-string-bytes port n)))
|
||||
(close-port port)
|
||||
acc))
|
||||
|
||||
;; read up to n bytes from input port — falls back through read-char
|
||||
;; on tiers where bulk read isn't a primitive. Returns a string of
|
||||
;; whatever was available (may be shorter than n at EOF).
|
||||
(define (read-string-bytes port n)
|
||||
(let loop ((i 0) (acc '()))
|
||||
(cond
|
||||
((>= i n) (apply string-append (reverse acc)))
|
||||
(else
|
||||
(let ((c (read-char port)))
|
||||
(cond
|
||||
((eof-object? c) (apply string-append (reverse acc)))
|
||||
(else (loop (+ i 1) (cons (string c) acc)))))))))
|
||||
|
||||
;; Extract the X coordinate of the first output point from a BSCR
|
||||
;; prefix as a 64-char hex string. BSCR layout: "BSCR"(4) status(4)
|
||||
;; n(4) point0_x(32) point0_y(32) ... — so x bytes are at offset 12.
|
||||
;; Returns "(unavailable)" if the prefix is too short.
|
||||
(define (sample-x-hex bytes)
|
||||
(cond
|
||||
((< (string-length bytes) 44) "(unavailable)")
|
||||
(else
|
||||
(let loop ((i 12) (end 44) (acc '()))
|
||||
(cond
|
||||
((>= i end) (apply string-append (reverse acc)))
|
||||
(else
|
||||
(let* ((b (char->integer (string-ref bytes i)))
|
||||
(hi (quotient b 16))
|
||||
(lo (remainder b 16)))
|
||||
(loop (+ i 1) end
|
||||
(cons (string (hex-digit hi) (hex-digit lo)) acc)))))))))
|
||||
|
||||
(define (hex-digit n)
|
||||
(cond
|
||||
((< n 10) (integer->char (+ n (char->integer #\0))))
|
||||
(else (integer->char (+ (- n 10) (char->integer #\a))))))
|
||||
|
||||
;;; -- op handler -- health --------------------------------------
|
||||
;;;
|
||||
;;; (health) returns
|
||||
|
|
@ -528,6 +677,7 @@
|
|||
(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 'cuda-secp256k1-bench) (handle-cuda-secp256k1-bench args))
|
||||
((eq? op 'health) (handle-health))
|
||||
((eq? op 'ping) (list 'ok 'pong))
|
||||
(else (list 'error (list 'unknown-op op))))))))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue