Tech15 min read

Running Idol Single-Image-to-3D on an M1 Max Without CUDA: 7 Minutes per Generation

IkesanContents

In late October, Speridlabs released Idol, an image-to-3D generation model.
Feeding it a single image outputs a 3D Gaussian Splat that reconstructs occluded areas not visible in the input.
A 3D Gaussian Splat (original paper) represents 3D volume by placing hundreds of thousands of tiny ellipsoids (Gaussians) with color and opacity in 3D space, rendering them as a dense collection of points rather than polygonal surface meshes.

The installation instructions in the README state: “an NVIDIA GPU with CUDA”.
My test machine is an M1 Max 64GB MacBook Pro, which does not have CUDA.
Because PyTorch supports running on Apple Silicon GPUs via MPS (Metal Performance Shaders), it can perform tensor computation on Mac GPUs without CUDA.
I tested whether Idol would run without CUDA on Apple Silicon, and if so, whether it could turn a full-body standing illustration of my character Kana into a 3D model.

Basic Specifications: Idol

A summary of the model card and SDK README:

ItemDetails
Repositorysperidlabs/idol (Apache-2.0). SDK code at speridlabs/idol-sdk
InputSingle image. Background removal and cropping are handled inside the pipeline
OutputStandard 3DGS .ply file. Gaussian count ranges from 32,768 to 1,048,576 (default: 262,144)
ArchitectureDiT (denoising backbone) 370.6M, Octree decoder 50.8M, Gaussian decoder 237.3M, BiRefNet (background removal) 220.7M, DINOv3 ViT-H+/16 840.6M, FLUX.2 VAE 84.1M. Total: ~1.80B
MemoryWeights: 3.7–4.4 GB, peak: 7.2 GB (values under CUDA)
Official SpeedRequest time for 50 steps / 262,144 Gaussians (median across 8 images): H100 6.7s, RTX 4090 19.1s, RTX 3080 46.2s
EnvironmentPython 3.10+, PyTorch 2.5+, NVIDIA GPU with CUDA
AccessImage encoders DINOv3 and FLUX.2-dev (only VAE is used) are gated on Hugging Face and require accepting their licenses

The output consists solely of Gaussians; polygon meshes or skeleton rigs are not produced.
Rather than standard polygonal assets that can be directly animated in tools like Blender, the resulting .ply file is formatted for display in 3D Gaussian Splatting viewers.

Verification Environment

ItemDetails
MachineMacBook Pro (M1 Max, 64GB memory)
OSmacOS 27.0
GPUMPS (PyTorch backend for Apple Silicon GPU). No CUDA
Python3.12 (Homebrew), dedicated venv for Idol
Key Librariestorch 2.15.0, torchvision 0.29.2, transformers 5.22.1, diffusers 0.42.1, tensordict 0.15.4
Idol SDKGitHub commit 4702a7f (October 29, 2026)
VisualizationLoaded .ply in Chromium using three.js 0.170.0 and @mkkellogg/gaussian-splats-3d 0.4.7; exported multi-angle captures via Playwright

Before starting, I stopped local image generation servers (ComfyUI and its queue runner) to free up memory.

Preparing to Run Without CUDA

The SDK consists of roughly 1,600 lines of pure PyTorch code.
It does not use custom CUDA extensions (custom CUDA kernels or rasterization libraries).
Attention uses flash_attn when available, falling back to PyTorch’s native SDPA otherwise.

CUDA dependencies were confined to optimizations:
torch.compile, batching CFG passes (evaluating conditional and unconditional passes in a single batch), cuDNN attention, and TF32 matrix multiplications are enabled only when the device is cuda.
On non-CUDA devices, these optimizations are simply bypassed and calculations run standard paths.

While IdolPipeline accepts a device argument, the bundled CLI tool idol.sample lacks a --device option.
I wrote a brief script to invoke the pipeline directly:

from idol import IdolPipeline

pipe = IdolPipeline.from_pretrained("weights/idol", device="mps")
out = pipe("kana_full.png", mode="synthetic", seed=1, steps=50, num_gaussians=262144)
out.gaussian.save_ply("kana.ply")

Running pip install -e ./idol-sdk succeeded without modification because it does not declare CUDA-only dependencies.
I set PYTORCH_ENABLE_MPS_FALLBACK=0 to ensure execution halts on any unsupported operations rather than silently falling back to CPU.

