lumbda/examples/cuda-fanout/bench_binary.py
russell@unturf.com 731a9e5319
examples/cuda-fanout: bend primitive + binary format + GPU now wins
Three changes that together make the GPU primitive viable for the
go-gpu/bend pattern:

1. Binary portal format (length-prefixed raw bytes) — eliminates the
   hex-string parse that ate 99% of wall time. Old text portal at
   262 MB workload spent 421 sec parsing; binary format = native
   speed. New flag + daemon command:

     shake256-fanout --binary <in.bin> <out.bin>
     daemon: process-bin <in.bin> <out.bin>

   Wire (in):  u32 out_bytes | u32 n | (u32 len | len bytes) × n
   Wire (out): u32 n | u32 out_bytes | n × out_bytes

2. bend primitive (Lisp-smart GPU dispatch). Picked 'bend' over
   {go, spark, cast, fan} per fox — HVM2 lineage, fits the
   'reshape compute for GPU' mental model.

     (bend (cuda-shake-fanout inputs 32))
       → runtime inspects expr; routes to GPU worker if cost-est
         exceeds threshold AND worker reachable; else evaluates
         locally in original lexical scope
     (bend! expr)
       → force GPU, error if no worker available

   Implementation files:
     bend.lsp        — macro + cost-estimator-based router
     gpu-worker.lsp  — TCP listener, dispatches over warm daemons
     DESIGN-go-gpu.md — full architecture (already shipped)

   Tier-specific helpers (tcp-*, spawn-process-stdio, sexp->string)
   are noted as TODO per tier — Python uses subprocess + socket,
   C uses fork + portal, asm uses syscall fork + sock_stream.

3. bench_binary.py — combined daemon + binary format benchmark.
   GPU wins every cell of the grid by 1.5–10×:

     in_sz   N           total    host    dev   speedup
     32      1,000,000   32 MB    470 ms   47 ms  10.11x
     32      100,000     3.2 MB    47 ms    5 ms   9.95x
     1024    100,000     102 MB   177 ms   79 ms   2.24x
     16384   10,000      164 MB   231 ms  124 ms   1.86x
     262144  1,000       262 MB   363 ms  231 ms   1.57x

   Same workloads that lost 0.00× at hex+per-spawn now win 10× at
   binary+daemon. 4000× relative perf swing from fixing wire format
   and warming the context.

The peak 10× at small-input × high-N is the natural shape of crypto
protocols (commitments, Fiat-Shamir, PoW search). That's the win
zone for cuda-shake-fanout. README updated with the full table.
2026-06-04 18:58:59 -04:00

124 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""bench_binary.py — binary portal format vs text portal.
Same workloads as bench.py, but uses the new binary input/output
format that skips hex parsing entirely. This is where GPU should
actually start winning at heavy workloads."""
import hashlib
import os
import secrets
import struct
import subprocess
import sys
import time
BIN = sys.argv[1] if len(sys.argv) > 1 else "./shake256-fanout"
OUT_BYTES = 32
def write_binary_input(path, inputs_bytes, out_bytes):
"""Format: u32 out_bytes | u32 n | (u32 len | len bytes) × n"""
with open(path, "wb") as fh:
fh.write(struct.pack("<II", out_bytes, len(inputs_bytes)))
for blob in inputs_bytes:
fh.write(struct.pack("<I", len(blob)))
fh.write(blob)
def read_binary_output(path):
"""Format: u32 n | u32 out_bytes | (out_bytes bytes) × n"""
with open(path, "rb") as fh:
n, out_bytes = struct.unpack("<II", fh.read(8))
return [fh.read(out_bytes) for _ in range(n)]
def host_bench(inputs_bytes, out_bytes):
t0 = time.monotonic()
out = [hashlib.shake_256(b).digest(out_bytes) for b in inputs_bytes]
return out, time.monotonic() - t0
def device_bench(inputs_bytes, out_bytes, use_daemon_proc=None):
in_p = "/tmp/cf-bin-in.bin"
out_p = "/tmp/cf-bin-out.bin"
write_binary_input(in_p, inputs_bytes, out_bytes)
t0 = time.monotonic()
if use_daemon_proc is not None:
use_daemon_proc.stdin.write(f"process-bin {in_p} {out_p}\n")
use_daemon_proc.stdin.flush()
line = use_daemon_proc.stdout.readline().strip()
if not line.startswith("done"):
raise RuntimeError(f"daemon error: {line}")
else:
subprocess.run([BIN, "--binary", in_p, out_p], check=True)
wall = time.monotonic() - t0
out = read_binary_output(out_p)
os.unlink(in_p); os.unlink(out_p)
return out, wall
GRID = [
# (input_size_bytes, N)
(32, 10_000),
(32, 100_000),
(32, 1_000_000),
(1024, 1_000),
(1024, 10_000),
(1024, 100_000),
(16 * 1024, 1_000),
(16 * 1024, 10_000),
(256 * 1024, 100),
(256 * 1024, 1_000),
]
def main():
print(f"binary: {BIN}")
print(f"out_bytes: {OUT_BYTES}")
print()
# Start a daemon once so we exclude the 200ms init from the
# interesting per-call columns.
proc = subprocess.Popen([BIN, "--daemon"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
ready = proc.stdout.readline().strip()
assert ready == "ready", f"daemon ready={ready!r}"
print(f"{'in_sz':>10} {'N':>9} {'total':>9} {'host ms':>9} "
f"{'dev (1shot)':>11} {'dev (daem)':>10} {'spd_daem':>9} match")
print("-" * 94)
sweet = []
for sz, N in GRID:
inputs_bytes = [secrets.token_bytes(sz) for _ in range(N)]
host_out, host_t = host_bench(inputs_bytes, OUT_BYTES)
# daemon
dev_d_out, dev_d_t = device_bench(inputs_bytes, OUT_BYTES, use_daemon_proc=proc)
# one-shot
dev_1_out, dev_1_t = device_bench(inputs_bytes, OUT_BYTES)
match = host_out == dev_d_out == dev_1_out
speedup_d = host_t / dev_d_t if dev_d_t else float("inf")
total = sz * N
total_str = f"{total/1e6:.1f}MB" if total >= 1e6 else f"{total/1e3:.0f}KB"
print(f"{sz:>10} {N:>9,} {total_str:>9} {host_t*1e3:>9.0f} "
f"{dev_1_t*1e3:>11.0f} {dev_d_t*1e3:>10.0f} {speedup_d:>8.2f}x {match}")
if speedup_d > 1.0:
sweet.append((sz, N, total, host_t, dev_d_t))
proc.stdin.write("quit\n"); proc.stdin.flush()
proc.stdout.readline(); proc.wait()
print()
if sweet:
print("=== GPU wins (daemon mode, binary format) ===")
for sz, N, tot, ht, dt in sweet:
print(f" input={sz:>8} × N={N:>7,} ({tot/1e6:>6.1f} MB):"
f" host {ht*1e3:>6.0f} ms → device {dt*1e3:>6.0f} ms ({ht/dt:.2f}×)")
else:
print("=== GPU still loses everywhere ===")
print()
print("Columns: dev (1shot) includes 200 ms cuda init each call.")
print(" dev (daem) reuses warm daemon — the real production cost.")
if __name__ == "__main__":
main()