modelbeast/BENCHMARKS.md
2026-07-20 04:16:29 +10:00

451 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MODELBEAST Benchmarks (M3 Ultra, 256GB)
First measurements on this machine, recorded 2026-07-12. Fixtures are synthetic
(Blender-rendered Suzanne), so quality numbers are not representative of real
photography — these validate that the pipeline *runs* and how fast.
## Scan track (fully validated, no gated weights)
| Stage | Input | Settings | Result |
|---|---|---|---|
| `colmap_poses` | 48 frames @ 800×600 | sequential matcher, global (GLOMAP) mapper, OPENCV, CPU SIFT | **48/48 images registered, 1479 points, 0.60px mean reprojection error**; global mapper step ~1.0s; full job a few seconds |
| `brush_train` | above colmap_dataset | 1500 steps, max_res 800, sh 2 | ~3060s wall, 465KB splat.ply, renders in the in-app SplatViewer |
A full-quality `brush_train` run is 30000 steps (the default) — expect minutes,
and a much crisper splat than the 1500-step preview above.
## Image generation — local FLUX lineup (M3 Ultra, mflux/MLX, 1024×1024, validated 2026-07-13)
All five installed and generating. **Warm** = weights cached (the real per-image cost):
| Model | Steps | Warm gen | Peak MLX mem | Gated? | Best for |
|---|---|---|---|---|---|
| **FLUX.2 Klein 4B** | 4 | **9.1s** | 18.0 GB | no (Apache) | volume / sprites — the default |
| FLUX.1 schnell-4bit | 4 | 18.5s | 19.1 GB | no (community quant) | fast draft, FLUX.1 look |
| **FLUX.2 Klein 9B** | 4 | **18.7s** | 28.4 GB | yes | **hero assets — best object accuracy of the whole lineup, cloud included** |
| FLUX.1 schnell | 4 | 20.4s | 25.0 GB | yes | fast draft (Klein 9B beats it at same speed) |
| FLUX.1 dev | 25 | 108.1s | 25.1 GB | yes | cinematic mood / DoF when you can wait ~2 min |
First-run downloads (one-time): schnell/dev ~31GB & ~1619 min each, Klein 9B ~32GB, Klein 4B ~15GB. Believed among the first published M3 Ultra mflux FLUX.1/FLUX.2 numbers.
**Sweet spot: Klein 9B.** Same ~19s as schnell but far better object coherence → it dominates schnell. Klein 4B when speed matters (2× faster), dev only when you want the cinematic atmosphere. Note the `hf_xet` chunked downloader fails on these repos ("Unable to parse string as hex hash value") — the operator sets `HF_HUB_DISABLE_XET=1` to force the reliable HTTP path.
**First A/B (same prompt, 2026-07-12):** FLUX.2 Klein 4B local (8s, $0) vs nano-banana via OpenRouter (`google/gemini-2.5-flash-image`, 7.7s, $0.0387 exact-billed). Klein: cleaner product-photo subject. nano-banana: richer scene dressing (books/inkwell/quill, dust motes) + finer engraving detail. Verdict: Klein is the volume workhorse; nano-banana wins on scene storytelling per prompt-adherence expectations (Elo 1154 vs ~1083).
## Mesh-gen (local, validated 2026-07-13)
| Operator | Config | Result |
|---|---|---|
| `sf3d` | image → GLB, MPS, tex 1024 | **~5s**, ~9GB peak, 1.5MB GLB. Fast draft tier — good on solid objects, struggles on thin/open geometry. Needs `OMP_NUM_THREADS=1`+`KMP_DUPLICATE_LIB_OK` (segfaults otherwise). |
| `trellis_mac` | TRELLIS.2-4B, pipeline 1024, tex 2048, MPS | **289s (~4.8 min) generation** + 16s bake, 18.4MB GLB with PBR. SOTA-tier local quality — clean coherent geometry even on a thin-ringed astrolabe (dramatically better than SF3D). First run adds a one-time ~15GB download (~30 min); cached after. Needs `HF_HUB_DISABLE_XET=1` + the OMP guards. |
| `hunyuan3d_mlx` | Hunyuan3D 2.1, native MLX (fp16), shape+PBR, tex 2048, remesh 40k | **260s (~4.3 min) total** (shape 149s + texture 112s), peak 20.2GB, 7.6MB GLB (40k faces, 2048² baseColor+MR PBR). Weights **public — no HF login**. Needs `diffusers`+`fast_simplification` in the venv. **MLX-native → the one local 3D op that runs on M1 Ultra** (trellis_mac's torch-MPS bf16 is unverified there). |
| `bg_remove_local` | RMBG-2.0, MPS, 1024 | seconds; clean transparent cutout. Run before SF3D for a big geometry improvement. Note: keeps original RGB under alpha (upscale before cutout). |
### Head-to-head, same mermaid cutout (M3 Ultra, 2026-07-16)
| | speed | GLB | faces | face/detail quality |
|---|---|---|---|---|
| `trellis_mac` | 318s | 23MB | 175,842 | **sharper** — defined eyes/nose/mouth, individually raised tail scales, vivid colors |
| `hunyuan3d_mlx` | 260s | 7.6MB | 40,000 | softer — melted face, smoothed scales, muted texture |
**Verdict:** at defaults `trellis_mac` wins on quality (crisper face + geometry, richer color); `hunyuan3d_mlx` is faster, ~3× lighter, and the only local 3D op that runs on M1. `sf3d` stays the ~5s draft tier. All free/offline; fal cloud for on-demand SOTA without the local wait.
## Video gen — mlx-video / Wan 2.1 on M3 Ultra (2026-07-17, first numbers)
[Blaizzy/mlx-video](https://github.com/Blaizzy/mlx-video) (native MLX, MIT) at `vendor/mlx-video`. Wan2.1-T2V-1.3B, PyTorch weights → MLX via their converter (56s), raw `.pth` deleted after (MLX copy is what runs; 14GB).
| run | config | time | note |
|---|---|---|---|
| **T2V 3s** | 832×480, 49 frames, 50 steps, `--tiling aggressive` | **809.6s (13.5 min)** | T5 6.5s · **denoise 780.7s (15.6s/step)** · VAE decode 21.7s |
| smoke | 832×480, 5 frames, 2 steps | 11.8s | pipeline sanity |
Quality: genuinely good — photoreal fox walking through snow, coherent across all 49 frames, correct gait, no flicker/melting. Output registered in the library (`wan_first.mp4`).
**⚠️ THE GOTCHA — default `--tiling auto` gets Wan SIGKILLed past ~25 frames.** Verified bisect (2 steps, 832×480): 13f (seq 6240) OK · 25f (seq 10920) OK · **33f (seq 14040) SIGKILL**. It is **not** memory: it dies at **4GB RSS with 195GB free**, and MLX reports `max_buffer_length: 179GB` / working set 239GB on this machine. The `auto` heuristic never enables **temporal** tiling, so the VAE decode explodes. Worse, it dies *after* denoising completes — with defaults you wait 14 min for the expensive part, then lose it at the decode with no error. **Fix: `--tiling aggressive`** (→ spatial=256px, temporal=32f) — 33f then saves fine. `--tiling temporal` does *not* help.
Other mlx-video packaging bugs found (all upstream-PR-worthy): (1) `torch` is required to read the `.pth` T5/VAE but is **undeclared** — conversion dies *halfway*, leaving a plausible-looking broken model dir; (2) `librosa` is a hard dep though only LTX-2's *audio* VAE uses it — drags in ancient numba and **breaks install** on modern Python (should be an optional `[audio]` extra); (3) README documents `python -m mlx_video.wan2.convert` — that module doesn't exist (it's `mlx_video.models.wan_2.convert`).
**Wan2.2-TI2V-5B** (23GB MLX, text+image→video): 704², 49 frames, 40 steps = **337s (5.6 min)***faster than the 1.3B* at 832×480 (fewer steps + better-conditioned I2V). Note `Wan2.2-I2V-A14B` is **126GB** (~250GB with conversion) — not attempted.
### ❌ NEGATIVE RESULT: I2V-synthesized orbits do NOT reconstruct in 3D (2026-07-17)
Tested the appealing shortcut — *single image → I2V "camera orbit" → frames → colmap_poses → brush_train* — i.e. synthesize the turntable instead of filming it. Prompted TI2V-5B explicitly for camera orbit around a static subject (parallax being what COLMAP needs), fed the RMBG mermaid cutout on white.
| source | registered images | **3D points triangulated** |
|---|---|---|
| real iPad room video | 34 | **1006** ✅ |
| **synthesized I2V orbit** | 26 | **0** ❌ |
**Zero triangulated points.** COLMAP matched features and guessed poses but could not place a single consistent point in space (hence `Failed to fix Gauge … insufficient number of fixed points: 0`, and a "reconstruction" in 2.4s vs ~60s for real footage). Brush still emitted a 190MB .ply — trained faithfully on geometry-free input, i.e. garbage. **Bigger splat ≠ better.**
**Why:** the video visibly rotates (back of the head by frame 47) but the subject is **not rigid** — the tail dissolves, an orb materialises, the figure morphs into a different object. Video models optimise **temporal plausibility**, not **multi-view consistency**; 3D needs the latter. Each frame is individually gorgeous and mutually incompatible.
**Conclusion: you cannot synthesize your way out of capture.** For video→3D, film the real turntable (→ `bg_remove_local`/CorridorKey → colmap → brush/hunyuan3d_mlx). Use I2V for motion/B-roll, not as a multi-view source. Don't re-run this experiment — it's a property of the objective, not a tuning problem. (A purpose-built multi-view/NVS model — e.g. TRELLIS-class or a camera-controlled LoRA — is the thing that could work; a general I2V model can't.)
## CorridorKey (neural green-screen keyer, MLX) — Ultra tuning (2026-07-16)
Corridor Digital's keyer (`vendor/corridorkey`, 14.4k★) with the native MLX backend (`corridorkey-mlx`, resolved from git — not on PyPI). Benchmarked on a synthetic 12-frame 2048² green-screen set with **exact ground-truth alpha** (RMBG mermaid cutout + soft-alpha stripes/disk over chroma green); hint = 8× downscaled truth. Scores = alpha MAE / soft-IoU vs truth, steady-state after 2-frame warmup.
| config | M3 s/f | M1 s/f | MAE ↓ | IoU ↑ | peak GB |
|---|---|---|---|---|---|
| full 2048 (stock default) | 3.81 | 4.97 | 0.0091 | 0.925 | **28.2** |
| full 1024 | 0.61 | 0.75 | 0.0111 | 0.908 | 3.7 |
| full 512 | 0.25 | 0.31 | 0.0139 | 0.885 | 2.6 |
| tiled 512 (stock: compile forced off) | 3.64 | 4.65 | **0.0082** | **0.932** | 2.5 |
| **tiled 512 + our compile patch** | **2.48** | 4.19 | **0.0082** | **0.932** | **2.3** |
**Findings:** (1) `compile` gains nothing full-frame at 2048 on Ultras (within noise) — the documented "1.52×" is a small-res/laptop figure. (2) **Tiled-512 is the QUALITY winner**, not just the memory fallback — best alpha accuracy, model at native tile scale over full-res input. (3) Upstream hard-codes `compile=False` in tiled mode; tiles are fixed-shape so compilation applies — our 1-line patch (`vendor/corridorkey-mlx`, branch `modelbeast`, editable-installed into the app venv on M3+M1) makes tiled **1.47× faster on M3** (3.64→2.48 s/f), 1.11× on M1, output bit-identical. (4) tile 1024 tested worse (quality + speed) — 512 is the sweet spot. (5) full-frame 2048 peaks **28 GB** → 32GB Macs (M1 Max) should run **tiled** (2.3 GB) — which is also the best-quality config anyway.
**Fleet verdict:** best-quality config = `tile_size=512, overlap=64` + our patch: M3 ~0.40 fps, M1 ~0.24 fps at 2048², IoU 0.932, 2.3 GB — runs on every node including the M4 24GB. Throughput mode: full-1024 (1.65/1.34 fps, IoU 0.908). MLX gaps: blue-screen checkpoint + despill/despeckle not yet on MLX (torch backend covers those). License: CC BY-NC-SA (non-commercial).
**8-machine matrix (2026-07-17) — tiled keys 2048 on EVERY Mac, same accuracy.** `tile_size=768, overlap=64` + our compile patch, full 2048² input, **peak ~2.2GB and alpha MAE 0.00849 identical on all**: M3 Ultra 1949ms · M1 Ultra 3275ms · M1 Max(24c/32GB) 4062ms · M4 mini(10c/16GB) 7284ms · **OG M1 mini (2020, 8c/8GB) 14269ms**. Full-frame 2048 needs 26GB → unreachable on most Macs ever sold; **tiled isn't the fallback, it's the answer**. Also: hdim-56 fast-path fallback confirmed on all 8 (padding win 1.3×4.9×, tracks fused-kernel quality **not** core count — the M1 Max refuted our width theory, the M4 mini then fit the corrected one as a prediction); M5's MLX gains live almost entirely in the fused kernel; fanless M3 Air does **not** throttle (1.01× at 5 min).
**Fleet ablation follow-up (2026-07-16, full report in the fork: `monster/corridorkey-mrp-mlx` → `docs/2026-07-16-m-series-fleet-ablation-results.md`):** ran upstream's own 6-toggle benchmark matrix on M3+M1+M4. New best config on BOTH Ultras = **tiled 768/64 + compile** (M3 1949ms, M1 3275ms per 2048² raster — vs 2788/5373 full-frame) at 2.4GB. Big discovery: **`sdpa` is a 4.5× regression on M1-class GPUs at 2048 (18.6s vs 4.1s) and free on M3; bf16 is neutral on both** — gate sdpa by GPU generation, the "M1 bf16 danger" assumption is wrong for this workload. `stage_gc` harmful on Ultras (0.530.66×). M4 24GB swaps at full-frame 2048 (~2024s all configs) → tiled mandatory there; healthy ≤1024.
### Phase D — hunyuan Studio tuning (2026-07-16, M3 Ultra) → new operator defaults
Raised the config from the laptop-tuned defaults to `octree_resolution 384` + `remesh_faces 120000` + `texture_size 4096`. **4096² bake works on the Studio GPU — no Metal command-buffer watchdog** (the existing `extract_textiles` tiling handles it; `uv_feature_map` never needed patching). Result: **380s** (shape 160 + tex 221), peak 20.2GB, 21.5MB GLB, 78k verts / **120k faces**, 4096² baseColor+MR. Quality jump is real — the melted face gains defined eyes + structure, tail geometry sharpens, textures crisper; closes most of the gap to trellis (trellis still edges the face). Cost: ~46% slower + ~3× file size vs the 40k/2048 default. **These are now the `hunyuan3d_mlx` operator defaults** (all still param-overridable; drop to `remesh_faces 40000`/`texture_size 2048` for fast drafts). **4096 confirmed watchdog-free on the M1 Ultra too** (751s total — M1 runs it at ~2× M3 time, so speed-critical jobs prefer M3, which is first in the pool). Defaults are fleet-safe on both Ultras.
## Mesh-gen — earlier install notes (superseded by the table above)
| Operator | Install | Runtime status |
|---|---|---|
| `sf3d` | venv + Metal texture_baker/uv_unwrapper kernels compiled OK; torch 2.13 MPS available | Runs end-to-end; **weights gated**`stabilityai/stable-fast-3d` returns `GatedRepoError` until the owner accepts the license + sets an HF token. Then expect seconds-to-a-minute on MPS. |
| `trellis_mac` | setup.sh built .venv (py3.11) + mtl* Metal kernels; torch 2.13 MPS available | Runs end-to-end; **weights gated** — needs HF access to `facebook/dinov3-vitl16-pretrain-lvd1689m` + `briaai/RMBG-2.0`. Expect ~35 min/gen once authed (M4 Pro reference; M3 Ultra should match or beat). |
| `fal_*` (trellis / trellis2 / hunyuan3d / rodin) | none (API) | Gated on `FAL_KEY`. Verified param surfaces; ~1s1min server-side per fal docs. |
## To unblock the gated local operators
1. Accept the model licenses on HuggingFace (one-time, usually instant):
- https://huggingface.co/stabilityai/stable-fast-3d
- https://huggingface.co/facebook/dinov3-vitl16-pretrain-lvd1689m
- https://huggingface.co/briaai/RMBG-2.0
2. Either `huggingface-cli login` on the machine, or paste an HF token into
Settings → "HuggingFace token" (injected as `HF_TOKEN` for the operators).
## Fleet hardware baseline (perfcheck, torch 2.13.0/MPS, 2026-07-17)
What each node's GPU is *capable* of, independent of any model — the denominator for every
number above, and the reference for "is this machine still healthy". Measured nightly by
`scripts/perfcheck/`; see **[PERFCHECK.md](PERFCHECK.md)** for the full story.
| node | chip | RAM | matmul fp16 | bandwidth | sdpa 56/64 |
|---|---|---|---|---|---|
| m3ultra | M3 Ultra | 256GB | 25202 | 627 GB/s | 4.35x |
| m1 | M1 Ultra | 128GB | ~17100 | 639 GB/s | 3.89x |
| studio | M1 Max | 32GB | 6996 | 348 GB/s | 3.09x |
| m4 | M4 Pro | 24GB | 5678 | 241 GB/s | 1.96x |
| m4mini | M4 | 16GB | 3774 | 105 GB/s | 5.10x |
| mini | M1 (2020) | 8GB | 2310 | 61 GB/s | 5.26x |
GFLOP/s. The M3 Ultra is **~2.2x** the M1 Ultra on fp16 matmul but they share the same ~625
GB/s memory bandwidth — so bandwidth-bound stages (VAE decode, big texture bakes) run at
similar speed on both, while compute-bound ones (denoise) scale with the newer silicon. Both
Ultras reach only ~78% of their ~800 GB/s spec on a single kernel; the smaller Macs hit ~88%.
`sdpa 56/64` is the fused-attention cliff at CorridorKey's awkward `head_dim=56` — it's why
padding to 64 wins (`CORRIDORKEY.md`), reproduced independently on all 6 nodes, and it is
watched nightly so we learn if a future torch ever closes it.
## Method
Timings are wall-clock from the job runner (`started_at`→`finished_at`), single
job at a time (gpu lane = 1). Re-run `tests/smoke.sh` for the framework
regression suite (12 checks, ~30s).
**Benchmarking on this fleet is contended.** These are working machines; a neighbouring job
can make a node look 511x slower and it is invisible to load average (the M3 Ultra reads load
2.45 with its GPU pinned). Anything measured here should use min-of-runs and check
`ioreg … IOAccelerator` "Device Utilization %" *before* touching the GPU. See PERFCHECK.md.
## 2026-07-19 — fleet MLX capability matrix (mlx 0.32.0 everywhere)
Probe: 4096² matmul TFLOPS (fp16/bf16), 256MB elementwise GB/s, sdpa head_dim 56 vs 64 (B1·H8·L2048).
| host | chip | RAM | fp16 TF | bf16 TF | ~GB/s | sdpa d56/d64 |
|---|---|---|---|---|---|---|
| m3ultra | M3 Ultra | 256 | 23.48 | 23.54 | 400 | 1.51× |
| ultra | M1 Ultra | 128 | 16.31 | 13.16 | 297 | 0.94× |
| JING5 | M5 | 16 | 8.61 | 8.99 | 43* | **4.72×** |
| m4pro | M4 Pro | 24 | 5.24 | 3.86 | 225 | 1.29× |
| m4mini | M4 | 16 | 3.65 | 3.65 | 98 | 1.55× |
| AIRPRO | M3 | 16 | 1.55 | 1.64 | 87 | 0.95× |
*JING5 bandwidth measured under interactive load — retest idle before trusting.
Verdicts:
- **bf16-on-M1 question answered**: works, ~20% slower than fp16 (16.3→13.2). Ship fp16 on M1-class, bf16 free on M3 Ultra/M4/M5.
- **head_dim pad-to-64 matters MOST on newest chips** (M5 4.7×!, M4-class ~1.3-1.6×, M3 Ultra 1.5×) and ~zero on M1-class (no fused fast path there anyway — matches the corridorkey ablation).
- Fleet total ≈ 59 TFLOPS fp16; the two Ultras are 2/3 of it. TRELLIS.2-MLX lane assignment: m3ultra primary, ultra second (fp16), M4 Pro light ops.
- Probe script: reusable one-liner via `uv run --with mlx` — mlxprobe.py (this bench).
Addendum (same day) — m1max studio joins the matrix (login `studio@100.92.78.24`;
the fleet doc's `m1max@` user and "disk-critical" note are both stale — 170GB free):
| M1MAX | M1 Max | 32 | 7.09 | 6.04 | 336 | 1.22× |
Notable: 2nd-best bandwidth in the fleet (336 GB/s > M4 Pro's 225) despite mid-pack
compute — good fit for memory-bound MLX work (VAE decode, big-tensor elementwise).
Fleet total ≈ 66 TFLOPS fp16 across 7 machines.
## 2026-07-19 — trellis_mac rung 1: sparse-conv backend verified (m3ultra, anatomy image, seed 42, 1024-cascade)
| SPARSE_CONV_BACKEND | total gen+bake | wall | peak RSS |
|---|---|---|---|
| flex_gemm (Metal) | **124.8 s** | 248.8 s | 20.8 GB |
| none (pure-torch) | 214.4 s | 337.6 s | 20.8 GB |
Verdicts:
- The 124.8s baseline WAS the Metal fast path (log line "[SPARSE] Conv backend: flex_gemm") — no hidden 10x jackpot; metallib loads fine on macOS 26.5.
- Metal sparse-conv is worth **1.72x end-to-end** (saves 89.6s/gen). Keep flex_gemm the default; the conv_none fallback is a real 90s regression if the metallib ever breaks silently — worth a GODBOARD/log assertion.
- Remaining fat in the fast path: ~46s diffusion sampling (padded SDPA attention — next kernel frontier), ~50s decode/extract (incl. aten::segment_reduce falling back to CPU — patchable), 19s Metal bake.
- Decode actually produces 2.79M faces before the forced ~200k simplify — the quality-recovery headroom (rung 2) is bigger than the recon estimated.
## 2026-07-19 — Hunyuan paint-UNet mx.compile (Target 1, branch mlx-tune)
Micro gate (real arch, random fp16 weights, 6-view 64² latents, m3ultra):
eager 270.5 ms/fwd → compiled 260.5 ms/fwd = **1.04×**; parity max|diff|
3.0e-05 (PASS); first-trace 1.2 s; peak 4.9 GB. Env-gated
`HY3D_MLX_COMPILE=1`, default off (numerics untouched).
Verdict: keep (free, safe) but the paint UNet is GEMM-bound — fusion alone
is small. The bigger Hunyuan levers are Target 3 (MoE `.item()` sync-tax,
4800 syncs/gen, unlocks whole-DiT compile) and Target 4 (manual→fused
SDPA in paint DINO). E2E flag-on-vs-off measurement pending after those.
## 2026-07-19 — A0 GATE: first end-to-end MLX TRELLIS.2 (Jourloy PR#175 fork, m3ultra)
Same anatomy image, seed 42, 1024-cascade, tex 2048:
| path | wall (incl. load) | bake | peak mem | output |
|---|---|---|---|---|
| torch-MPS (trellis-mac) | 248.8 s | 19 s @191K faces | 20.8 GB | 21 MB GLB |
| **mlx-experimental (Jourloy)** | **216.6 s** | 83 s @**2.74M faces** | 75.4 GB | 131 MB PBR GLB |
**VERDICT: ADOPT — the MLX path BEAT the MPS baseline on wall-clock while
baking 14× the faces.** Key discovery: `mlx-experimental` is a HYBRID —
resolver log shows `Conv backend: flex_gemm; Attention: flex_gemm_sparse_attn`,
i.e. sparse ops still run pedronaugusto's Metal kernels; MLX runs the dense
stages. The feared naive-MLX-sparse-conv path never executes. Model load is
lazy/instant on the MLX side (vs 103 s torch load).
Quality: geometry excellent at full density; the DARK-PATCH texture defect
REPRODUCES on this second, independent pipeline — both paths share the
pedronaugusto mtldiffrast/BVH texel sampling. Culprit narrowed to those
shared Metal kernels OR the decoded tex attrs; the pure-Python KDTree bake
control (running) decides. fal renders clean with the same weights, so the
attrs are likely fine → Metal sampling is the prime suspect.
Caveat: 75 GB peak fits both Ultras but check m1 headroom under load.
## 2026-07-19 — Hunyuan mlx-tune e2e (T1+T3+T4, HY3D_MLX_COMPILE=1, m3ultra)
anatomy bench image, defaults (50 steps, octree 256): shape 146s + tex 101s
= **246s total, 17.5GB peak** vs recorded defaults baseline 260s (149+112).
**1.06× e2e** — texture stage carries the win (112→101s = T4 fused DINO
+ T1 UNet compile); shape delta noise-level. Caveat: baseline row used a
different input image; same settings though. Next lever: Target 2
whole-DiT compile (unblocked by T3).
Addendum — with T2 (whole-DiT compile) included: shape 144s + tex 99s =
**243s total**. Full mlx-tune vs 260s baseline ≈ **1.07× e2e**. Verdict:
on M3 Ultra these 2B GEMM-bound models get ~1% from mx.compile fusion;
the tune's real wins were T4 fused attention (texture stage) + T3
compile-safety. Branch mlx-tune @ 5a32240; flag HY3D_MLX_COMPILE=1.
Worth re-benching on M1 Ultra where launch overhead is proportionally larger.
## 10 · DARK-PATCH VERDICT: Metal texel-sampling convicted (2026-07-19)
Decisive experiment: dumped the decoded tex voxels (E2 fixture,
1,398,769 voxels × 6 PBR attrs) and rendered base_color as a direct
front-projection — `trellis2-bench/attrs_front.png`. **The attrs are
near-fal quality** (clean anatomy palette; dark voxels only 5.6%, all
interior). Both local pipelines (trellis-mac MPS and Jourloy MLX) produce
the same mottled dark bake from these clean attrs, and both share the
pedronaugusto mtldiffrast/mtlbvh texel→voxel sampling. ⇒ **the bake's
attr-sampling is the sole quality gap.** The 3h+ KDTree run is now merely
confirmatory (left running).
### Designed fix (next session's opening move)
The attrs live on a REGULAR voxel grid — no BVH/KDTree needed at all:
texel → (xatlas UV → triangle barycentric → world pos) →
`grid[(pos-origin)/voxel_size]` direct index (+8-neighbor occupancy
search). One vectorized gather over all texels; replaces the suspect
ray-cast sampling entirely; implementable in torch-MPS or MLX in ~a page.
Validate against `attrs_front.png` colors + the fal render. If it works,
BOTH local paths get fal-class texture quality at zero bake-speed risk.
### Parity fixture set: COMPLETE
All 8 fixtures captured at `trellis2-bench/parity_fixtures/` (cond_512/
1024, ss_coords, shape_slat, shape_dec, tex_slat, tex_voxels, mesh_final;
seed 42, 1024-cascade). Hook env needed: PYTORCH_ENABLE_MPS_FALLBACK=1
ATTN_BACKEND=sdpa SPARSE_ATTN_BACKEND=sdpa SPARSE_CONV_BACKEND=flex_gemm.
## 2026-07-19 — KDTree bake control: post-mortem (confirmatory run, superseded)
The TRELLIS2_FORCE_KDTREE control finally finished: **bake 21,063s (5.85h)**
and the exported GLB is GARBAGE geometry (mangled 395K-tri blob) — the
fork's KDTree export path has its own vertex-mapping bug on top of being
unusably slow (per-triangle Python rasterizer loop). Verdict unchanged —
the attrs-projection had already convicted the Metal texel-sampling — but
this rules out "just use the Python baker" as shipped: it needs the
fast-rasterizer rewrite + export fix (in progress as fast_bake_test.py).
## 11 · BAKE FIX PROVEN (2026-07-19 evening) — dark patches eliminated
`trellis2-bench/render_vbake5_front.png`: full anatomy figure, correct
colors, ZERO dark patches — from the same decoder attrs the Metal baker
mangles. Method: cKDTree IDW sampling of the voxel grid at mesh vertices
(k=4, distance² weights, 1.5-voxel radius, single-nearest fallback) →
vertex-colored GLB. **Sampling cost: 0.3s** for 270K verts / 1.4M voxels.
Two integration gotchas (cost hours; do not rediscover):
1. **Axis spaces.** Voxel grid + raw pipeline mesh = Z-up-long-axis in
[-0.5,0.5]³; GLB-loaded meshes = glTF Y-up. Transform before sampling:
`(x,y,z)_gltf → (x,z,y)_voxel`. The "mangled blob" renders were ONLY
this (figure viewed down its own axis) — no data corruption anywhere.
2. **glTF vertex colors are LINEAR** — do not pre-gamma (double-gamma =
washed out). Store linear, let the renderer do the transfer.
Also learned: raw `MeshWithVoxel.vertices/faces` need to_glb's weld/init
before use as a plain triangle soup; `output_3d.obj` inherits the raw
convention. And xatlas hangs (2h+) on écorché-topology meshes — the UV
path needs chart budgeting or per-island parallel unwrap.
### Production path (next session)
- Option A (fast to ship): full-density vertex-colored GLB as the raw
output; MESHGOD's Blender finish farm already decimates + can bake
verts→texture (standard Blender bake) — sidesteps xatlas entirely.
- Option B (fal-parity): fast bucket rasterizer (written, in
fast_bake_test.py) + xatlas-alternative UV → 2048² texture bake with
this sampler. Needs the écorché-safe unwrap.
- Wire as `--baker python-fast` in the Jourloy fork + trellis-mac, make it
the default over the convicted Metal texel path; port sampler to MLX
(trivial — gather + weighted sum) for the pure-MLX lane.
### §11 addendum — vertex baker PRODUCTIONIZED (2026-07-19 night)
Wired and validated in BOTH pipelines:
- `vendor/trellis-mac` @ 55fdd7b: `TRELLIS2_BAKER=vertex` env mode in
generate.py (Metal + kdtree paths untouched; sentinel-gated).
- Jourloy fork @ 746e727 (pushed to monster/trellis-2-mrp-mlx): CLI
`--baker vertex`.
E2E validation (anatomy, seed 42, 1024-cascade, uncapped density):
**130.0s total, bake 1s**, 232s wall, 20.8GB peak — full 2.79M-face
vertex-colored GLB, ZERO dark patches, best local quality yet
(`render_vprod_front.png`). vs Metal-bake baseline: same speed envelope,
quality gap closed for albedo. Not yet carried: metallic/roughness maps
(vertex colors are albedo-only) — MR needs the UV path (Option B) or a
COLOR_1 convention. MESHGOD finish-farm verts→texture bake = the
remaining Option-A step for game-budget assets.
## 2026-07-19 — Qwen-Image-Layered on MLX (m1ultra) — WORKING
New capability: image → N editable RGBA layers, fully local. Model:
Qwen/Qwen-Image-Layered (20B, BF16, Apache 2.0, 54GB in m1 HF cache).
Runtime: mflux PR#302 fork (unmerged upstream; vendored at
monster/Qwen-Image-Layered-MRP-MLX, pin tag pr302-head-a255e4f, isolated
venv ~/qwen-layered-staging/mflux-layered/.venv — production mflux venv
untouched). CLI: `mflux-generate-qwen-layered`.
Smoke (anatomy test image, 4 layers, 640, q8-on-the-fly): **1180.8s wall
first-run** (dominated by 20B load + live quantization), 27.1GB peak.
Output verified: 4 true RGBA PNGs — bg layer, atmosphere layer, clean
figure cutout (74.5% transparent, crisp to the fingers), detail layer.
The figure-cutout layer doubles as a high-end matting result — relevant
to the MESHGOD prep frontline for hero assets.
Optimization in progress: `mflux-save --quantize 8` pre-baked model →
skips load+quant cost; timed gen-only rerun to follow. Next: 1024-res,
q4/q6 quality ladder, m3ultra cross-bench.
### Qwen-Image-Layered optimization ladder (m1ultra, 640, q8, 4 layers)
| config | wall | note |
|---|---|---|
| cold: HF bf16 + on-the-fly q8, 50 steps | 1180.8 s | first smoke |
| baked q8 model, 50 steps | 1075.9 s | load cost was NOT dominant |
| **baked q8, 20 steps** | **365.6 s** | **quality held — figure cutout indistinguishable from 50-step. RECOMMENDED DEFAULT.** |
3.2× end-to-end. Peak 36.6GB. Baked model: ~/qwen-layered-staging/qwen-layered-q8 (34GB).
Remaining ladder (untested): q6/q4 quality/speed, 1024-res, fewer-steps floor (10?),
m3ultra cross-bench (~1.4× compute but needs 34GB disk — m3 disk currently tight),
MODELBEAST operator wrapper (qwen_layered_local) once defaults settle.
### Cross-bench addendum: m3ultra joins (same baked q8, 20 steps, 640, 4 layers)
| box | wall | note |
|---|---|---|
| m3ultra | **243.0 s** | 1.50× vs m1 — matches fleet-matrix TFLOPS ratio (1.44×) |
| m1ultra | 365.6 s | |
Both Ultras now serve Qwen-Image-Layered from the same 34GB baked-q8
artifact (m3: ~/qwen-layered/, m1: ~/qwen-layered-staging/) + the same
Gitea fork clone. Peak 36.7GB either box. m3 disk after install: ~82GB free.
### Qwen-Image-Layered — COMPLETE ladder + verdicts (2026-07-19/20)
| config | box | wall | verdict |
|---|---|---|---|
| q8 · 20 steps · 640 | m3ultra | **243.0 s** | **volume default** |
| q8 · 20 steps · 640 | m1ultra | 365.6 s | second lane |
| q8 · 20 steps · **1024** | m3ultra | 857.6 s | **hero tier** — superb detail, true RGBA (68% transp) |
| q6 · 20 steps · 640 | m1ultra | 366.4 s | ❌ REJECTED — decomposition semantics break (silhouette, no cutout) |
| q4 · 20 steps · 640 | m1ultra | 350.1 s | ❌ REJECTED — pure noise |
Quant findings: speed is FLAT across q4/q6/q8 (compute-bound at these
settings) — quantizing below q8 buys only disk/RAM and DESTROYS quality.
**q8 is the floor for this model.** q6/q4 bakes deleted (m1 back to 3.4T).
Operator `qwen_layered_local` shipped: vendored fork at
vendor/mflux-qwen-layered (install_qwen_layered.sh), tuned defaults
(20/640/baked-q8, resolution 1024 opt-in), m1 registered in nodes.json
(machine-local), e2e run.py contract smoke passed on m3.
Also audited flux_local (mflux 0.18.0): DEFAULT PATH HEALTHY — flux2-klein
family has mx.compile on denoise + compiled scheduler (the 9s/image path).
GAP: FLUX.1 family (dev/schnell/krea) has ZERO mx.compile — backport =
same env-gated pattern as Hunyuan T1/T2, modest expected win (1-4% M3U,
more on small boxes), upstreamable to mflux. Queued, not urgent.
## 12 · RUNG-3 KERNEL DAY — RESULTS (2026-07-20)
**Headline: 216.6s → 71.96s e2e (3.0×), peak 75.4GB → 26.5GB (2.8×),
parity held (raw mesh Δ0.03%), full gate EXIT=0.** All on the fork
(monster/trellis-2-mrp-mlx): fa972aa + 7860148 + 0db816b.
What actually moved the needle (in order of impact):
1. **Vertex baker properly wired into generate_asset** (scheduler
fallthrough fixed — `--baker vertex` was silently running the 5h
pure-python kdtree path): "other" bucket 69.4s → 8.3s, and the
75GB peak turned out to be the METAL BAKE's, not the conv's.
2. **Pure-MLX sampler loop** (mlx_samplers.py): torch CPU loop + per-
forward host bounces → one conversion per stage, batched dense CFG,
once-per-stage concat_cond. Sampling 56.8→54.4s (~4% — forwards are
compute-bound) but it is the correctness-proven, compile-ready
architecture, and removes ~72 host round-trips/gen.
3. **Validator taught the vertex-colored asset class** (COLOR_0 without
UVs = legit; normals materialized at export).
Honest negatives (documented so nobody re-chases them):
- mx.compile on the step forward: ~0 on M3 Ultra (GEMM-bound; matches
Hunyuan T1/T2 findings).
- Fused Metal sparse-conv kernel: built, parity 8e-4, but stock chunking
already bounds memory at decoder scale and beats the scalar kernel on
speed. Kept as TRELLIS2_METAL_SPCONV=1 opt-in reference (simdgroup
tiling = the future version if decoders ever matter for time).
- Remaining gap to H100 (~17s at 1024): inside the DiT forwards
(54s sampling ≈ 2.3× ideal FLOPs) — attention/MLP internals, a
future deep-dive, not orchestration.
Env knobs (all default-sane): TRELLIS2_MLX_SAMPLER=0 (revert sampler),
TRELLIS2_MLX_COMPILE=1, TRELLIS2_METAL_SPCONV=1.
M1 Ultra validation: in flight (26.5GB peak fits with 100GB headroom).
### Rung-3 addendum — M1 Ultra validated (2026-07-20)
| box | e2e wall | peak mem |
|---|---|---|
| m3ultra | 71.96 s | 26.5 GB |
| **m1ultra** | **181.5 s** | **17.2 GB** |
The M1 Ultra — which could not run TRELLIS.2 at all before this fork
(torch-MPS path was m3-only in practice) — now generates full-density
vertex-baked assets in ~3 min at 17GB. Install: clone
monster/trellis-2-mrp-mlx to ~/trellis2-mlx + scripts/setup_macos.sh
(PYTHON_BIN=uv 3.11) + gated weights rsynced from m3's HF cache
(no HF login needed on workers). Both Ultras now serve the fastest
local TRELLIS.2 in existence. TRELLIS.2 fleet: m3 primary (72s),
m1 second lane (182s), zero dollars per asset.