Substituting the FLUX.2-dev VAE

Idol relies on the FLUX.2-dev VAE for image feature extraction.
My Hugging Face account had not accepted the FLUX.2-dev license agreement, resulting in a 403 Forbidden error.
However, Black Forest Labs provides a vae/ directory inside FLUX.2-klein-4B, which was accessible without restriction.

The config.json inside klein’s VAE lists "_name_or_path": "black-forest-labs/FLUX.2-dev".
Its file size is 168,120,878 bytes compared to dev’s 336,213,556 bytes—exactly half because klein is stored in bf16 rather than FP32.
Since Idol casts the dev VAE to bf16 upon loading anyway, I updated flux2_vae in Idol’s config.json to point to klein.

To verify interchangeability, I compared klein against the FP32 FLUX.2-dev VAE published by Comfy-Org (Comfy-Org/flux2-dev under split_files/vae/flux2-vae.safetensors, 336,213,556 bytes).
All 251 tensors matched in name and shape.
When casting the Comfy-Org weights to bf16, all 251 tensors matched klein bit-for-bit.
Because Idol loads the VAE in bf16, loading dev versus klein feeds identical numerical weights into the model.
While Comfy-Org’s SHA-256 did not match the official dev hash reported by the Hugging Face API, the file size matches, so this is likely a difference in header metadata.

Running the Official Demo Image

I first tested the bundled demo image fig-knight.webp (a figurine of an armored knight) under default settings: mode="synthetic", seed 42, 50 steps, 262,144 Gaussians.

The run completed without errors, encountering zero unsupported MPS operations.

MetricM1 Max (MPS)
Model Loading59.8s (initial run, including image encoder download)
Generation434.1s (sampling 50 steps took 7m 00s; 8.4s per step)
Peak Memory14.79 GB (maximum torch.mps.driver_allocated_memory())
Output.ply 17.8 MB

This was roughly 23x slower than the RTX 4090’s 19.1s.
Note that official figures represent median request times across 8 images, whereas this measures the generation phase for a single image.
Additionally, because non-CUDA backends do not batch CFG (which evaluates both conditional and unconditional passes), 50 steps require 100 sequential model evaluations.

Peak memory was roughly double the model card’s 7.2 GB figure.
BiRefNet half-precision casting also executes only under CUDA, leaving it in FP32 on MPS.

I submitted the same image with identical settings to the official Hugging Face Space (ZeroGPU).
The API reported hardware as zero-a10g.
The Space uses the original FLUX.2-dev VAE with full CUDA acceleration.
In the comparison images below, the leftmost panel shows the preprocessed input, followed by front, front-three-quarter, side, rear-three-quarter, back, and opposite side views.
All widescreen figures in this article follow this sequence.

EnvironmentResult
M1 Max (MPS)Reconstructed front and back shapes cleanly. An extra floating shield appeared detached in the air next to the knight
Hugging Face Space (CUDA)Single shield held in hand; no floating duplicate
Knight generated on M1 Max. An extra shield is floating when viewed from the side and rear-three-quarter angles Knight generated on Hugging Face Space. Single shield

The back of the helmet, plume, and back armor were filled in cleanly in both environments despite not appearing in the input image.

However, PyTorch generates different pseudorandom number streams between CUDA and MPS, producing different initial noise distributions even with seed 42.
Consequently, this comparison alone cannot isolate whether the duplicate shield stemmed from MPS numerical differences or random noise.

Testing a Standing Illustration of Kana

Next, I tested an existing full-body standing illustration of my character Kana, originally generated locally.
It is an 832x1216 image on a white background, showing her standing upright with arms down in a school uniform.
Settings matched the knight run (seed 42).

Generation completed in 427.7s, but the output did not resemble a human figure.
From every viewing angle, only a few dark splotches hovered near the top.

Kana with seed 42. Failed to form a human body, producing only splotches

Analyzing the .ply revealed that out of 262,144 Gaussians, only 28 had opacity exceeding 0.5.
By comparison, the knight run had 50,607 on MPS and 45,701 on Space above 0.5.
Nearly every Gaussian was transparent, rendering an empty void.

