Go to file
John 9bfe219d3a Share the 5-D layout disambiguation
spconv KRSC [O,kz,ky,kx,I] and torch Conv3d [O,I,kz,ky,kx] are both rank 5 and cannot
be separated by rank. Both LATO.2 and Pixal3D contain a mix, and guessing wrong is
silent - the reshape succeeds and the output is quietly garbage. Verified against
Pixal3D's real weights: ss_dec classified conv3d (20), shape_dec/tex_dec krsc (40 each).
2026-08-02 11:25:09 +10:00
bench Shared TRELLIS-lineage sparse core in MLX 2026-08-02 10:31:07 +10:00
tests Add masked upsample and parameterise downsample reduction (16/16 tests) 2026-08-02 10:42:20 +10:00
trellis_sparse_mlx Share the 5-D layout disambiguation 2026-08-02 11:25:09 +10:00
trellis_sparse_mlx.egg-info Split VarLenTensor / SparseTensor to match Pixal3D's hierarchy 2026-08-02 10:35:22 +10:00
.gitignore Shared TRELLIS-lineage sparse core in MLX 2026-08-02 10:31:07 +10:00
pyproject.toml Shared TRELLIS-lineage sparse core in MLX 2026-08-02 10:31:07 +10:00
README.md Shared TRELLIS-lineage sparse core in MLX 2026-08-02 10:31:07 +10:00

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 and TencentARC's Pixal3D among them. Every one of them hard-requires spconv or torchsparse:

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).