#!/usr/bin/env python3
"""
HOW LONG DOES ONE TILE TAKE ON THE CPU?
The number the whole background-generation question turns on, and nobody had
taken it. The GPU is the renderer's; a generator that shares it costs frames
(the model holds 976-3522 MiB resident and bursts 85-92% for seconds per tile).
CPU is the only option that does not contend — IF it is fast enough.
A TIMER WITH NO CORRECTNESS GATE MEASURES NOTHING. #50 section 2.1 is the
session where a 14x speedup turned out to be returning 70% exact zeros. So each
timed tile is compared against the SAME tile in the cache, which was generated
by the CUDA service at seed 1234. That gate also answers a second question that
matters more than the timing:
DO CPU AND GPU AGREE, AND HOW CLOSELY?
Peer generation is built on tiles being a pure function of (seed, tx, ty) with
the content hash AS the verification. That was measured across machines, not
across DEVICES. If CPU and CUDA differ at all, then a CPU-generated tile has a
different hash from a GPU-generated one and the two cannot verify each other —
which is a design constraint, not a rounding detail.
python bench_cpu_tile.py [--reps 3]
"""
import argparse, os, re, sys, time
import numpy as np
from server import fetch_diffusion
# Reads LOOKDEV_TILE_CACHE like the other tools -- it did NOT before, so it
# silently benchmarked a different tile set than stitch_block/server saw.
CACHE = os.environ.get("LOOKDEV_TILE_CACHE",
os.path.expanduser(
"~/.local/share/godot/app_userdata/HyperTPS/tile_cache/1234"))
TILE_PX = 256
def cached(tx, ty):
p = os.path.join(CACHE, f"t2_256_{tx}_{ty}.npy")
return np.load(p).astype(np.float64) if os.path.exists(p) else None
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--cpu-url", default="http://127.0.0.1:8830")
ap.add_argument("--gpu-url", default="http://127.0.0.1:8000")
ap.add_argument("--reps", type=int, default=3)
ap.add_argument("--tiles", default="0,0 1,0 0,1")
a = ap.parse_args()
tiles = []
for t in a.tiles.split():
tx, ty = t.split(",")
tiles.append((int(tx), int(ty)))
area_km2 = (TILE_PX * 30.0 / 1000.0) ** 2
print(f"tile {TILE_PX}px @ 30 m = {TILE_PX*30/1000:.2f} km square "
f"= {area_km2:.1f} km2\n")
rows = []
for tx, ty in tiles:
ref = cached(tx, ty)
if ref is None:
print(f" ({tx},{ty}) not in cache — skipped (no oracle, no measurement)")
continue
for rep in range(a.reps):
t0 = time.perf_counter()
elev, clim = fetch_diffusion(a.cpu_url, tx * TILE_PX, ty * TILE_PX, TILE_PX)
dt = time.perf_counter() - t0
# The cache holds the tile WITHOUT its apron; compare the overlap.
h = min(elev.shape[0], ref.shape[0]); w = min(elev.shape[1], ref.shape[1])
d = elev[:h, :w] - ref[:h, :w]
rows.append((tx, ty, rep, dt, float(np.abs(d).max()),
float(np.sqrt((d ** 2).mean())),
int(np.count_nonzero(d)), d.size,
0 if clim is None else clim.shape[2]))
print(f" ({tx:>2},{ty:>2}) rep {rep}: {dt:7.2f} s "
f"max|d| {np.abs(d).max():8.2f} m rmse {np.sqrt((d**2).mean()):7.3f} m "
f"{'IDENTICAL' if not np.any(d) else 'differs'}"
f" climate ch {0 if clim is None else clim.shape[2]}")
if not rows:
raise SystemExit("no tiles measured")
ts = np.array([r[3] for r in rows])
print(f"\n CPU seconds per tile: median {np.median(ts):.2f} "
f"min {ts.min():.2f} max {ts.max():.2f} (n={len(ts)})")
print(f" throughput: {area_km2/np.median(ts):.2f} km2/s "
f"(CUDA measured ~50 km2/s in #50)")
worst = max(r[4] for r in rows)
ident = all(r[6] == 0 for r in rows)
print(f"\n CPU vs CUDA: worst |diff| {worst:.2f} m, "
f"{'BIT-IDENTICAL' if ident else 'DIFFERENT BYTES'}")
if not ident:
print(" => a CPU tile and a GPU tile of the same (seed,tx,ty) do NOT share\n"
" a content hash. Cross-device peers cannot verify each other by\n"
" hash; the tile key must name the device/pipeline, or one device\n"
" must be canonical for a given world.")
if __name__ == "__main__":
main()