The preprocessed input image showed Kana cleanly cropped on a black background.
The failure occurred in the generative phase rather than background segmentation.

Idol also predicts camera distance and elevation (pitch) from the input image.
For Kana seed 42, predicted distance was 1000 (effectively an orthographic projection from extreme distance) and pitch was 0.45 radians, markedly different from the knight’s distance of 3.09 and pitch of 0.01 radians.

This pointed to two possible explanations:
Either MPS numerical behavior diverged significantly from CUDA, causing both the knight’s shield and this collapse, or a tall, flat 2D anime illustration fell outside the model’s expected input distribution.
To check this without uploading private character assets to external spaces, I compared CPU and MPS outputs component by component under identical inputs.

Checking MPS Numerical Drift

CPU Baseline Bottleneck

Running pure CPU inference directly stalled on a single core for over 30 minutes without progressing.
Profiling via macOS sample showed execution stuck in CPU bf16 matrix multiplication.

SDK inspection showed the FLUX.2 VAE is loaded in bf16 and inputs are hardcoded to images.to(torch.bfloat16).
The DiT and decoders also specify self.dtype = torch.float16, casting weights to half-precision.
Because CPU half-precision math is painfully slow, I built an all-FP32 CPU pipeline to serve as the ground-truth baseline.

Additionally, the VAE samples latent distributions using random numbers via latent_dist.sample().
To prevent device RNG divergence from altering latent inputs, I patched it to return the distribution mode (mode()) during comparisons.

Component-by-Component Comparison

Comparing MPS default (mixed FP16/bf16) and MPS full-FP32 against the CPU full-FP32 baseline, using identical preprocessed inputs and identical initial DiT noise vectors.
Relative error is calculated as the L2 norm of the difference divided by baseline norm.

ComponentMPS Default (FP16/bf16)MPS All-FP32
DINOv3 Features1.02%0.00016%
FLUX.2 VAE Features0.84%0.0029%
DiT Latent Shape t=10000.11%0.00006%
DiT Latent Shape t=5000.33%0.00038%
DiT Latent Shape t=500.19%0.00021%
DiT Camera t=1000 / 500 / 500.09% / 0.15% / 0.26%< 0.0001%

In diffusion pipelines, tt denotes the denoising timestep (t=1000t=1000 represents pure noise, decreasing toward t=0t=0 for the final reconstructed shape).
No NaNs or Infs appeared under any condition.

In full FP32, MPS matched CPU output across six decimal places.
MPS operations are mathematically sound.
The 0.1–1% drift observed in default mode falls within standard half-precision rounding bounds.

This confirms MPS arithmetic behaves consistently with CPU execution.
Because the failure was not caused by broken MPS kernels, I turned to testing alternative seeds and inputs.

Varying Random Seeds

Using the same standing illustration and default settings, I ran two additional seeds.

SeedGenerationOpacity > 0.5Predicted PitchResult
42427.7s280.45 radCollapsed (empty splotches)
1409.1s199,6000.08 radFull body formed
2419.4s214,6200.20 radBody formed, but side ponytail detached and floated
Kana seed 42. Only splotches Kana seed 1. Full body formed Kana seed 2. Side ponytail floating behind the head

Seed 1 produced a standing pose matching the front input view.
Rotating the model revealed a continuous 3D figure, synthesizing the back shirt, skirt pleats, knee socks, and shoes.

Seed 2 reconstructed the torso, uniform, and legs, but detached the side ponytail and scrunchie, leaving them suspended behind the head.
From the front, the ponytail was occluded by the skull; rotation exposed the spatial gap.
Similar to the knight’s duplicated shield, slender protruding features suffered depth misplacement.

Only seed 1 yielded a coherent 3D shape.
The collapsed seed 42 was the sole run with an outlier predicted pitch, suggesting camera misestimation may trigger structural failure.

Zooming into Seed 1

Close-up render of seed 1 around the head (leftmost panel is the input):

Close-up of seed 1 head. Ponytail, scrunchie, and ahoge rendered in 3D

The side ponytail, blue scrunchie, and ahoge remained anchored to the head in 3D.
Hair on the back of the head, unrepresented in the input, filled in as shoulder-length volume.
However, facial line art and eye highlights softened considerably compared to the 2D source, and yellow mottled artifacts appeared across the opposite side of the hair.

