Model management (v2.4.0): - single-active-per-family registry releases the previous model fully (RAM, VRAM, ComfyUI current_loaded_models) before a new one loads - file-identity cache keys (basename+mtime+size+attention+q4+dtype) prevent cross-file collisions; ASR request keys share the consumer namespace so identical re-runs never spuriously evict Quant-resident runtime (v2.5.0): - GGUF weights stay raw-block resident end-to-end: uint8 parameters, per-matmul dequant kernels for Q8_0/Q4_K/Q5_K/Q6_K pinned bitwise to the gguf-py oracle; F32/F16/BF16 pass through at native dtype via zero-copy views; load-time RAM spike (~2x float size) eliminated - quantized safetensors via *.comfy_quant metadata: rotated ConvRot INT8 residents through comfy-kitchen, plain rowwise int8 / fp8 e4m3+e5m2 / int8_blockwise dequant-at-load (per-row, scalar, and per-gs-block scale layouts); unsupported formats hard-fail with actionable errors; dense gate rejects unplanned quant storages; rotated non-Linear targets (embeddings) fail with re-export guidance - dtype casts filter quant-resident storage (_quant_resident markers, fp32 weight_scale protection); SageAttention wrapper resolves activation dtype per module kind, fixing uint8-weight crash Tests: 937 passed / 5 pre-existing failures / 4 skipped
110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""Phase 0 groundwork tests: synthetic GGUF fixtures, type matrix, backend probe."""
|
|
|
|
import pytest
|
|
import torch
|
|
import gguf
|
|
from gguf.constants import GGMLQuantizationType as T
|
|
|
|
from ComfyUI_VibeVoice.modules import gguf_quant as G
|
|
from ComfyUI_VibeVoice.modules.convrot_quant import (
|
|
CONVROT_FORMAT,
|
|
assert_convrot_backend,
|
|
)
|
|
|
|
|
|
class TestSyntheticFixtureRoundtrip:
|
|
def _spec(self):
|
|
return [
|
|
("model.language_model.layers.0.self_attn.q_proj.weight", "Q8_0", (16, 32)),
|
|
("model.language_model.layers.0.mlp.down_proj.weight", "Q4_K", (32, 256)),
|
|
("model.language_model.layers.0.mlp.up_proj.weight", "Q5_K", (32, 256)),
|
|
("model.language_model.layers.1.self_attn.q_proj.weight", "Q6_K", (32, 256)),
|
|
("model.language_model.embed_tokens.weight", "BF16", (64, 32)),
|
|
("model.language_model.norm.weight", "F32", (32,)),
|
|
("lm_head.weight", "F16", (16, 8)),
|
|
]
|
|
|
|
def test_writer_reader_roundtrip(self, make_gguf_file):
|
|
path = make_gguf_file(self._spec(), tag="roundtrip")
|
|
reader = gguf.GGUFReader(str(path))
|
|
by_name = {t.name: t for t in reader.tensors}
|
|
assert len(reader.tensors) == 7
|
|
|
|
checks = {
|
|
"model.language_model.layers.0.self_attn.q_proj.weight": (T.Q8_0, (32, 16)),
|
|
"model.language_model.layers.0.mlp.down_proj.weight": (T.Q4_K, (256, 32)),
|
|
"model.language_model.layers.0.mlp.up_proj.weight": (T.Q5_K, (256, 32)),
|
|
"model.language_model.layers.1.self_attn.q_proj.weight": (T.Q6_K, (256, 32)),
|
|
"model.language_model.embed_tokens.weight": (T.BF16, (32, 64)),
|
|
"model.language_model.norm.weight": (T.F32, (32,)),
|
|
"lm_head.weight": (T.F16, (8, 16)),
|
|
}
|
|
for name, (tt, shape) in checks.items():
|
|
t = by_name[name]
|
|
assert t.tensor_type == tt, name
|
|
assert tuple(int(s) for s in t.shape) == shape, name
|
|
|
|
def test_byte_counts_match_expected_numel(self, make_gguf_file):
|
|
"""A3 cross-check: expected_numel matches actual reader byte counts."""
|
|
spec = self._spec()
|
|
path = make_gguf_file(spec, tag="numel")
|
|
reader = gguf.GGUFReader(str(path))
|
|
by_name = {t.name: t for t in reader.tensors}
|
|
for name, kind, shape in spec:
|
|
n_elem = 1
|
|
for s in shape:
|
|
n_elem *= s
|
|
tt = getattr(T, kind)
|
|
assert by_name[name].n_bytes == G.expected_numel(n_elem, tt), name
|
|
|
|
def test_builder_rejects_undivisible_shape(self, make_gguf_file):
|
|
with pytest.raises(ValueError, match="multiple of"):
|
|
make_gguf_file(
|
|
[("x.weight", "Q4_K", (8, 100))], tag="bad"
|
|
)
|
|
|
|
|
|
class TestTypeMatrix:
|
|
def test_supported_matrix_exported(self):
|
|
names = {t.name for t in G.SUPPORTED_GGML_TYPES}
|
|
assert {"Q8_0", "Q4_K", "Q5_K", "Q6_K"} <= names
|
|
|
|
def test_float_matrix_includes_bf16(self):
|
|
"""The real VibeVoice GGUF is dense-BF16 heavy; BF16 must be supported."""
|
|
names = {t.name for t in G.FLOAT_GGML_TYPES}
|
|
assert {"F32", "F16", "BF16"} <= names
|
|
|
|
def test_convrot_format_constant(self):
|
|
assert CONVROT_FORMAT == "int8_tensorwise"
|
|
|
|
|
|
class TestConvRotBackendProbe:
|
|
def test_probe_passes_in_embedded_env(self):
|
|
backend = assert_convrot_backend()
|
|
assert backend in ("triton", "cuda", "eager")
|
|
|
|
def test_probe_fails_without_kitchen(self, monkeypatch):
|
|
import sys
|
|
|
|
saved = sys.modules.get("comfy_kitchen")
|
|
monkeypatch.setitem(sys.modules, "comfy_kitchen", None)
|
|
try:
|
|
with pytest.raises(RuntimeError, match="comfy_kitchen"):
|
|
assert_convrot_backend()
|
|
finally:
|
|
if saved is not None:
|
|
sys.modules["comfy_kitchen"] = saved
|
|
|
|
def test_probe_fails_when_capability_missing(self, monkeypatch):
|
|
import sys
|
|
import types
|
|
|
|
fake = types.ModuleType("comfy_kitchen")
|
|
fake.list_backends = lambda: {
|
|
"eager": {"available": True,
|
|
"capabilities": ["int8_linear"]}, # dequant capability absent
|
|
}
|
|
monkeypatch.setitem(sys.modules, "comfy_kitchen", fake)
|
|
with pytest.raises(RuntimeError, match="capabilities"):
|
|
assert_convrot_backend()
|