mirror of
https://github.com/Comfy-Org/comfy-kitchen.git
synced 2026-11-04 23:37:42 +00:00
[PR #249] Add an mps backend: weight-only INT8 and W4A8/W6A8 linears for Apple GPUs #254
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#254
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?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-kitchen/pull/249
Author: @roblennon
Created: 10/13/2026
Status: 🔄 Open
Base:
main← Head:mps-backend📝 Commits (9)
1992f38feat(mps): weight-only int8_linear backend for Apple GPUs24913d4feat(mps): weight-only W4A8/W6A8 linear; power-of-two row scaling for fp161940aa8feat(mps): dequantize and requantize W4A8/W6A8 on Apple GPUs3d26778feat(mps): range-safe fused prologue and epilogue for the weight-only GEMM34f5427feat(mps): unmerged LoRA inside the INT8 and W4A8/W6A8 linearsfd1965cfix(mps): release chunk temporaries before the next decodefb9d707fix(mps): return empty contractions before reshaping; declare INT8 out_dtype650dd6dfix(mps): keep tiny and cancelling terms through the LoRA fast path43fe176fix(mps): coerce lora_scale to float before dispatch; list mps in the README matrix📊 Changes
13 files changed (+3402 additions, -41 deletions)
View changed files
📝
README.md(+70 -39)📝
comfy_kitchen/__init__.py(+22 -0)➕
comfy_kitchen/backends/mps/__init__.py(+106 -0)➕
comfy_kitchen/backends/mps/linear.py(+1090 -0)➕
comfy_kitchen/backends/mps/w4a8.py(+468 -0)📝
comfy_kitchen/constraints.py(+38 -0)📝
comfy_kitchen/registry.py(+4 -1)📝
comfy_kitchen/tensor/w4a8_int8.py(+10 -1)📝
tests/test_constraints.py(+42 -0)➕
tests/test_mps_empty_linear.py(+38 -0)➕
tests/test_mps_int8_linear.py(+774 -0)➕
tests/test_mps_linear_lifetime.py(+83 -0)➕
tests/test_mps_w4a8.py(+657 -0)📄 Description
What
A new
mpsbackend for Apple GPUs that implements:int8_linear(INT8, optionally ConvRot).w4a8_int8_linear(theAsymW4A8Int8Layoutformats: 4-bit with or without a Lloyd-Max codebook, 6-bit, symmetric or asymmetric).dequantize_w4a8_int8_weightandquantize_w4a8_int8_weight, so hosts can merge LoRAs into those weights.int8_linearandw4a8_int8_linearalso accept an unmerged LoRA (lora_down[R, K],lora_up[N, R],lora_scale). These are opt-in parameters: the registry skips a backend that does not declare them, so existing backends and callers are unchanged. The backend registers only when MPS is available andtorch.mps.compile_shaderexists.Why
These formats have no fast path on a Mac today. MPS does not implement
torch._int_mm, so ComfyUI disables the INT8 formats on Apple GPUs and dequantizes the weights in full on every call, and W4A8/W6A8 cannot run there at all (#92). On a 32 GB Mac that means MiniMax H3 INT8 swaps at 38 to 74 s per step, the 6-bit H3 file does not load, and Krea 2 and Qwen-Image 2.1 INT8 pay for a full bf16 dequantization per layer per call.Design
Measurements
Mac Studio M1 Max 32 GB, macOS 15.7.9, PyTorch 2.14.1, October 9, 2026. bf16 in and out, median ms per call, relative L2 error against an fp32 linear over the dequantized weight in brackets.
F.linearF.linearThe backend runs 1.9 to 2.3x faster than dequantizing per call, at fp16-dense speed and accuracy. On the wide-input shape it is also about 2x faster than a dense fp16
F.linear, because denseF.linearhits a slow MPS path for transposed weights at large K (reported separately to PyTorch), which the transposed decode avoids. Eager's W4A8/W6A8 dequantize cannot run on MPS with e4m3 scales (no fp8 arithmetic), so for those files there is no path today.Unmerged LoRA, rank 64, fused overhead per call: +2.0 ms on 4608x6144x6144 (base 38.8 ms) and +3.2 ms on 4608x16384x6144 (base 105.5 ms).
End to end in ComfyUI, seconds per step, measured with an integrated runtime that routes these formats to the backend (that ComfyUI change is a separate PR):
Relationship to #92, #107 and #145
This fixes #92 on Apple GPUs. It is complementary to the two open fallback PRs and touches none of their files:
compile_shader) or declines a call, and it can land independently.How tested
tests/test_mps_int8_linear.pyandtests/test_mps_w4a8.pycompare against eager on the CPU: every format and layout; fp16, bf16 and fp32 inputs; bias and residual order per format; near-max, subnormal and zero rows; exact residual cancellation; K > 65504; strided inputs and storage offsets; empty and 1-D inputs; shader index arithmetic past 2^32; LoRA at rank tails and column tails, including cancelling and extreme-exponent LoRAs.tests/test_constraints.pycovers the opt-in parameter check.tests/test_mps_empty_linear.pycovers empty contractions and the output-dtype declaration;tests/test_mps_linear_lifetime.pycovers chunk temporaries at M = 1 and M = 3.Two deliberate differences from eager are documented and pinned by tests: asymmetric W4A8 computes from the exact stored weight (eager rounds the weight to the input dtype first, so results can differ by that rounding), and dequantized values below the fp32 normal range flush to zero, as eager's own arithmetic does on Apple GPUs. A scale restore that overflows before a cancelling bias near 2e38 returns inf exactly as eager does and is pinned by a parity test.
Limitations
quantize_w4a8_int8_weightruns eager on the CPU (MPS has no fp8 arithmetic). Merging a LoRA into 4/6-bit weights is therefore slow (about 6.5 minutes for MiniMax H3) and lossy. The unmerged LoRA arguments exist so hosts can avoid that merge.Hardware tested
Mac Studio M1 Max (32-core GPU), 32 GB, macOS 15.7, PyTorch 2.14.1.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.