OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev / bench_cpu_tile.py

103 lines · python ref:
#!/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()