Go to file
John 2a0f64375c Cache the prepared device gather index (5.9x total on the hot op)
Caching only the raw indice map left two thirds of the runtime on the table. Every
layer still rebuilt the missing->N sentinel substitution over 5.6M elements in numpy
and re-uploaded a 22.6MB index to the GPU. Isolating the kernel showed ~13ms of
actual GPU work behind ~26ms of CPU bookkeeping.

The transposed, sentinel-substituted index depends only on the coordinate set - the
same invariant that justifies caching the indice map - so it is now cached on-device
whole.

m3ultra 128^3/128ch, cumulative:
  per-offset loop      81.6ms   2.57 Mvox/s
  fused gather+matmul  39.3ms   5.34
  cached device index  13.8ms  15.25   <- 5.9x total

A chunk-size sweep confirmed 256MB (11 dispatches) is at the optimum; unchunked is
marginally slower (14.2ms), so the chunking is free insurance for small-memory boxes.
2026-08-02 10:09:57 +10:00
bench Fuse the K^3 taps into one gather + matmul (2.1x on M3 Ultra) 2026-08-02 10:07:53 +10:00
lato_mlx Cache the prepared device gather index (5.9x total on the hot op) 2026-08-02 10:09:57 +10:00
tests MLX sparse core: SubMConv3d + SparseTensor + weight converter 2026-08-02 10:04:24 +10:00
.gitignore MLX sparse core: SubMConv3d + SparseTensor + weight converter 2026-08-02 10:04:24 +10:00
CLAUDE.md MLX sparse core: SubMConv3d + SparseTensor + weight converter 2026-08-02 10:04:24 +10:00
README.md MLX sparse core: SubMConv3d + SparseTensor + weight converter 2026-08-02 10:04:24 +10:00

lato.2_mrp_mlx

An MLX port of LATO.2 — factorised 3D mesh generation (vertex flow, then connectivity flow) — so it runs natively on Apple Silicon.

Why

LATO.2 generates meshes to a controllable vertex budget, which is the interesting part: it sidesteps the generate-dense-then-decimate loop that TRELLIS-style pipelines force on you. But upstream inherits TRELLIS.2's setup.sh and hard-requires CUDA:

# upstream modules/sparse/__init__.py
BACKEND = 'spconv'    # accepts only ['spconv', 'torchsparse']  — both CUDA-only
ATTN = 'flash_attn'   # accepts only ['xformers', 'flash_attn'] — both CUDA-only

No SDPA fallback, no MPS path. Neither sparse backend has a Metal build.

The scope, once measured

The CUDA surface is far smaller than setup.sh --all implies. Of the seven released checkpoints, five are entirely dense. Sparse code touches only vertex_autoencoder and vertex_structured_flow, and every SparseConv3d in the model is constructed with the defaults stride=1, padding=None — which upstream dispatches to SubMConv3d.

So the whole blocker is one operation: submanifold 3×3×3 convolution.

Upstream Here
spconv.SubMConv3d lato_mlx/sparse/conv.py — gather/scatter over a sorted-key indice map
SparseInverseConv3d never instantiated upstream; not needed
strided sparse conv never used; upsampling is SparseSubdivide (coord expansion ×8)
nn.Conv3d (voxel encoder) dense, maps to mlx.nn.Conv3d
flash_attn / xformers attn_mode="full" everywhere → plain SDPA

Status

  • SparseTensor container + subdivide
  • SubMConv3d (k=3 and k=1) — 7/7 correctness tests pass, max err 3e-7 vs an independent naive reference
  • Weight converter, all 7 checkpoints → MLX safetensors (3.3 GB)
  • Remaining sparse ops: SparseLinear, SparseGroupNorm32, activations, attention
  • Model graphs: V-VAE, V-Flow, T-VAE, T-Flow, encoders
  • End-to-end inference
  • Fleet benchmark (m1max / m2max / m4pro / m1ultra / m3ultra)

Correctness

spconv cannot be installed here — that is the reason this port exists — so there is no numerical diff against upstream. Instead tests/test_sparse.py checks the vectorised implementation against a deliberately naive one written straight from the definition (dict lookup, per-voxel loop). They share no indexing code, so an off-by-one cannot hide in both. Tests also cover batch isolation, isolated voxels, and indice-map hit rates.

One assumption remains unverified: whether spconv gathers feats[c+d] (cross-correlation — the deep-learning convention, and what this implements) or feats[c-d]. A flipped kernel is numerically silent. It gets settled end-to-end: the V-VAE is an autoencoder, so a clean reconstruction confirms the orientation. Run the converter with --flip-kernel to test the alternative without touching code.

Use

uv venv --python 3.12 .venv
VIRTUAL_ENV=.venv uv pip install mlx numpy torch trimesh

hf download 0x4c48/LATO.2 --local-dir ckpt      # 3.3 GB upstream weights
.venv/bin/python -m lato_mlx.convert --ckpt ckpt --out weights
.venv/bin/python tests/test_sparse.py

Upstream source is vendored read-only under upstream/LATO.2 for reference.

Licence

Upstream LATO.2 is MIT (Copyright the LATO.2 authors); its sparse module carries Microsoft and VAST-AI-Research copyright, also MIT. This port is MIT on the same terms.