Files
WildAi 29488e4a1f feat: unload-on-change eviction + quant-resident runtime for GGUF and quantized safetensors (v2.4.0 -> v2.5.0)
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
2026-08-26 12:17:24 +03:00

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()