Skip to content

Decoder phase silently produces empty mesh on M2 Ultra: Metal command buffer killed with kIOGPUCommandBufferCallbackErrorImpactingInteractivity #8

Description

@Hanning-Liu

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)

  1. 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.
  2. 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.)
  3. 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:

  1. 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).
  2. Continues execution with empty sparse tensors.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions