trellis_sparse_mrp_mlx/README.md
John 70cd436eb9 Shared TRELLIS-lineage sparse core in MLX
Extracted from lato.2_mrp_mlx. The same sparse module underlies LATO.2, Pixal3D and
the rest of the TRELLIS.2 family, and all of them are blocked on Apple Silicon by the
same single op - submanifold conv - so it belongs in one tested package rather than
vendored per port.

13/13 tests: 7 for the conv against a hand-written reference (spconv is uninstallable
here so there is no upstream oracle), 6 for the remaining layers against torch.

SubMConv3d runs 13.8ms at 128^3/128ch on m3ultra, 5.9x faster than the obvious
per-offset loop.
2026-08-02 10:31:07 +10:00

79 lines
3.8 KiB
Markdown
Raw 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.

# trellis_sparse_mlx
The TRELLIS-lineage sparse module, in MLX, so the models built on it run on Apple Silicon.
## Why this is a package and not vendored code
Microsoft's TRELLIS.2 sparse stack has been inherited near-verbatim by a growing family
of 3D generation models — [LATO.2](https://github.com/LoHhhha/LATO.2) and TencentARC's
[Pixal3D](https://github.com/TencentARC/Pixal3D) among them. Every one of them hard-requires
`spconv` or `torchsparse`:
```python
BACKEND = 'spconv' # accepts only ['spconv', 'torchsparse'] — both CUDA-only
ATTN = 'flash_attn' # accepts only ['xformers', 'flash_attn'] — both CUDA-only
```
Neither sparse backend has a Metal build, and there is no SDPA fallback. That single
dependency is what keeps the entire lineage off Apple Silicon.
The blocker turns out to be **one operation**. Every `SparseConv3d` in these models is
constructed `stride=1, padding=None`, which spconv dispatches to `SubMConv3d`. Nothing
instantiates strided or inverse sparse conv. Implement submanifold convolution and the
remainder is ordinary linear / norm / attention work.
Since each new model in this family needs the same core, it lives here once — tested —
rather than being copy-pasted per port.
## What's in it
| | |
|---|---|
| `SubMConv3d` | submanifold 3×3×3 (and 1×1×1) sparse conv — the actual blocker |
| `SparseTensor` | coords `[N,4]` + feats `[N,C]`, batch-contiguous, with an indice-map cache |
| `subdivide` / `downsample` | ×2 coord expansion; ×2 **max**-pool (upstream says "average" in its docstring but passes `reduce="amax"`) |
| `SparseLinear`, `LayerNorm32`, `SparseGroupNorm32`, `SparseSiLU/GELU` | |
| `SparseResBlock`, `SparseFeedForwardNet` | |
| `SparseMultiHeadAttention` (self + cross), `SparseTransformerBlock`, `SparseTransformerCrossBlock` | `attn_mode="full"`, which is all these models instantiate |
## Performance
`SubMConv3d`, 128³ grid / 128 channels / ~10% occupancy (209,715 voxels), warm indice cache:
| box | chip | GPU cores | conv_ms |
|---|---|---|---|
| m3ultra | M3 Ultra | 80 | **13.8** |
| m2max | M2 Max | 38 | 27.3 |
| m1ultra | M1 Ultra | 64 | 39.7 |
| m1max | M1 Max | 32 | 41.4 |
| m4pro | M4 Pro | ~20 | 55.1 |
The naive implementation — a loop accumulating `gather(i) @ W[i]` over the 27 offsets —
was **5.9× slower**, and the giveaway was that throughput *anti-correlated* with GPU core
count: the 80-core Ultra lost to a 38-core M2 Max, because 2·K³ = 54 tiny dispatches per
layer never fill the machine and UltraFusion punishes small dispatches hardest.
Two fixes: fuse the taps into a single `[N, K³·Cin] × [K³·Cin, Cout]` matmul, and cache the
*prepared device* gather index (not just the raw indice map — rebuilding the sentinel
substitution and re-uploading cost ~26 ms against ~13 ms of real GPU work). Only after
both does core count predict performance, which is the sign it is finally compute-bound.
## Correctness
`spconv` cannot be installed on Apple Silicon — that is the entire reason this exists — so
there is no upstream oracle for the conv. `tests/test_sparse.py` instead checks the
vectorised implementation against a deliberately naive one written from the definition
(dict lookup, per-voxel loop) sharing no indexing code, so an off-by-one cannot hide in
both. `tests/test_ops.py` compares every other layer against **torch**, which *is*
available. 13/13 pass.
**One assumption is 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 — latent statistics were tried as a discriminator and do not separate
them. It needs an end-to-end reconstruction to settle.
## Licence
MIT. Derived from the TRELLIS/TRELLIS.2 sparse module (Copyright Microsoft Corporation and
VAST-AI-Research contributors, MIT).