mirror of
https://github.com/Comfy-Org/comfy-kitchen.git
synced 2026-11-05 07:47:40 +00:00
[GH-ISSUE #92] eager backend advertises int8_linear on MPS but dispatches to CUDA-only torch._int_mm #29
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-kitchen#29
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @namikazi25 on GitHub (Aug 3, 2026).
Original GitHub issue: https://github.com/Comfy-Org/comfy-kitchen/issues/92
Describe the bug
int8 quantized checkpoints fail immediately on Apple Silicon. The eager backend lists
int8_linearunder native ops on MPS, butfast_int8_mmcallstorch._int_mm, which has no MPS kernel. It dies at the first linear layer of step 0.Interestingly, the nvfp4 text encoder in the same workflow loads and runs fine. It's classified as emulated. So the emulation path works on MPS. The problem looks like just the native/emulated classification being wrong for int8 on this device.
Reproduce
Files (from the MiniMax H3 template):
diffusion_models/minimax_h3_fl2va_pruned_int8_convrot.safetensorstext_encoders/qwen3vl_32b_minimax_h3_nvfp4_awq.safetensorsLoad and run any sampler. Fails at 0/20.
Startup shows:
Model load shows:
Traceback
Upstream op tracker: [pytorch/pytorch#141287]
Environment
Notes
Not a regression. [#9](https://github.com/Comfy-Org/comfy-kitchen/issues/9) scoped the int8 path to Nvidia 8.0-8.9 with
torch._int_mmas the matmul, so MPS was never considered. Just a gap.The README says out-of-domain requests fall back to torch/eager, and describes HIP degrading on RDNA2 by not advertising GEMMs it can't run. Same idea seems like it should apply here. Eager is the fallback tier, so it shouldn't advertise something it can't service on the current device.
Two ways I can see to fix it:
1. Don't advertise
int8_linearon non-CUDA devices, so dispatch falls through to the normal torch path. Probably the cleaner one given the HIP precedent.2. Fall back inside
fast_int8_mm:fp32 holds integers exactly to 2^24, so this should be bit-identical to
_int_mmfor realistic K with int8 inputs. Loses the int8 speedup but stays on GPU. (PYTORCH_ENABLE_MPS_FALLBACK=1"works" but round-trips to CPU on every linear in every block on every step, so it's not really usable.)Happy to open a PR for either. Let me know which you'd prefer.
@coderabbitai[bot] commented on GitHub (Aug 3, 2026):
🔗 Related PRs
Comfy-Org/comfy-kitchen#90 - Add optimized w4a8 with int8 codebook [open]
🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
@namikazi25 commented on GitHub (Aug 3, 2026):
Confirmed the overlap with #90. Its eager backend
(
comfy_kitchen/backends/eager/w4a8_int8.py) describes itself as a"portable fallback for any torch device (CPU/CUDA/ROCm)" and ends in
return int8_linear(...), so it reachesfast_int8_mmand hits the same_int_mmwall on MPS. Worth handling together rather than twice.That also shifts my preference toward option 2. The int4 to int8 dequant
in that file is pure torch and runs fine on MPS. Only the terminal
int8_linearfails, so a single fallback infast_int8_mmunblocks boththe existing int8 path and the new w4a8 one. Option 1 would need the
capability mask applied in two places.
#90 does validate devices and compute capability when registering
backend ops, so either direction has precedent in the codebase already.
@AstroPig7 commented on GitHub (Aug 4, 2026):
@namikazi25 Did you test the fallback in
fast_int8_mmfor speed? I modified comfy_kitchen’s quantization routine with your patch and found it to be very, very slow.@Terralien-jp commented on GitHub (Aug 5, 2026):
@AstroPig7 I hit the same thing, and I think I can explain the slowness and offer a faster shape for the fix.
Why a
fast_int8_mmfallback is slowA fallback there has to keep producing the int32 accumulator, so the only exact option on MPS is an fp32 matmul. MPS fp32 matmul is ~4x slower than bf16, and that dominates everything else. Measured on M5 Pro / 64GB, torch 2.12.0, at a shape the H3 DiT actually uses (M=10032 tokens, K=3072):
The int8→float cast itself is negligible (0.82 ms). Chunking K to keep the accumulation inside fp32's 24-bit mantissa adds ~4.5 ms. The rest is just fp32 being slow on this backend. So no amount of tuning inside
fast_int8_mmgets close to bf16 — the integer domain is the problem, not the implementation.What worked instead
Handle it one level up, in
int8_linear: dequantize the weight to the activation dtype and run a plainF.linear. The weight stays int8 in memory (only the per-call operand is widened), and the activation no longer round-trips through int8 quantization, so this is more accurate than the int8 path, not less. ComfyUI already drives these models at bfloat16 (manual cast: torch.bfloat16), so bf16 is the target precision anyway — the int32 exactness buys nothing.The convrot Hadamard rotation still has to be applied to the activation exactly as the int8 path does it, since the stored weight is pre-rotated.
Accuracy: relative difference vs the CPU
int8_linearreference is 0.0085 with convrot on and 0.0086 with it off. That is the reference's own per-row activation quantization error (1/127 ≈ 0.0079), i.e. the difference is the int8 path's error, not this path's.End-to-end, MiniMax H3
minimax_h3_fl2va_pruned_int8_convrot+qwen3vl_32b_minimax_h3_int8_convrot, same seed and settings:fast_int8_mmint8_linear3.4x on sampling. Output is visually indistinguishable at the same seed.
Since this sits in
int8_linearrather thanfast_int8_mm, it also covers the w4a8 path from #90 that @namikazi25 mentioned — that one ends inint8_lineartoo.Two notes for whoever picks this up:
--gpu-onlyis not a workaround for the related text-encoder placement issue.text_encoder_device()never returns the GPU on Apple Silicon becausevram_stateisSHARED, and forcing it with--gpu-onlyput the 26GB TE on the GPU next to the 20GB DiT: swap hit 28GB and the run got slower (217 s vs 175 s).Trying to convert Float8_e4m3fn to the MPS backend but it does not have support for that dtype), so the fp8_scaled checkpoints are not an escape hatch either.Environment: M5 Pro, 64GB unified, macOS 26.5, torch 2.12.0, comfy-kitchen 0.2.26, ComfyUI 0.30.0, eager backend only.
@namikazi25 commented on GitHub (Aug 5, 2026):
<html><head></head>Confirmed working on a second config. macOS 26.1, Apple Silicon 48GB, torch 2.10.0, comfy-kitchen 0.2.26, eager backend only.
I applied the dequant path as a runtime monkeypatch on a custom node rather than editing site-packages, so a pip upgrade doesn't quietly wipe it.
fast_int8_mmleft alone.608x352, 39 frames, 20 steps, res_multistep. Sampling ran 6m56s at 20.84 s/it, 525s total with VAE decode. Output looks right. Same DiT and nvfp4 TE from the H3 template.
Worth flagging that's roughly 2.8x slower than @Terralien-jp's 7.4 s/it at the same resolution and frame count. Probably just hardware and torch version, since they're on M5 Pro with torch 2.12 and I'm on torch 2.10. Their 640x640 number tracks about the same ratio, so it looks like a uniform gap rather than anything specific to this path. Mentioning it in case someone else on older silicon sees the same and it turns out to be fixable.
One thing that cost me time and isn't obvious from the source:
registry.get_implementationresolves viagetattron the backend module object, so if you only patchbackends.eager.quantization.int8_linearit gets silently bypassed and you sit there wondering why nothing changed. You needbackends.eager.int8_lineartoo.Two small deviations I'd suggest from the snippet as posted, both just matching what the CUDA path already does. Keep the K-mismatch and scale-shape checks. And run
_apply_input_actbefore the MPS branch, since swiglu halves K and skipping it breaks the dimension check.Also
w = weight.to(x.dtype)thenw = w * scaleallocates two [N,K] bf16 tensors.mul_in place drops the transient from 36 MiB to 18 at K=N=3072, and 144 to 72 at N=12288. Free, no behaviour change.Accuracy against the CPU
int8_linearreference at M=10032, K=3072, N=3072: 0.00966 rel Frobenius with convrot off, 0.00964 with it on, max abs 0.035. Close to @Terralien-jp's 0.0085. The two convrot cases differing by 2e-5 is what convinced me the rotation matches the existing path properly.I also went down a dead end that's probably worth recording so nobody else repeats it.
torch._weight_int8pack_mmexists and runs on MPS in torch 2.10, which looks like it should give a proper fused native path. It doesn't help. K=3072, bf16, mean of 20 after 5 warmup:It's a decode kernel. At M=1 the cast is the whole cost because there's no GEMM to hide it behind. At diffusion M you're bandwidth-bound on the [M,N] output and the [N,K] weight read anyway, so the cast just amortises away. Only argument left for it is memory, since it kills the transient entirely. But it's a private ATen op with no version guarantee and a 0.0026 rel diff from the cast path, so I don't think it's worth it unless someone actually hits OOM.
I should admit I spent a while assuming the fix belonged in
fast_int8_mmbefore it clicked that by the time you're down there you're already committed to returning an int32 accumulator, which forces fp32, which is the slow thing. Going up a level is the whole trick.Two other notes. The bug survives a ComfyUI update, aimdo 0.4.11 to 0.4.13 and frontend 1.47 to 1.48, comfy-kitchen still 0.2.26.
And the eager backend already advertises
</html>dequantize_int8_convrot_weightanddequantize_int8_simple_dtypeon MPS. So the primitive this needs is sitting right there in the same backend, working. It just never gets reached fromint8_linear.@MrLavender commented on GitHub (Aug 7, 2026):
Similar problem with AMD RDNA2 (gfx1030).
The nvfp4 text encoder step works fine but it crashes on the next step because
torch._int_mmtries to use hipBLASLt which isn't available on RDNA2.Note that I have set
TORCH_BLAS_PREFER_HIPBLASLT=0.Stack trace
EDIT: Tested both of the solutions posted above and they work (obviously I had to remove the
device.type != "cuda"check because AMD gpus are cuda in PyTorch). Theint8_linearfix is only about 7-8% faster on my RX 6800 16GB.Using the default T2V example that comes with ComfyUI;
EDIT2: Found https://github.com/patientx/ComfyUI-INT8-Fast-ROCM which has a faster implementation for RDNA2 using custom triton kernels. Still, it's only 17-18% faster than
int8_linearon the default T2V template @ 0.2 megapixels;And I guess that's as about good as I'm going get on this hardware without LoRA/cache/other speedup hacks. Tbh I'm still amazed that I can run this SOTA model at all!
@Maxim-Mazurok commented on GitHub (Aug 30, 2026):
I can reproduce this on Apple Silicon with TRELLIS.2 using
trellis_2_int8_convrot.safetensors.Environment:
Sampling fails immediately at step 0 in
comfy_kitchen.backends.eager.quantization.fast_int8_mm()becausetorch._int_mmhas no MPS implementation. This confirms the issue is still present with the newer comfy-kitchen version and the TRELLIS.2 INT8 ConvRot checkpoint.PYTORCH_ENABLE_MPS_FALLBACK=1is the suggested workaround, although I have not yet measured its performance for this workflow.