Summary
On Apple M2 Ultra, python generate.py path/to/image.png consistently crashes during the decode_latent() phase. The macOS Window Server GPU watchdog kills a Metal command buffer with kIOGPUCommandBufferCallbackErrorImpactingInteractivity, the decoder silently returns an empty SparseTensor, and the pipeline crashes much later in an unrelated-looking traceback (either an IndexError: max(): Expected reduction dim 0 to have non-zero size inside decode_latent, or BVH needs at least 8 triangles, got 0 in o_voxel.postprocess.to_glb).
The README's perf table is from M4 Pro 24GB. I haven't seen anyone confirm the pipeline working on M2 Ultra; this issue is to track that.
Environment
- Hardware: Apple M2 Ultra, 192 GB unified memory
- macOS: 26.4.1
- Python: 3.11 (from
setup.sh default)
- PyTorch: from
setup.sh (MPS backend)
- Setup: vanilla
bash setup.sh with the Metal toolchain installed
- Backends loaded at runtime:
[SPARSE] Conv backend: flex_gemm; Attention backend: sdpa
- Display: Apple Studio Display 27-inch 5K resolution * 2 <fill in: built-in / external N×6K / etc — the WindowServer watchdog is sensitive to this>
Steps to reproduce
cd trellis-mac
source .venv/bin/activate
python generate.py assets/shoe_input.png
Default args (--pipeline-type 512 --texture-size 1024).
Expected behavior
Pipeline produces a non-empty mesh and writes a GLB, per README results.
Actual behavior
The three sampling phases (sparse structure, shape SLat, texture SLat) all complete normally. Right at the start of decode_latent (specifically the shape/tex SLat decoder forward) the watchdog fires:
Sampling texture SLat: 100%|████████████████| 12/12 [00:06<00:00, 1.86it/s]
Error: command buffer exited with error status.
The Metal Performance Shaders operations encoded on it may not have completed.
Error:
(null)
Impacting Interactivity (0000000e:kIOGPUCommandBufferCallbackErrorImpactingInteractivity)
<AGXG14XFamilyCommandBuffer: 0x...>
label = <none>
device = <AGXG14DDevice: 0x...>
name = Apple M2 Ultra
commandQueue = <AGXG14XFamilyCommandQueue: 0x...>
...
The Metal error does not raise a Python exception — it only prints to stderr. Execution continues with corrupted/empty tensors, so the eventual Python crash is downstream and looks unrelated:
File "TRELLIS.2/trellis2/pipelines/trellis2_image_to_3d.py", line 482, in decode_latent
voxel_shape = torch.Size([*v.shape, *v.spatial_shape]),
File "TRELLIS.2/trellis2/modules/sparse/basic.py", line 474, in __cal_spatial_shape
return torch.Size((coords[:, 1:].max(0)[0] + 1).tolist())
IndexError: max(): Expected reduction dim 0 to have non-zero size.
…or, if the shape decoder corruption happens to land on an empty mesh that propagates further, the crash surfaces in the BVH builder:
File ".venv/lib/python3.11/site-packages/o_voxel/postprocess.py", line 203, in to_glb
bvh = _BVH(vertices, faces)
File ".venv/lib/python3.11/site-packages/mtlbvh/bvh.py", line 61, in __init__
assert triangles.shape[0] > 8, ...
AssertionError: BVH needs at least 8 triangles, got 0
Reproduces 100% across multiple runs.
What I tried (none of these fix it)
- Insert
torch.mps.synchronize() between blocks of SparseUnetVaeDecoder.forward, with K=4 and K=1 (sync after every block). Verified that flex_gemm does encode into PyTorch's MPSStream->commandBuffer() (see flex_gemm/kernels/metal/common/metal_context.mm dispatch_mps), so torch.mps.synchronize() should drain it. Watchdog still fires at the same point.
SPARSE_CONV_BACKEND=none to bypass flex_gemm entirely and route through pure PyTorch ops. Watchdog still fires at the same point. (Total time grows ~30 s as expected, then dies the same way.)
- Both at once (
SPARSE_CONV_BACKEND=none MPS_DECODER_SYNC_EVERY=1). Same crash.
Across all three, the failing trace is identical: two kIOGPUCommandBufferCallbackErrorImpactingInteractivity lines on stderr, then IndexError: max() on an empty tex_voxels.coords inside decode_latent.
Hypothesis on root cause
Splitting the command buffer at block boundaries does not help, which strongly suggests that a single Metal kernel dispatch within one decoder block is itself running longer than the watchdog timeout (~2–5 s on macOS). Looking at the shape decoder config:
model_channels = [1024, 512, 256, 128, 64]
num_blocks = [4, 16, 8, 4, 0]
stage 1 does 16 ConvNeXt blocks at 512 channels over a sparse grid that, after the first upsample, can have millions of active voxels. A single flex_gemm implicit-GEMM dispatch (or the equivalent conv_none matmul) at that size is plausibly multi-second on M2 Ultra's GPU and gets preempt-killed.
Why this might bite M2 Ultra harder than M4 Pro:
- M2 Ultra's GPU is shared with potentially-larger external displays, so the WindowServer's "impacting interactivity" threshold is hit sooner.
- M2 Ultra has 64GB+ unified memory and the model fits without eviction, so the kernel reaches its full compute size — on a 24GB M4 Pro, paging may indirectly chunk the work.
Secondary issue: silent failure mode
Independent of the root cause, the pipeline currently:
- Lets the Metal kill print to stderr without converting it to a Python exception (this is a PyTorch-MPS-level issue, not really fixable in this repo).
- Continues execution with empty sparse tensors.
- Crashes much later with a misleading traceback.
A small defensive check after pipeline.run() (or inside decode_latent) would massively improve debuggability — abort with a clear message like "shape SLat decoder produced empty output, likely a Metal watchdog kill, please retry / reduce GPU load / set SPARSE_CONV_BACKEND=none".
Mitigations I'd value pointers on
- Has anyone here actually run this on M2 Ultra (or M1/M3 Ultra) end-to-end?
- Is there a known per-layer chunking strategy for the
SparseConvNeXtBlock3d forward that bounds a single kernel's runtime?
- Would a CPU fallback for just the shape SLat decoder be acceptable as an opt-in (
DECODER_DEVICE=cpu) with the trade-off being decode time, since the rest of the pipeline runs fine on MPS?
Logs
Full failing run (default args, python generate.py assets/shoe_input.png):
Click to expand
(.venv) (mlx) Bill_Admin@gst169057 trellis-mac % python generate.py /Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/assets/shoe_input.png
============================================================
TRELLIS.2 on Apple Silicon
============================================================
Loading pipeline...
[SPARSE] Conv backend: flex_gemm; Attention backend: sdpa
[ATTENTION] Using backend: sdpa
Loading weights: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 415/415 [00:00<00:00, 11204.18it/s]
/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/.venv/lib/python3.11/site-packages/timm/models/layers/__init__.py:49: FutureWarning: Importing from timm.models.layers is deprecated, please import via timm.layers
warnings.warn(f"Importing from {__name__} is deprecated, please import via timm.layers", FutureWarning)
/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/.venv/lib/python3.11/site-packages/timm/models/registry.py:4: FutureWarning: Importing from timm.models.registry is deprecated, please import via timm.models
warnings.warn(f"Importing from {__name__} is deprecated, please import via timm.models", FutureWarning)
Loading weights: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 754/754 [00:00<00:00, 19932.84it/s]
Loaded in 124s
Device: MPS
Input: /Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/assets/shoe_input.png (512x512)
Generating 3D model (pipeline=512, seed=42)...
Sampling sparse structure: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [00:35<00:00, 2.94s/it]
Sampling shape SLat: 0%| | 0/12 [00:00<?, ?it/s]/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/TRELLIS.2/trellis2/modules/sparse/basic.py:283: UserWarning: The operator 'aten::segment_reduce' is not currently supported on the MPS backend and will fall back to run on the CPU. This may have performance implications. (Triggered internally at /Users/runner/work/pytorch/pytorch/pytorch/aten/src/ATen/mps/MPSFallback.mm:34.)
red = torch.segment_reduce(red, reduce=op, lengths=self.seqlen)
Sampling shape SLat: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [00:11<00:00, 1.07it/s]
Sampling texture SLat: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 12/12 [00:06<00:00, 1.89it/s]
Error: command buffer exited with error status.
The Metal Performance Shaders operations encoded on it may not have completed.
Error:
(null)
Impacting Interactivity (0000000e:kIOGPUCommandBufferCallbackErrorImpactingInteractivity)
<AGXG14XFamilyCommandBuffer: 0x9f3f58a80>
label = <none>
device = <AGXG14DDevice: 0x103600010>
name = Apple M2 Ultra
commandQueue = <AGXG14XFamilyCommandQueue: 0x103600c10>
label = <none>
device = <AGXG14DDevice: 0x103600010>
name = Apple M2 Ultra
retainedReferences = 1
Error: command buffer exited with error status.
The Metal Performance Shaders operations encoded on it may not have completed.
Error:
(null)
Impacting Interactivity (0000000e:kIOGPUCommandBufferCallbackErrorImpactingInteractivity)
<AGXG14XFamilyCommandBuffer: 0x9f3f58a80>
label = <none>
device = <AGXG14DDevice: 0x103600010>
name = Apple M2 Ultra
commandQueue = <AGXG14XFamilyCommandQueue: 0x103600c10>
label = <none>
device = <AGXG14DDevice: 0x103600010>
name = Apple M2 Ultra
retainedReferences = 1
Traceback (most recent call last):
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/generate.py", line 259, in <module>
main()
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/generate.py", line 95, in main
outputs = pipeline.run(
^^^^^^^^^^^^^
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/.venv/lib/python3.11/site-packages/torch/utils/_contextlib.py", line 124, in decorate_context
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/TRELLIS.2/trellis2/pipelines/trellis2_image_to_3d.py", line 592, in run
out_mesh = self.decode_latent(shape_slat, tex_slat, res)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/.venv/lib/python3.11/site-packages/torch/utils/_contextlib.py", line 124, in decorate_context
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/TRELLIS.2/trellis2/pipelines/trellis2_image_to_3d.py", line 482, in decode_latent
voxel_shape = torch.Size([*v.shape, *v.spatial_shape]),
^^^^^^^^^^^^^^^
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/TRELLIS.2/trellis2/modules/sparse/basic.py", line 494, in spatial_shape
spatial_shape = self.__cal_spatial_shape(self.coords)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/Bill_Admin/Research/nnndl/try-mlx-vlm/trellis-mac/TRELLIS.2/trellis2/modules/sparse/basic.py", line 474, in __cal_spatial_shape
return torch.Size((coords[:, 1:].max(0)[0] + 1).tolist())
^^^^^^^^^^^^^^^^^^^^
IndexError: max(): Expected reduction dim 0 to have non-zero size.
Summary
On Apple M2 Ultra,
python generate.py path/to/image.pngconsistently crashes during thedecode_latent()phase. The macOS Window Server GPU watchdog kills a Metal command buffer withkIOGPUCommandBufferCallbackErrorImpactingInteractivity, the decoder silently returns an emptySparseTensor, and the pipeline crashes much later in an unrelated-looking traceback (either anIndexError: max(): Expected reduction dim 0 to have non-zero sizeinsidedecode_latent, orBVH needs at least 8 triangles, got 0ino_voxel.postprocess.to_glb).The README's perf table is from M4 Pro 24GB. I haven't seen anyone confirm the pipeline working on M2 Ultra; this issue is to track that.
Environment
setup.shdefault)setup.sh(MPS backend)bash setup.shwith the Metal toolchain installed[SPARSE] Conv backend: flex_gemm; Attention backend: sdpaSteps to reproduce
Default args (--pipeline-type 512 --texture-size 1024).
Expected behavior
Pipeline produces a non-empty mesh and writes a GLB, per README results.
Actual behavior
The three sampling phases (sparse structure, shape SLat, texture SLat) all complete normally. Right at the start of
decode_latent(specifically the shape/tex SLat decoder forward) the watchdog fires:The Metal error does not raise a Python exception — it only prints to stderr. Execution continues with corrupted/empty tensors, so the eventual Python crash is downstream and looks unrelated:
…or, if the shape decoder corruption happens to land on an empty mesh that propagates further, the crash surfaces in the BVH builder:
Reproduces 100% across multiple runs.
What I tried (none of these fix it)
torch.mps.synchronize()between blocks ofSparseUnetVaeDecoder.forward, with K=4 and K=1 (sync after every block). Verified thatflex_gemmdoes encode into PyTorch'sMPSStream->commandBuffer()(seeflex_gemm/kernels/metal/common/metal_context.mmdispatch_mps), sotorch.mps.synchronize()should drain it. Watchdog still fires at the same point.SPARSE_CONV_BACKEND=noneto bypassflex_gemmentirely and route through pure PyTorch ops. Watchdog still fires at the same point. (Total time grows ~30 s as expected, then dies the same way.)SPARSE_CONV_BACKEND=none MPS_DECODER_SYNC_EVERY=1). Same crash.Across all three, the failing trace is identical: two
kIOGPUCommandBufferCallbackErrorImpactingInteractivitylines on stderr, thenIndexError: max()on an emptytex_voxels.coordsinsidedecode_latent.Hypothesis on root cause
Splitting the command buffer at block boundaries does not help, which strongly suggests that a single Metal kernel dispatch within one decoder block is itself running longer than the watchdog timeout (~2–5 s on macOS). Looking at the shape decoder config:
stage 1does 16 ConvNeXt blocks at 512 channels over a sparse grid that, after the first upsample, can have millions of active voxels. A singleflex_gemmimplicit-GEMM dispatch (or the equivalentconv_nonematmul) at that size is plausibly multi-second on M2 Ultra's GPU and gets preempt-killed.Why this might bite M2 Ultra harder than M4 Pro:
Secondary issue: silent failure mode
Independent of the root cause, the pipeline currently:
A small defensive check after
pipeline.run()(or insidedecode_latent) would massively improve debuggability — abort with a clear message like "shape SLat decoder produced empty output, likely a Metal watchdog kill, please retry / reduce GPU load / set SPARSE_CONV_BACKEND=none".Mitigations I'd value pointers on
SparseConvNeXtBlock3dforward that bounds a single kernel's runtime?DECODER_DEVICE=cpu) with the trade-off being decode time, since the rest of the pipeline runs fine on MPS?Logs
Full failing run (default args,
python generate.py assets/shoe_input.png):Click to expand