Testing with Chibi Art

SDK demo images (fig-*.webp) consistently depict chibi/proportioned figures with large heads.
If training data heavily favored stylized proportions, chibi inputs might perform better.
I tested a front-facing chibi illustration of Kana (832x1024, white background, red uniform skirt).

SeedGenerationOpacity > 0.5Predicted PitchResult
42414.3s163,2870.19 radFull body formed. Back of skirt turned white
1415.0s197,4860.16 radFull body formed. Skirt remained red on reverse
Chibi, seed 42. Rear half of the skirt turned white Chibi, seed 1. Skirt retained red color; ponytail remained attached

Seed 42, which collapsed on the full-body standing art, successfully formed a complete body with chibi proportions.
In both seeds, the side ponytail and scrunchie stayed properly attached to the head.

In seed 42, viewing the rear revealed that the back half of the red skirt bleached white, appearing as an elongated shirt hem.
Seed 1 preserved red skirt coloring on the reverse, though heavy dark contour strokes appeared along hair flow on the back of the head—mimicking input line-art aesthetics onto unobserved surfaces.

Testing with Figure-Rendered Artwork

Reviewing the 20 character demo images (fig-*.webp) showed they are 3D CG renders depicting soft vinyl or PVC figurines with studio lighting.
Even nb-dragon-anime.webp, titled with “anime”, depicted a sculpted figurine rather than flat cell illustration.

Official Demo ImagesKana Standing Art
StyleFigurine 3D CG with shading, specular highlights, ground contact shadowsFlat-color cell animation art with black outlines and minimal shading
View AnglePrimarily slightly elevated 3/4 viewsDirect front-facing
Proportions2–3 heads tall~7 heads tall (chibi is ~3 heads tall)

Generation quality ranked knight > chibi > standing art, mirroring similarity to demo images.
Using the standing and chibi illustrations as references, I had Codex generate figure-style re-renderings.
Using the knight image as a stylistic reference, prompts specified black backgrounds, pronounced shading and highlights, and slightly elevated three-quarter perspectives.
I generated four variants (1254x1254 each): two scale figurines with clear acrylic circular bases (Full-body A1, A2) and two chibi soft-vinyl figures (Chibi B1, B2).
Hair and outfit designs remained faithful to character references.

InputSeedGenerationOpacity > 0.5Predicted PitchResult
Full-body A142400.8s203,0560.23 radFull body formed. Base shadow persisted as dark floor artifact
Full-body A11404.3s203,6930.11 radFull body formed cleanly
Full-body A242400.6s172,7400.10 radFull body formed cleanly
Chibi B142402.0s243,9080.17 radFull body formed cleanly
Chibi B242400.7s239,4800.16 radFull body formed cleanly
Figure-style Full-body A1, seed 42. Full body formed with dark shadow residue at the feet Figure-style Full-body A1, seed 1. Full body formed Figure-style Chibi B1, seed 42. Full body formed

Seed 42, which collapsed on flat-color art, successfully generated complete bodies under figure styling.
All five runs produced intact torsos and separated legs.
Front and front-three-quarter angles mirrored the input shading and specular highlights accurately.
Volume depth measured 0.73–0.74x the frontal width, noticeably thicker than flat standing art seed 1 (0.57x).
Only A1 seed 42 retained base shadow artifacts as a dark ring and hazy floor patch.

However, the side ponytail duplicated into twin tails across all five runs.
Despite the input having a single ponytail on Kana’s left side, rotating to the rear revealed an identical ponytail and blue scrunchie mirrored on the right side.
The image below shows Full-body A1 seed 1 (rear-three-quarter and back) on the left, and Chibi B1 seed 42 (rear-three-quarter and back) on the right:

Back of the head from figure-style inputs. Ponytail and scrunchie mirrored on both sides, creating a twin-tail appearance

Flat standing and chibi inputs maintained a single ponytail when viewed from the back.
Figure-style rendering stabilized overall anatomy, but triggered bilateral hair mirroring on unobserved angles.

Idol accepts only a single image with no provision for multi-view conditioning.
IdolPipeline consumes one image, and DiT conditions generation solely on features from that view.
Passing a directory to --image simply processes images independently in sequence.
All unobserved geometry relies strictly on generative priors.

