trellis-2-mrp-mlx/trellis2/modules/sparse/config.py
Pedro Augusto 2780bc459a sparse/config: probe flex_gemm on MPS, soft-fall-back to pytorch backend
The default Darwin path used to be a try/except ImportError, which only
catches build failures. With the mtlgemm round 1 fixes shipped, the more
common failure mode for end users will be running an *older* mtlgemm that
still returns CPU tensors from MPS calls — that doesn't fail import, it
fails with a cryptic LayerNorm crash inside the model on the first conv.

The new probe runs a tiny SparseConv3d on MPS and checks the output
device. If anything breaks (import, build, dispatch, return-device), fall
back to the pure-PyTorch backend rather than crashing inside the model.

Tensors in the probe are built on CPU then moved to MPS because some
PyTorch builds lack int/fp16 torch.zeros kernels on MPS — that's a
PyTorch issue, separate from anything we control here.

Plus: end-to-end smoke test (test_flex_gemm_integration.py) that
exercises both Algorithm.IMPLICIT_GEMM and Algorithm.MASKED_IMPLICIT_GEMM
through a real SparseConv3d → F.layer_norm chain on MPS. Confirms both
algorithms return MPS tensors of the right dtype and produce equivalent
output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-17 07:51:05 +03:00

90 lines
2.8 KiB
Python

from typing import *
import platform
import sys
CONV = 'flex_gemm'
DEBUG = False
ATTN = 'flash_attn'
def __detect_defaults():
"""Auto-detect best backends for current platform."""
global CONV, ATTN
if platform.system() == 'Darwin':
ATTN = 'sdpa'
if __flex_gemm_works_on_mps():
CONV = 'flex_gemm'
else:
CONV = 'pytorch'
elif not __has_cuda():
CONV = 'pytorch'
ATTN = 'sdpa'
def __flex_gemm_works_on_mps():
"""Probe flex_gemm with a tiny MPS conv. If the install pre-dates the
device-routing fix (or fails to build), it returns a CPU tensor — fall
back to the pure-PyTorch backend rather than crashing inside the model
on the first LayerNorm. Build tensors on CPU and move to MPS because some
PyTorch builds lack int/fp16 torch.zeros kernels on MPS."""
try:
import torch
if not torch.backends.mps.is_available():
return False
import flex_gemm
from flex_gemm.ops.spconv import sparse_submanifold_conv3d, Algorithm, set_algorithm
set_algorithm(Algorithm.IMPLICIT_GEMM)
coords = torch.tensor([[0, 0, 0, 0]], dtype=torch.int32).to('mps')
feats = torch.zeros((1, 4), dtype=torch.float16).to('mps')
weight = torch.zeros((4, 1, 1, 1, 4), dtype=torch.float16).to('mps')
out, _ = sparse_submanifold_conv3d(feats, coords, torch.Size([1, 4, 1, 1, 1]), weight)
return out.device.type == 'mps'
except Exception:
return False
def __has_cuda():
try:
import torch
return torch.cuda.is_available()
except Exception:
return False
def __from_env():
import os
global CONV
global DEBUG
global ATTN
__detect_defaults()
env_sparse_conv_backend = os.environ.get('SPARSE_CONV_BACKEND')
env_sparse_debug = os.environ.get('SPARSE_DEBUG')
env_sparse_attn_backend = os.environ.get('SPARSE_ATTN_BACKEND')
if env_sparse_attn_backend is None:
env_sparse_attn_backend = os.environ.get('ATTN_BACKEND')
if env_sparse_conv_backend is not None and env_sparse_conv_backend in ['none', 'spconv', 'torchsparse', 'flex_gemm', 'pytorch']:
CONV = env_sparse_conv_backend
if env_sparse_debug is not None:
DEBUG = env_sparse_debug == '1'
if env_sparse_attn_backend is not None and env_sparse_attn_backend in ['xformers', 'flash_attn', 'flash_attn_3', 'sdpa']:
ATTN = env_sparse_attn_backend
print(f"[SPARSE] Conv backend: {CONV}; Attention backend: {ATTN}")
__from_env()
def set_conv_backend(backend: Literal['none', 'spconv', 'torchsparse', 'flex_gemm', 'pytorch']):
global CONV
CONV = backend
def set_debug(debug: bool):
global DEBUG
DEBUG = debug
def set_attn_backend(backend: Literal['xformers', 'flash_attn', 'sdpa']):
global ATTN
ATTN = backend