image_to_mesh() now runs the real cascade, not the single-stage shortcut:
structure 3048 voxels @32^3 (64^3 occupancy, MAX-POOLED DOWN)
LR SLAT 3048 x 32 shape_512 extractor
refine 13147 coords @64^3 four decoder stages -> coords -> quantise
HR SLAT 13147 x 32 shape_1024 extractor
mesh 3988052 verts, 7996876 faces @1024^3
TOTAL 258.4s, peak 27.9GB with every model resident silhouette IoU 0.969
Three things the cascade needed:
1. occupied_coords_at() - ss_dec always decodes 64^3 but the cascade STARTS at 32^3.
Upstream max-pools the boolean grid down by the ratio (a voxel survives if ANY of
its eight children was occupied). I had been feeding the raw 64^3 set to the HR flow.
2. decoder.upsample() - pushes the LR latent four stages in and returns COORDS, not
features. The predicted subdivisions grow the occupied set; those coords quantise
onto the HR flow's grid. Stops BEFORE stage `upsample_times`, as upstream does;
one stage further doubles the resolution and misplaces every voxel.
3. grid_resolution override on ProjConditioner - upstream backs the HR grid off in
128-unit steps while the token count exceeds max_num_tokens, so a dense object
degrades instead of exploding. refine_coords() implements that loop.
I WAS WRONG ABOUT THE HALO. The previous commit blamed the single-stage shortcut for a
0.639 silhouette IoU and predicted the cascade would fix it. The cascade measured
0.640 - no change. The real fault was in my VERIFICATION, not the pipeline: o_voxel
returns vertices in the voxel-grid frame, while ProjGrid rotates its lattice by
_BLENDER_ROT before projecting. Rotating the mesh the same way scores 0.969 on the
same geometry the earlier commit had already produced. Added mesh.to_camera_frame()
so the trap is named where it bites; the earlier mesh was correct all along.
The cascade is still the right thing - it is the shipped path, and staged loading
halves peak memory (12.8GB vs 22.6GB) when models are released between stages.
Also adds models.load_all(), so a server builds all five models plus both conditioners
ONCE. Warmup is ~71s against ~17s of compute, so an operator must never fork per job.
Holding everything resident costs 27.9GB peak - nothing on a 256GB box.
scripts/image_to_mesh.py exits non-zero if IoU < 0.85: a run that completes with a bad
reconstruction has failed even though nothing raised.
27/27 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>