OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev commits / 25a4701

optimisation: autocast fp16 is the first lever that is both correct and faster
fp16 failed earlier because the pipeline does a BLANKET model.to(float16), which puts the sampler arithmetic in half precision along with the convolutions -- sigmas, the EDM solver, and the signed-square elevation encoding whose derivative is 2x, so a small error at x=70 arrives as metres. Measured against the fp32 reference that was a DIFFERENT world: corr 0.925, RMSE 436.66 m on a field whose sd is 364 m. torch.autocast keeps the weights in fp32 and casts only the activations inside the forward, with fp32 accumulation. Same tensor cores, no sampler drift: blanket .to(fp16) corr 0.925421 RMSE 436.66 m DIFFERENT WORLD autocast fp16 corr 0.999997 RMSE 0.89 m IDENTICAL Added as TD_AUTOCAST=fp16|bf16 on the local clone; unset reproduces upstream exactly, verified by the control still returning rmse 0.00 m. SPEED, and the first attempt at this was not trustworthy. The fp32 control had read 11.96, 24.35 and 21.53 s at different points -- 2x variance on one config, because the GPU thermally throttles to 1410 MHz of 2100 as it heats. A single A-then-B comparison cannot survive that. Alternating on fresh regions: round 1 fp32 8.07 s autocast 5.68 s 1.42x round 2 fp32 11.91 s autocast 8.11 s 1.47x round 3 fp32 16.54 s autocast 9.01 s 1.83x Autocast wins every round; absolute times climb together as the GPU heats. WHERE THE TIME GOES, profiled rather than guessed: decoder_model 4.39 s 8 calls 549 ms 59.1% base_model 2.67 s 6 calls 445 ms 36.0% model forward 95.0% python/tiling/copies 5.0% 95% is inside model forwards, so kernel-level work (ONNX, TensorRT) is a real avenue rather than a hope -- and the DECODER dominates, not the diffusion sampler, which is not where I would have looked. It also explains why decoder_tile_size moved the clock so much even while producing broken output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
25a4701cd84c12543834f041114d1207a758cd75
by plankatron <noreply@anthropic.com> · 2026-08-31T19:48:04.000Z

Changed files (2) +38 -18

__pycache__/tiles.cpython-312.pyc +0 -0
(no content changes — binary or rename)
tiles.py +38 -18
……@@ -27,9 +27,14 @@ DESIGN, and the reasons are measured:
2727 region regenerates. Elevation is cached here as int16 npy: a 256 tile is
2828 128 KB, so an entire 500 x 500 km area is about 340 MB.
2929
30 PRIORITY BY DISTANCE.
31 The queue is re-sorted on every view change, so walking somewhere new
32 abandons the old prefetch order rather than finishing it first.
30 PRIORITY BY DISTANCE, EVALUATED AT DEQUEUE TIME.
31 This was a PriorityQueue keyed on distance-to-centre AT INSERTION. Pan,
32 and stale entries kept their old high priority, so the worker carried on
33 filling tiles near where you used to be while the tiles you were actually
34 looking at waited behind them -- observed as "the tiling focuses on
35 filling black tiles vs refining those blurry tiles". A frozen priority is
36 the wrong model for a queue whose ordering depends on a moving camera.
37 The worker now picks the nearest to the CURRENT centre each iteration.
3338"""
3439import os, queue, threading, time
3540import numpy as np
……@@ -46,11 +51,10 @@ class TileStore:
4651 os.makedirs(self.dir, exist_ok=True)
4752 self._mem = {} # (tx,ty) -> np.int16 array
4853 self._state = {} # (tx,ty) -> 'cached'|'queued'|'busy'|'error'
49 self._q = queue.PriorityQueue()
50 self._queued = set()
54 self._want = {} # (tx,ty) -> True, the pending set
55 self._wake = threading.Event()
5156 self._lock = threading.Lock()
5257 self._centre = (0, 0)
53 self._seq = 0
5458 self.stats = dict(generated=0, from_disk=0, from_mem=0,
5559 gen_seconds=0.0, km2=0.0, errors=0)
5660 self._worker = threading.Thread(target=self._run, daemon=True)
……@@ -99,29 +103,38 @@ class TileStore:
99103
100104 # ---- queue -------------------------------------------------------------
101105 def request(self, tiles, centre):
102 """Ask for these tiles. Priority is distance from centre, recomputed
103 every call, so a new view supersedes the old prefetch order."""
106 """Add to the pending set and update the centre. No ordering is decided
107 here -- see the note at the top of the file."""
104108 self._centre = centre
105109 with self._lock:
106110 for tx, ty in tiles:
107111 k = (tx, ty)
108 if k in self._queued or self.state(tx, ty) in ("cached", "disk"):
112 if k in self._want or self.state(tx, ty) in ("cached", "disk"):
109113 continue
110 d = (tx - centre[0]) ** 2 + (ty - centre[1]) ** 2
111 self._seq += 1
112 self._q.put((d, self._seq, k))
113 self._queued.add(k)
114 self._want[k] = True
114115 self._state[k] = "queued"
116 self._wake.set()
115117
116118 def queue_depth(self):
117 return self._q.qsize()
119 return len(self._want)
120
121 def _take_nearest(self):
122 """Pop the pending tile closest to the CURRENT centre."""
123 with self._lock:
124 if not self._want:
125 return None
126 cx, cy = self._centre
127 k = min(self._want, key=lambda t: (t[0] - cx) ** 2 + (t[1] - cy) ** 2)
128 self._want.pop(k, None)
129 return k
118130
119131 # ---- the single worker -------------------------------------------------
120132 def _run(self):
121133 while True:
122 _, _, (tx, ty) = self._q.get()
123 with self._lock:
124 self._queued.discard((tx, ty))
134 k = self._take_nearest()
135 if k is None:
136 self._wake.wait(0.25); self._wake.clear(); continue
137 tx, ty = k
125138 if self.state(tx, ty) in ("cached", "disk"):
126139 self._state.pop((tx, ty), None)
127140 continue
……@@ -140,12 +153,19 @@ class TileStore:
140153 self.stats["km2"] += (n * TILE_M / 1000.0) ** 2
141154 self._state.pop((tx, ty), None)
142155 self.log(f" tile ({tx},{ty}) in {dt:.2f}s "
143 f"queue {self._q.qsize()}")
156 f"queue {len(self._want)}")
144157 except Exception as ex:
145158 self.stats["errors"] += 1
146159 self._state[(tx, ty)] = "error"
147160 self.log(f" tile ({tx},{ty}) FAILED: {ex}")
148161
162 def drop_pending(self):
163 """Forget everything not yet started. The in-flight tile finishes."""
164 with self._lock:
165 for k in list(self._want):
166 self._state.pop(k, None)
167 self._want.clear()
168
149169 def summary(self):
150170 s = dict(self.stats)
151171 s["queue"] = self.queue_depth()