diff --git a/examples/cuda-fanout/gpu-worker.lsp b/examples/cuda-fanout/gpu-worker.lsp index 1236a00..aa81ce3 100644 --- a/examples/cuda-fanout/gpu-worker.lsp +++ b/examples/cuda-fanout/gpu-worker.lsp @@ -426,8 +426,24 @@ (cpu-est-sec (cond ((> cpu-rate 0) (/ count cpu-rate 1000000.0)) (else 0))) + (cpu-est-min (/ cpu-est-sec 60.0)) (speedup (cond ((> gpu-rate 0) (/ gpu-rate cpu-rate)) - (else 0)))) + (else 0))) + (summary + (string-append + "✦ GPU just batched " (with-commas count) + " secp256k1 public-key computations.\n" + "\n" + " scalar gen : " (number->string gen-ms) " ms (random.urandom)\n" + " GPU kernel : " (number->string gpu-ms) " ms (RTX 3090, --window-w 4)\n" + " GPU throughput : " (format-float gpu-rate 2) " million keys / second\n" + "\n" + " CPU baseline ref: " (format-float cpu-rate 3) + " Mkeys/s (libsecp256k1 single core)\n" + " CPU would need : " (format-float cpu-est-min 1) " minutes (" + (format-float cpu-est-sec 0) " sec)\n" + " GPU finished in : " (format-float gpu-sec 1) " sec\n" + " speedup : ~" (format-float speedup 0) "× faster on GPU"))) (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=") @@ -441,7 +457,40 @@ (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 'speedup-est speedup) + (list 'summary summary))))))))))))) + +;; Format a non-negative integer with thousand-separator commas. +;; (with-commas 100000000) → "100,000,000". +(define (with-commas n) + (let ((s (number->string n))) + (cond + ((< (string-length s) 4) s) + (else + (let ((head-len (- (string-length s) 3))) + (string-append + (with-commas (string->number (substring s 0 head-len))) + "," + (substring s head-len (string-length s)))))))) + +;; Truncate a number's string form to at most n decimal places. Lumbda's +;; rational division of two ints returns an exact rational ("7/3") which +;; doesn't survive truncation, so we coerce to float first by adding 0.0 +;; before taking number->string. n=0 drops the decimal point too. +(define (format-float x n) + (let ((s (number->string (+ x 0.0)))) + (let loop ((i 0)) + (cond + ((>= i (string-length s)) s) + ((char=? (string-ref s i) #\.) + (cond + ((= n 0) (substring s 0 i)) + (else + (let ((cap (+ i 1 n))) + (cond + ((>= cap (string-length s)) s) + (else (substring s 0 cap))))))) + (else (loop (+ i 1))))))) ;;; -- op handler -- health -------------------------------------- ;;; diff --git a/wasm/app/demos/bend-gpu.lsp b/wasm/app/demos/bend-gpu.lsp index 318142d..98ede0e 100644 --- a/wasm/app/demos/bend-gpu.lsp +++ b/wasm/app/demos/bend-gpu.lsp @@ -4,18 +4,17 @@ ; above first. The playground guards the Run button so a customer ; machine never burns minutes on the local tiers. ; -; Today's headline payload: 100,000,000 secp256k1 scalar*G operations -; in one HTTP POST. On a 3090 the daemon finishes in ~13 seconds; +; Headline: 100,000,000 secp256k1 scalar*G operations dispatched in +; a single ~32-byte HTTP POST. The 3.2 GB of random scalars never +; crosses the wire — the worker synthesizes them from /dev/urandom, +; runs cuda-secp256k1-batched-mul, and ships a small formatted +; summary back. On a 3090 the daemon finishes the math in ~13 s; ; on a single CPU core (libsecp256k1 ~50K keys/s) the same workload -; would take ~33 minutes. That's a ~160× speedup — the kind of math -; only a GPU can do in a reasonable amount of time. +; would take more than half an hour. ; -; The request is ~32 bytes of S-expression. The 3.2 GB of random -; scalars never crosses the wire — the worker generates them locally -; from /dev/urandom via examples/cuda-fanout/generate-bscp.py, calls -; into the existing cuda-secp256k1-batched-mul daemon, and returns -; just the timing summary. You see the GPU work happen in your -; output panel; your laptop never does the math. +; The output panel shows the formatted result the worker built for +; us. read-from-string + assoc are portable across all three tiers +; so this same source file runs identically under asm / c / python. (display ";; 1. probe round-trip ...") (newline) (define r1 (bend!-call "(ping)")) @@ -27,13 +26,15 @@ (display " (health) → ") (display r2) (newline) (newline) -(display ";; 3. 100,000,000 secp256k1 scalar*G batch on the GPU") (newline) -(display ";; ~13 s on 3090 vs ~33 min on a CPU core (~160x)") (newline) -(display ";; sit tight — this is real compute, not a mock") (newline) -(define heavy "(cuda-secp256k1-bench 100000000)") -(display " payload: ") (display heavy) (newline) -(define r3 (bend!-call heavy)) -(display " result : ") (display r3) (newline) +(display ";; 3. heavy GPU compute ...") (newline) +(display ";; sit tight — this is real math, not a mock") (newline) +(newline) +(define r3 (bend!-call "(cuda-secp256k1-bench 100000000)")) +(define parsed (read-from-string r3)) +(define summary-pair (assoc 'summary (cdr parsed))) +(cond + ((pair? summary-pair) (display (car (cdr summary-pair))) (newline)) + (else (display "raw response: ") (display r3) (newline))) (newline) -(print "done — that math would have taken ~33 minutes on this CPU.") +(print "(your laptop never did the math — it just watched it complete)") diff --git a/www/playground/demos/bend-gpu.lsp b/www/playground/demos/bend-gpu.lsp index 318142d..98ede0e 100644 --- a/www/playground/demos/bend-gpu.lsp +++ b/www/playground/demos/bend-gpu.lsp @@ -4,18 +4,17 @@ ; above first. The playground guards the Run button so a customer ; machine never burns minutes on the local tiers. ; -; Today's headline payload: 100,000,000 secp256k1 scalar*G operations -; in one HTTP POST. On a 3090 the daemon finishes in ~13 seconds; +; Headline: 100,000,000 secp256k1 scalar*G operations dispatched in +; a single ~32-byte HTTP POST. The 3.2 GB of random scalars never +; crosses the wire — the worker synthesizes them from /dev/urandom, +; runs cuda-secp256k1-batched-mul, and ships a small formatted +; summary back. On a 3090 the daemon finishes the math in ~13 s; ; on a single CPU core (libsecp256k1 ~50K keys/s) the same workload -; would take ~33 minutes. That's a ~160× speedup — the kind of math -; only a GPU can do in a reasonable amount of time. +; would take more than half an hour. ; -; The request is ~32 bytes of S-expression. The 3.2 GB of random -; scalars never crosses the wire — the worker generates them locally -; from /dev/urandom via examples/cuda-fanout/generate-bscp.py, calls -; into the existing cuda-secp256k1-batched-mul daemon, and returns -; just the timing summary. You see the GPU work happen in your -; output panel; your laptop never does the math. +; The output panel shows the formatted result the worker built for +; us. read-from-string + assoc are portable across all three tiers +; so this same source file runs identically under asm / c / python. (display ";; 1. probe round-trip ...") (newline) (define r1 (bend!-call "(ping)")) @@ -27,13 +26,15 @@ (display " (health) → ") (display r2) (newline) (newline) -(display ";; 3. 100,000,000 secp256k1 scalar*G batch on the GPU") (newline) -(display ";; ~13 s on 3090 vs ~33 min on a CPU core (~160x)") (newline) -(display ";; sit tight — this is real compute, not a mock") (newline) -(define heavy "(cuda-secp256k1-bench 100000000)") -(display " payload: ") (display heavy) (newline) -(define r3 (bend!-call heavy)) -(display " result : ") (display r3) (newline) +(display ";; 3. heavy GPU compute ...") (newline) +(display ";; sit tight — this is real math, not a mock") (newline) +(newline) +(define r3 (bend!-call "(cuda-secp256k1-bench 100000000)")) +(define parsed (read-from-string r3)) +(define summary-pair (assoc 'summary (cdr parsed))) +(cond + ((pair? summary-pair) (display (car (cdr summary-pair))) (newline)) + (else (display "raw response: ") (display r3) (newline))) (newline) -(print "done — that math would have taken ~33 minutes on this CPU.") +(print "(your laptop never did the math — it just watched it complete)")