mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-07-07 07:01:46 +08:00
Pins that supports_cast() lets fp8-capable devices skip the quantized text encoder fallback (these run on non-MPS CI too), that low vram states still place text encoders on the CPU, and adds bf16 placement, dict-form (full checkpoint) state dicts, and non-tensor state dict entries to the MPS tests.
154 lines
6.0 KiB
Python
154 lines
6.0 KiB
Python
"""Text encoder device placement on Apple Silicon (MPS).
|
|
|
|
MPS machines run in VRAMState.SHARED. Text encoders the device supports
|
|
(fp16/bf16/fp32) should load on the GPU, while fp8/quantized ones must
|
|
stay on the CPU because MPS cannot cast float8 dtypes.
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
import comfy.model_management as mm
|
|
import comfy.ops
|
|
import comfy.sd
|
|
|
|
mps_only = pytest.mark.skipif(
|
|
not (torch.backends.mps.is_available() and mm.is_device_mps(mm.get_torch_device())),
|
|
reason="requires an Apple Silicon MPS device",
|
|
)
|
|
|
|
FP8_DTYPES = [torch.float8_e4m3fn, torch.float8_e5m2]
|
|
|
|
# Big enough that text_encoder_initial_device() picks the load device.
|
|
LARGE_PARAM_COUNT = 2 * 1024 * 1024 * 1024
|
|
|
|
|
|
class DummyTokenizer:
|
|
def __init__(self, embedding_directory=None, tokenizer_data={}):
|
|
pass
|
|
|
|
|
|
class DummyTEModel(torch.nn.Module):
|
|
def __init__(self, device=None, dtype=None, model_options={}):
|
|
super().__init__()
|
|
operations = model_options.get("custom_operations", comfy.ops.manual_cast)
|
|
self.linear = operations.Linear(8, 8, dtype=dtype, device=device)
|
|
self.dtypes = set([dtype])
|
|
self.construct_device = torch.device(device) if device is not None else None
|
|
|
|
def load_sd(self, sd):
|
|
return ([], [])
|
|
|
|
def forward(self, x):
|
|
return self.linear(x)
|
|
|
|
|
|
def make_clip(dtype, state_dict=[], clip_class=DummyTEModel):
|
|
class Target:
|
|
params = {}
|
|
clip = clip_class
|
|
tokenizer = DummyTokenizer
|
|
|
|
return comfy.sd.CLIP(
|
|
target=Target(),
|
|
parameters=LARGE_PARAM_COUNT,
|
|
model_options={"dtype": dtype},
|
|
state_dict=state_dict,
|
|
disable_dynamic=True,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def unload_models():
|
|
yield
|
|
mm.unload_all_models()
|
|
|
|
|
|
@mps_only
|
|
class TestTextEncoderDeviceMPS:
|
|
def test_vram_state_is_shared(self):
|
|
assert mm.vram_state == mm.VRAMState.SHARED
|
|
|
|
def test_text_encoder_device_is_mps(self):
|
|
assert mm.is_device_mps(mm.text_encoder_device())
|
|
|
|
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
|
def test_castable_dtype_clip_loads_on_mps(self, dtype):
|
|
clip = make_clip(dtype)
|
|
assert mm.is_device_mps(clip.patcher.load_device)
|
|
weight = clip.cond_stage_model.linear.weight
|
|
assert weight.device.type == "mps"
|
|
x = torch.ones((1, 8), dtype=dtype, device=weight.device)
|
|
assert clip.cond_stage_model(x).device.type == "mps"
|
|
|
|
@pytest.mark.parametrize("fp8_dtype", FP8_DTYPES)
|
|
def test_fp8_clip_falls_back_to_cpu(self, fp8_dtype):
|
|
clip = make_clip(fp8_dtype)
|
|
assert clip.patcher.load_device == clip.patcher.offload_device
|
|
assert clip.cond_stage_model.construct_device.type == "cpu"
|
|
weight = clip.cond_stage_model.linear.weight
|
|
assert weight.device.type == "cpu"
|
|
x = torch.ones((1, 8), dtype=torch.float16, device=weight.device)
|
|
assert clip.cond_stage_model(x).device.type == "cpu"
|
|
|
|
@pytest.mark.parametrize("fp8_dtype", FP8_DTYPES)
|
|
def test_fp8_state_dict_falls_back_to_cpu(self, fp8_dtype):
|
|
# fp8 weights in the state dict aren't reflected in the declared
|
|
# model dtypes, e.g. quantized checkpoints with fp16 norm layers.
|
|
sd = {"linear.weight": torch.zeros((8, 8), dtype=fp8_dtype)}
|
|
clip = make_clip(torch.float16, state_dict=[sd])
|
|
assert not mm.is_device_mps(clip.patcher.load_device)
|
|
assert clip.cond_stage_model.construct_device.type == "cpu"
|
|
|
|
def test_comfy_quant_state_dict_falls_back_to_cpu(self):
|
|
sd = {
|
|
"linear.weight": torch.zeros((8, 8), dtype=torch.uint8),
|
|
"linear.comfy_quant": torch.zeros(16, dtype=torch.uint8),
|
|
"spiece_model": b"not a tensor",
|
|
}
|
|
clip = make_clip(torch.float16, state_dict=[sd])
|
|
assert not mm.is_device_mps(clip.patcher.load_device)
|
|
assert clip.cond_stage_model.construct_device.type == "cpu"
|
|
|
|
def test_fp8_full_model_state_dict_falls_back_to_cpu(self):
|
|
# Full checkpoints pass a single dict instead of a list.
|
|
sd = {"linear.weight": torch.zeros((8, 8), dtype=torch.float8_e4m3fn)}
|
|
clip = make_clip(torch.float16, state_dict=sd)
|
|
assert not mm.is_device_mps(clip.patcher.load_device)
|
|
assert clip.cond_stage_model.construct_device.type == "cpu"
|
|
|
|
@pytest.mark.parametrize("fp8_dtype", FP8_DTYPES)
|
|
def test_mixed_declared_dtypes_fall_back_to_cpu(self, fp8_dtype):
|
|
# A secondary declared dtype (e.g. dtype_llama) can be fp8 while the
|
|
# primary dtype is fp16.
|
|
class MixedDtypeTEModel(DummyTEModel):
|
|
def __init__(self, device=None, dtype=None, model_options={}):
|
|
super().__init__(device=device, dtype=dtype, model_options=model_options)
|
|
self.dtypes = set([dtype, fp8_dtype])
|
|
|
|
clip = make_clip(torch.float16, clip_class=MixedDtypeTEModel)
|
|
assert not mm.is_device_mps(clip.patcher.load_device)
|
|
assert clip.cond_stage_model.linear.weight.device.type == "cpu"
|
|
|
|
@pytest.mark.parametrize("fp8_dtype", FP8_DTYPES)
|
|
def test_fp8_cast_still_unsupported_on_mps(self, fp8_dtype):
|
|
# If a torch release adds fp8 casts on MPS, supports_cast() can be
|
|
# updated to let fp8 text encoders onto the GPU (pytorch#132624).
|
|
assert not mm.supports_cast(mm.get_torch_device(), fp8_dtype)
|
|
t = torch.zeros(4, dtype=fp8_dtype, device="mps")
|
|
with pytest.raises((RuntimeError, TypeError)):
|
|
t.to(torch.float16)
|
|
|
|
|
|
@pytest.mark.parametrize("fp8_dtype", FP8_DTYPES)
|
|
def test_fp8_capable_devices_skip_the_quant_fallback(fp8_dtype):
|
|
# The state dict scan in CLIP.__init__ is gated on this, so devices
|
|
# that can cast fp8 keep loading quantized text encoders on the GPU.
|
|
assert mm.supports_cast(torch.device("cuda"), fp8_dtype)
|
|
|
|
|
|
@pytest.mark.parametrize("state", [mm.VRAMState.LOW_VRAM, mm.VRAMState.NO_VRAM])
|
|
def test_low_vram_states_keep_text_encoders_on_cpu(monkeypatch, state):
|
|
monkeypatch.setattr(mm, "vram_state", state)
|
|
assert mm.text_encoder_device() == torch.device("cpu")
|