View Angles and Asymmetric Accessories

To determine whether bilateral duplication was specific to side ponytails or affected any asymmetric accessory, I generated two additional variations based on Chibi B1:
C1 and C2 depicted the same chibi figure rotated toward her left (the ponytail side) at roughly 45° and 60° three-quarter angles, exposing the ponytail protruding laterally and leaving the opposite temple bare.
D1 removed the ponytail, styling the hair down with a single red star hairpin on her left side.

InputSeedGenerationOpacity > 0.5Result
C1 (~45°)42400.8s243,362Duplicated to 2 tails in back. Mirrored tail lacked scrunchie
C1 (~45°)1401.6s240,408Duplicated to 2 tails in back, with 2 scrunchies
C2 (~60°)42402.3s231,790Single ponytail preserved. Ahoge detached and blurred
D1 (Star hairpin)42400.7s245,313Hairpin remained singular on left side
D1 (Star hairpin)1403.9s240,698Hairpin remained singular on left side
Chibi three-quarter view (~45°), seed 1. Ponytail duplicated into two on the rear view Chibi three-quarter view (~60°), seed 42. Body orientation flipped, face visible on far right column Chibi with star hairpin, seed 42. Hairpin remained only on one side

Close-up comparisons of head regions (top: C1 seed 1, middle: C2 seed 42, bottom: D1 seed 42):

Head comparison. Top C1 has two ponytails and scrunchies, middle C2 has one ponytail, bottom D1 has star hairpin on one side only

The asymmetric star hairpin never duplicated across either seed.
Back hair formed a unified surface, with only the clip edge visible along the left contour.
Bilateral mirroring specifically affected hair volume projecting outward from the head rather than surface accessories.

At ~45°, the ponytail still duplicated.
Rotating to ~60°, where ponytail depth was unambiguous, preserved a single tail.
However, C2 caused the ahoge to detach and blur into a hazy clump, likely because extreme profile perspective caused depth ambiguity across the thin antenna hair.
Near-frontal single views cannot resolve whether an asymmetric hair cluster is unilateral or bilateral, causing the prior to bias toward twin tails.

Across 13 generations featuring Kana’s original hairstyle (flat and figure styles combined), 12 produced geometry (excluding 1 collapse), but none reconstructed the back of the head cleanly.
Failures varied across detached floating ponytails, bilateral twin tails, floating ahoge, and mottled back-hair streaks.
While shaded inputs stabilized torso and limb geometry, resolving the back of the head from a single near-frontal 2D image remained unresolved.

Gaussian Counts and Denoising Steps

To evaluate latency versus quality, I adjusted Gaussian counts and step counts using standing art seed 1.

Gaussian Count

CountGenerationPeak Memory.ply Size
262,144 (Default)409.1s15.61 GB17.8 MB
1,048,576 (Max)427.3s15.61 GB71.3 MB

A 4x increase in Gaussian count extended generation time by only 18 seconds and left peak memory unchanged.
DiT sampling consumes virtually all time and memory; Gaussian count impacts only the trailing decoder stages.

Because random seed was identical, overall geometry matched.
Left to right: 262,144 front, 1,048,576 front, 262,144 front-3/4, 1,048,576 front-3/4.

Comparison by Gaussian count. Left to right: 262,144 front, 1,048,576 front, 262,144 front-3/4, 1,048,576 front-3/4

At 1,048,576 Gaussians, hair strand borders and collar seams gained sharpness.
Eye softening remained unaffected, and an isolated white point artifact hovered before the nose in both renders.

Step Count

StepsGenerationOpacity > 0.5Result
50 (Default)409.1s199,600Full body formed
25206.9s167,932Face omitted; entire figure blurred into a blob
Kana at 25 steps. Face vanished, legs fused into a thick blob

Halving steps halved generation time, but erased facial features, fused the legs into a single mass, and reduced the skirt to gray haze.
The README notes “Fewer are faster but lose some detail”, but on this input, reduction destroyed structural integrity.
Predicted pitch was 0.03 rad (accurate), indicating the collapse stemmed from incomplete noise removal rather than camera error.
On an M1 Max, runtime is dominated by the 50 DiT steps; reducing Gaussian count offers negligible speedup.