87 lines
6.3 KiB
Markdown
87 lines
6.3 KiB
Markdown
# Verification
|
||
|
||
Every ported stage is validated numerically against the PyTorch reference, per
|
||
component, not just end-to-end — the depth-anything.cpp approach. Reference
|
||
activations are dumped from `microsoft/TRELLIS.2` (via `scripts/refgen.sh`
|
||
inside the CUDA container) into GGUF files under `dumps/`, and the C++ side is
|
||
compared tap-by-tap with `tests/parity.hpp` (gate: `|got-ref| <= atol +
|
||
rtol*|ref|`, reported as max-abs / rel-L2 / per-row).
|
||
|
||
## One-time setup
|
||
|
||
```sh
|
||
docker build -f docker/Dockerfile.ref -t trellis2-ref docker # PyTorch reference env
|
||
scripts/download_models.sh # HF checkpoints -> models/
|
||
docker run --rm -v "$PWD":/work -w /work trellis2-ref bash -c '
|
||
python convert_dino_to_gguf.py --output ggufs/dino_f32.gguf --ftype 0
|
||
python convert_ss_flow_to_gguf.py --model models/TRELLIS.2-4B/ckpts/ss_flow_img_dit_1_3B_64_bf16.safetensors --output ggufs/ss_flow_f32.gguf --ftype 0
|
||
python convert_ss_dec_to_gguf.py --model models/TRELLIS-image-large/ckpts/ss_dec_conv3d_16l8_fp16.safetensors --output ggufs/ss_dec_f32.gguf --ftype 0
|
||
python convert_slat_flow_to_gguf.py --model models/TRELLIS.2-4B/ckpts/slat_flow_img2shape_dit_1_3B_512_bf16.safetensors --pipeline-json models/TRELLIS.2-4B/pipeline.json --output ggufs/slat_flow_f32.gguf --ftype 0
|
||
python convert_shape_dec_to_gguf.py --output ggufs/shape_dec_f32.gguf --ftype 0'
|
||
scripts/refgen.sh # dump reference activations
|
||
```
|
||
|
||
## Run
|
||
|
||
```sh
|
||
cmake -B build -DTRELLIS2_BUILD_TESTS=ON && cmake --build build -j
|
||
ctest --test-dir build -LE model # fast, no assets (marching cubes, preprocess)
|
||
ctest --test-dir build # full parity (needs ggufs/ + dumps/)
|
||
```
|
||
|
||
## Parity table (f32 GGUF vs true-fp32 reference)
|
||
|
||
| stage | test | tap coverage | result |
|
||
|---|---|---|---|
|
||
| image preprocess (alpha crop, premultiply, Lanczos-512) | `test_preprocess` | full 512×512 RGB | **byte-exact** (0/786432 differ) |
|
||
| DINOv3 ViT-L/16 encoder | `test_dino` | 40 taps: embeddings, RoPE, per-layer output + first/last-layer detail (norm/attn/layerscale/mlp), final affine-free LN | **PASS**, rel-L2 ≤ 7e-7 all taps |
|
||
| SS-flow DiT forward | `test_ss_flow_forward` | full output | **PASS**, rel-L2 2.4e-4 |
|
||
| SS-flow Euler sampler (12-step CFG) | `test_ss_sample` | z_s latent | **PASS**, rel-L2 5.7e-3, sign 99.85% (CPU) |
|
||
| SS decoder (dense 3D-conv → 64³ occupancy) | `test_ss_dec` | occupancy logits | **PASS**, rel-L2 2e-5 |
|
||
| shape-SLAT flow forward | `test_slat` | full output | **PASS**, rel-L2 2.9e-4 (CPU) / 8e-4 (GPU) |
|
||
| shape-SLAT VAE decoder (sparse ConvNeXt U-Net, 4 levels, 16× up) | `test_slat` | per-level features + subdivision logits + final 7-ch output, all 5 levels | **PASS**, rel-L2 ≤ 6e-7 (levels 0–3 exact; final set within 0.0001%) |
|
||
| integrated subdivision guide | `test_slat` | all decoder levels; final guide coordinates equal decoded shape coordinates | **PASS** |
|
||
| standalone shape encoder → texture flow → texture decoder | `test_texture` | shape latent, flow forward/sampler, guided 6-channel PBR decode | parity-gated; sampler backend drift uses the documented loose gate |
|
||
| sparse PBR surface sampling | `test_pbr_sampling` | dense trilinear interpolation + sparse-boundary normalization | **PASS** |
|
||
| GLB PBR/alpha export | `test_mesh_export` | direct vertex RGBA, retained metallic/roughness, glTF alpha mode | **PASS** |
|
||
| dual-grid mesh extraction | `test_marching_cubes` (invariants) + visual | watertight-manifold, Euler characteristic, winding | **PASS** |
|
||
| **1024 cascade** — decoder upsample(×4) → 512³ coords | `test_cascade` | full coord set + quantized 64³ HR scaffold | **PASS**, set match to 0.0001% (1 voxel of 995k) |
|
||
| **1024 cascade** — HR (1024-model) flow forward | `test_cascade` | full output | **PASS**, rel-L2 ~3e-4 (CPU); ~1e-2 on GPU flash |
|
||
| **1024 cascade** — final 1024³ decode (3.97M voxels) | `test_cascade` | per-level features + subdivision + 7-ch output | **PASS**, rel-L2 ≤ 2e-2, set within 0.0001% |
|
||
|
||
Notes:
|
||
- **Flash attention (default for every flow forward).** `sdpa_auto()` uses
|
||
`ggml_flash_attn_ext` for both flow DiTs at all token counts;
|
||
`TRELLIS2_SDPA_EXACT` restores the old materialized `[L_k, L_q, heads]`
|
||
softmax. Flash is bit-faithful to full softmax on CPU (SS-flow 2.4e-4, SLAT
|
||
2.9e-4 — identical to exact, so the tap parity above is unaffected) but incurs
|
||
~3e-3 rel-L2 on the CUDA F16-MMA kernel. It was already required for the HR
|
||
cascade (49,152-token attention fits on 16 GB vs a 108 GiB exact matrix); it
|
||
is now the default because on the GPU it is also ~30 % faster per forward and
|
||
O(L) memory — the exact path's 805 MB SS-flow score matrix `alloc_graph`-fails
|
||
when the resident pipeline leaves little free VRAM. Was previously gated to
|
||
score matrices >1 GiB (i.e. only the HR stage). See docs/PLAN.md for the
|
||
per-stage runtime profile.
|
||
- **TF32 matters.** PyTorch's default CUDA matmul/attention uses TF32 (≈10-bit
|
||
mantissa) and reduced-precision flash SDPA, which shows up as ~1e-3 relative
|
||
error versus true fp32. `scripts/ref_common.py` disables it so the golden
|
||
dumps are real fp32; otherwise a correct port looks like it has a 0.08-rel-L2
|
||
bug (this exact trap cost a debugging session — see the flow-forward gate).
|
||
- **Sampler drift.** The 12-step Euler + CFG-rescale loop chaotically amplifies
|
||
per-step fp differences between backends; it validates tightly on CPU and
|
||
drifts to ~0.1 rel-L2 on GPU. The decoder gate therefore decodes the
|
||
*reference* SLAT so decoder parity is independent of sampler trajectory.
|
||
- **Subdivision boundary.** A handful of level-3 subdivision logits sit within
|
||
fp-noise of zero; the `>0` threshold can flip them, so the final active-voxel
|
||
set differs by ~4 voxels out of ~4 million (0.0001%) run-to-run and
|
||
hardware-to-hardware. This is inherent to a hard threshold, not a port bug.
|
||
|
||
## GPU
|
||
|
||
`-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=120` (Blackwell / RTX 50-series). The
|
||
flow DiTs and DINOv3 run on CUDA; the 3D-conv decoders (SS decoder CONV_3D and
|
||
the sparse-conv gather-GEMM) run on CPU because the bundled ggml has no CUDA
|
||
CONV_3D kernel — they are a small fraction of total inference time. GPU f32
|
||
matmul is fp16-class, so tap parity on CUDA is ~1e-3 (deterministic, not device
|
||
noise); CPU is the tight-tolerance reference backend.
|