Files
WildAi 4d8686f9ad test: clean the suite to match the current architecture, and de-localise it
Suite goes from 1894 passed / 66 failed to 1876 passed / 0 failed.

The 66 failures were not 66 problems. Two mechanisms caused all of them:

  * comfy_stream._STREAMING_CONVERSION_ENABLED is False and nothing in
    production sets it, so convert_tree_for_streaming is a no-op. Every
    test asserting "this module was converted" was asserting on a sweep
    that never ran.
  * select_patcher_class ignores both its arguments and returns
    legacy_cls. Every test parametrized over a "dynamic" arm was
    exercising a configuration production cannot produce.

Deleted (assert wiring that no longer exists): test_dynamic_patcher_
selection.py, test_dynamic_vram_mechanism.py, test_ram_residual_
attribution.py, test_loader_streaming_integration.py (three of its five
tests were passing VACUOUSLY once conversion went), and
TestCensusOnRealDynamicRoute.

Corrected to the real contract, keeping the coverage: the conversion
tests now enable the gate explicitly and test the capability, which is
what they were written for; the TestP1 load tests now assert that the
LOADER places each tensor (it took a target_device) instead of asserting
the superseded CPU staging; the both-patcher-classes tests were
de-parametrized onto the selector's real answer.

Two production error messages had lost the hints their tests pinned, and
the tests were right: UnmappedKeyError no longer told a user with a
non-standard converter to place a sidecar config, and the GGUF block-size
error no longer said the layout could not be recovered.

Also fixes a latent bug: test_comfy_stream.py imported `modules.comfy_stream`
while everything else imported `ComfyUI_VibeVoice.modules.comfy_stream`.
Under pytest those are two distinct module objects with independent
globals, so a gate set through one was invisible to the other. All bare
`modules.*` imports in collected tests now use the package name.

Portability, verified by simulation rather than inspection:

  * conftest no longer falls back to a hardcoded C:\_Dev ComfyUI path. It
    used to put a nonexistent directory on sys.path off-box and surface
    as an opaque import error across 14 test files; now it names
    COMFYUI_ROOT and stops.
  * The 5.4 GB and 3.2 GB checkpoint fixtures are opt-in via env vars with
    no machine-specific default (VIBEVOICE_TEST_DENSE_CHECKPOINT,
    VIBEVOICE_TEST_GGUF).
  * comfy_kitchen guards added. It is genuinely optional; a driver
    script that blocks the import went from 9 failed to 37 passed /
    11 skipped / 0 errors.
  * comfy_aimdo import guarded, though it is in practice a hard
    dependency of the ComfyUI fork (core imports it unconditionally).
  * test_audio_backend writes to tmp_path instead of into the repo tree.
  * test_asr_loader no longer registers the real folder_paths.models_dir
    in an autouse fixture without restoring it.

The large-checkpoint and .dev-symlink tests skip cleanly off-box; that is
the intended shape for files that cannot ship with the repo.
2026-10-01 15:49:15 +03:00

588 lines
25 KiB
Python

"""Pytest configuration for ComfyUI-VibeVoice tests.
Sets up the ComfyUI path, mocks the network server, and registers the
custom node package under a stable alias so relative imports work.
"""
import sys
import os
import importlib.util
from unittest.mock import MagicMock, patch
import pytest
# torch is required by the behavioral patcher stub (_TinyHandler) below.
import torch # noqa: E402
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
# ====================================================================
# 1. PATH CONFIGURATION
# ====================================================================
def _resolve_comfyui_root() -> str:
"""Locate a ComfyUI checkout for the suite to import.
An explicit ``COMFYUI_ROOT`` always wins and is returned verbatim (even if
missing — a bad override should surface as a loud import error, not be
silently "helpfully" replaced).
Otherwise the parent of this repo and its siblings are scanned for a
checkout carrying ComfyUI's top-level ``nodes.py`` and ``execution.py``.
There is deliberately NO hardcoded fallback. An earlier revision defaulted
to ``C:\\_Dev\\ComfyUI_dev\\ComfyUI``, which on any other machine put a
nonexistent path on ``sys.path`` and surfaced as an opaque "attempted
relative import beyond top-level package" (the repo's own ``nodes`` package
shadowing ComfyUI's) across a dozen unrelated test files. Failing here, by
name, is the only actionable outcome.
The scan is a convenience for a developer who keeps ComfyUI beside this
repo; ``COMFYUI_ROOT`` is the portable answer and the only one a fresh
checkout needs.
"""
explicit = os.environ.get("COMFYUI_ROOT")
if explicit:
return explicit
# Search the parent of the ComfyUI_vendored-nodes tree first, then its
# siblings, for any checkout that carries ComfyUI's top-level modules.
search_dirs = []
comfy_nodes_parent = os.path.dirname(os.path.dirname(ROOT_DIR)) # .../ComfyUI_dev
if os.path.isdir(comfy_nodes_parent):
search_dirs.append(comfy_nodes_parent)
search_dirs.append(os.path.dirname(os.path.dirname(ROOT_DIR)))
for base in search_dirs:
try:
entries = sorted(os.listdir(base))
except OSError:
continue
for entry in entries:
candidate = os.path.join(base, entry)
if _is_comfyui_root(candidate):
return candidate
nested = os.path.join(candidate, "ComfyUI")
if _is_comfyui_root(nested):
return nested
# Nothing found. There is deliberately NO hardcoded fallback: a stale path
# on sys.path produces an opaque import error deep inside an unrelated test
# ("attempted relative import beyond top-level package", or a module that
# silently resolves to the wrong tree). Naming the missing prerequisite is
# the only actionable thing this function can do, so it says so loudly and
# stops collection here rather than 14 files later.
raise RuntimeError(
"Could not locate a ComfyUI checkout.\n"
"These tests import ComfyUI itself (`comfy`, `folder_paths`), so a "
"real installation is required.\n"
"Point COMFYUI_ROOT at it and re-run, e.g.:\n"
" COMFYUI_ROOT=/path/to/ComfyUI pytest tests/\n"
"A directory qualifies when it contains ComfyUI's top-level nodes.py "
"and execution.py.\n"
f"Searched: {search_dirs}"
)
def _is_comfyui_root(path: str) -> bool:
"""True when ``path`` is a ComfyUI root carrying its top-level modules."""
return all(
os.path.isfile(os.path.join(path, name))
for name in ("nodes.py", "execution.py")
)
COMFYUI_ROOT = _resolve_comfyui_root()
if COMFYUI_ROOT not in sys.path:
sys.path.insert(0, COMFYUI_ROOT)
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
# ====================================================================
# 2. MOCK ONLY THE NETWORK SERVER
# ====================================================================
# We let comfy and folder_paths load normally from COMFYUI_ROOT.
# We only mock 'server' and 'aiohttp' to prevent network activity.
_mock_server = MagicMock()
_mock_prompt_server_instance = MagicMock()
_mock_server.PromptServer.instance = _mock_prompt_server_instance
sys.modules.setdefault("server", _mock_server)
sys.modules.setdefault("aiohttp", MagicMock())
sys.modules.setdefault("aiohttp.web", MagicMock())
# ====================================================================
# 2b. MOCK HEAVY VENDORED DEPENDENCIES
# ====================================================================
# The vendored VibeVoice source imports diffusers, which may be broken
# in some environments (e.g. huggingface_hub version mismatch). We mock
# the heavy vendored modules so tests can import modules.loader etc.
# without actually loading the full model stack.
# Tests that need real model behavior already mock at the function level.
# Mock the entire src.vibevoice package to prevent loading heavy dependencies
# This must be done BEFORE any imports that might trigger the vendored code
_src_vibevoice_mock = MagicMock()
_src_vibevoice_mock.__path__ = [] # Make it a package
sys.modules["src.vibevoice"] = _src_vibevoice_mock
sys.modules["src.vibevoice.modular"] = MagicMock()
sys.modules["src.vibevoice.processor"] = MagicMock()
sys.modules["src.vibevoice.schedule"] = MagicMock()
sys.modules["src.vibevoice.configs"] = MagicMock()
# Also mock the aliased versions
sys.modules["ComfyUI_VibeVoice.src.vibevoice"] = _src_vibevoice_mock
sys.modules["ComfyUI_VibeVoice.src.vibevoice.modular"] = MagicMock()
sys.modules["ComfyUI_VibeVoice.src.vibevoice.processor"] = MagicMock()
sys.modules["ComfyUI_VibeVoice.src.vibevoice.schedule"] = MagicMock()
sys.modules["ComfyUI_VibeVoice.src.vibevoice.configs"] = MagicMock()
# Mock specific submodules that are imported directly
_vibevoice_mock_modules = [
"src.vibevoice.modular.configuration_vibevoice",
"src.vibevoice.modular.configuration_vibevoice_streaming",
"src.vibevoice.modular.modeling_vibevoice",
"src.vibevoice.modular.modeling_vibevoice_asr",
"src.vibevoice.modular.modeling_vibevoice_streaming",
"src.vibevoice.modular.modeling_vibevoice_streaming_inference",
"src.vibevoice.modular.modular_vibevoice_diffusion_head",
"src.vibevoice.modular.modular_vibevoice_text_tokenizer",
"src.vibevoice.modular.modular_vibevoice_tokenizer",
"src.vibevoice.modular.sage_attention_patch",
"src.vibevoice.modular.streamer",
"src.vibevoice.processor.audio_utils",
"src.vibevoice.processor.vibevoice_asr_processor",
"src.vibevoice.processor.vibevoice_processor",
"src.vibevoice.processor.vibevoice_streaming_processor",
"src.vibevoice.processor.vibevoice_tokenizer_processor",
"src.vibevoice.schedule.dpm_solver",
"src.vibevoice.schedule.timestep_sampler",
# Also mock the aliased versions
"ComfyUI_VibeVoice.src.vibevoice.modular.configuration_vibevoice",
"ComfyUI_VibeVoice.src.vibevoice.modular.configuration_vibevoice_streaming",
"ComfyUI_VibeVoice.src.vibevoice.modular.modeling_vibevoice",
"ComfyUI_VibeVoice.src.vibevoice.modular.modeling_vibevoice_asr",
"ComfyUI_VibeVoice.src.vibevoice.modular.modeling_vibevoice_streaming",
"ComfyUI_VibeVoice.src.vibevoice.modular.modeling_vibevoice_streaming_inference",
"ComfyUI_VibeVoice.src.vibevoice.modular.modular_vibevoice_diffusion_head",
"ComfyUI_VibeVoice.src.vibevoice.modular.modular_vibevoice_text_tokenizer",
"ComfyUI_VibeVoice.src.vibevoice.modular.modular_vibevoice_tokenizer",
"ComfyUI_VibeVoice.src.vibevoice.modular.sage_attention_patch",
"ComfyUI_VibeVoice.src.vibevoice.modular.streamer",
"ComfyUI_VibeVoice.src.vibevoice.processor.audio_utils",
"ComfyUI_VibeVoice.src.vibevoice.processor.vibevoice_asr_processor",
"ComfyUI_VibeVoice.src.vibevoice.processor.vibevoice_processor",
"ComfyUI_VibeVoice.src.vibevoice.processor.vibevoice_streaming_processor",
"ComfyUI_VibeVoice.src.vibevoice.processor.vibevoice_tokenizer_processor",
"ComfyUI_VibeVoice.src.vibevoice.schedule.dpm_solver",
"ComfyUI_VibeVoice.src.vibevoice.schedule.timestep_sampler",
]
for _mod_name in _vibevoice_mock_modules:
if _mod_name not in sys.modules:
sys.modules[_mod_name] = MagicMock()
# ====================================================================
# 3. NATIVE ALIAS FOR THE CUSTOM NODE PACKAGE
# ====================================================================
# ComfyUI loads custom nodes by directory name. When the directory is
# "ComfyUI-VibeVoice" the import name becomes "ComfyUI-VibeVoice" which
# is not a valid Python identifier. We register it under a stable alias.
_PKG_ALIAS = "ComfyUI_VibeVoice"
if _PKG_ALIAS not in sys.modules:
spec = importlib.util.spec_from_file_location(
_PKG_ALIAS,
os.path.join(ROOT_DIR, "__init__.py"),
submodule_search_locations=[ROOT_DIR],
)
pkg = importlib.util.module_from_spec(spec)
sys.modules[_PKG_ALIAS] = pkg
# The __init__.py has a pytest guard that exits early, so this is safe.
try:
spec.loader.exec_module(pkg)
except Exception:
# If __init__.py fails (e.g. missing deps during collection),
# the alias is still registered for submodule imports.
pass
# Register submodules under the package alias so relative imports work
for _submod in ("modules", "nodes", "src"):
_submod_path = os.path.join(ROOT_DIR, _submod)
if os.path.isdir(_submod_path):
_submod_alias = f"{_PKG_ALIAS}.{_submod}"
if _submod_alias not in sys.modules:
_submod_spec = importlib.util.spec_from_file_location(
_submod_alias,
os.path.join(_submod_path, "__init__.py"),
submodule_search_locations=[_submod_path],
)
_submod_pkg = importlib.util.module_from_spec(_submod_spec)
sys.modules[_submod_alias] = _submod_pkg
try:
_submod_spec.loader.exec_module(_submod_pkg)
except Exception:
pass
# ====================================================================
# 4. GLOBAL FIXTURES
# ====================================================================
@pytest.fixture
def mock_prompt_server():
"""Provide a shared PromptServer mock with reset send_sync."""
_mock_server.PromptServer.instance = _mock_prompt_server_instance
_mock_prompt_server_instance.send_sync.reset_mock()
return _mock_prompt_server_instance
@pytest.fixture
def temp_model_dir(tmp_path):
"""Create a temporary model directory mimicking models/tts/VibeVoice."""
model_dir = tmp_path / "models" / "tts" / "VibeVoice"
model_dir.mkdir(parents=True, exist_ok=True)
return model_dir
@pytest.fixture(autouse=True)
def comfyui_env():
"""Indicate that we are running in a ComfyUI test environment."""
return True
# ====================================================================
# 5. BEHAVIORAL PATCHER STUB (NTH-003)
# ====================================================================
# A tiny torch.nn.Module stand-in for the real VibeVoice handler so the
# patcher's device transitions and cache keying can be exercised in CI
# without loading gigabytes of weights. Shared with CRIT-001 / NTH-004 tests.
class _TinyHandler(torch.nn.Module):
def __init__(self):
super().__init__()
# AUD-010: mirror the real VibeVoiceModelHandler — the heavy model is
# lazily created by load_model(), not pre-set in __init__. This lets the
# patcher's lazy-load branch (``if self.model.model is None``) be
# exercised by the behavioral tests.
self.model = None
self.processor = object()
self.model_pack_name = "tiny"
self.cache_key = "tiny"
self.size = 1024
def load_model(self, device, attention_mode: str = "sdpa"):
# Mirror the real handler: lazy instantiation + move onto target device.
if self.model is None:
self.model = torch.nn.Linear(8, 8)
self.model.to(device)
@pytest.fixture
def tiny_handler():
"""A fresh tiny stub handler (no patcher)."""
return _TinyHandler()
@pytest.fixture
def tiny_patcher(tiny_handler):
"""A VibeVoicePatcher wrapping the tiny stub handler, ModelPatcher.__init__ mocked."""
from ComfyUI_VibeVoice.modules.patcher import VibeVoicePatcher
with patch("comfy.model_patcher.ModelPatcher.__init__"):
patcher = VibeVoicePatcher(
tiny_handler,
attention_mode="sdpa",
load_device=torch.device("cpu"),
offload_device=torch.device("cpu"),
size=1024,
)
# Attributes ModelPatcher.__init__ would normally set.
patcher.load_device = torch.device("cpu")
patcher.offload_device = torch.device("cpu")
patcher.model = tiny_handler
patcher.pinned = set() # ModelPatcher.__del__ → unpin_all_weights() needs this
return patcher
# ====================================================================
# 6. SYNTHETIC GGUF FIXTURES (quant-resident runtime)
# ====================================================================
# Spec-driven builders producing real .gguf files via gguf.GGUFWriter so the
# loader/planner/modules are exercised against genuine container bytes.
# K-quants cannot be produced by gguf-py (dequantize-only), so Q4_K/Q5_K/Q6_K
# blocks are handcrafted with controlled fp16 scales — valid blocks whose
# dequantized values are finite and bounded (usable by forward-parity tests).
_GGUF_BLOCK_SHAPES = {
"Q8_0": (32, 34),
"Q4_K": (256, 144),
"Q5_K": (256, 176),
"Q6_K": (256, 210),
}
def craft_kquant_blocks(qtype_name: str, n_elements: int, seed: int = 0):
"""Handcraft one flat uint8 array of raw K-quant/Q8_0 blocks.
Scales are small sane fp16 values and quanta bounded, keeping
dequantized magnitudes well inside float range.
"""
import numpy as np
block_size, type_size = _GGUF_BLOCK_SHAPES[qtype_name]
n_blocks = n_elements // block_size
rng = np.random.default_rng(seed)
b = np.zeros((n_blocks, type_size), dtype=np.uint8)
def _fp16_bytes(values):
return np.asarray(values, dtype=np.float16).view(np.uint8).reshape(-1)
d = 0.03 + np.abs(rng.standard_normal(n_blocks)) * 0.02
if qtype_name == "Q8_0":
qs = rng.integers(0, 256, size=(n_blocks, 32), dtype=np.uint8)
b[:, 0:2] = _fp16_bytes(d).reshape(n_blocks, 2)
b[:, 2:] = qs
elif qtype_name in ("Q4_K", "Q5_K"):
dmin = 0.0005 + np.abs(rng.standard_normal(n_blocks)) * 0.001
b[:, 0:2] = _fp16_bytes(d).reshape(n_blocks, 2)
b[:, 2:4] = _fp16_bytes(dmin).reshape(n_blocks, 2)
b[:, 4:16] = rng.integers(0, 64, size=(n_blocks, 12), dtype=np.uint8)
if qtype_name == "Q4_K":
b[:, 16:] = rng.integers(0, 256, size=(n_blocks, 128), dtype=np.uint8)
else:
b[:, 16:48] = rng.integers(0, 256, size=(n_blocks, 32), dtype=np.uint8)
b[:, 48:] = rng.integers(0, 256, size=(n_blocks, 128), dtype=np.uint8)
elif qtype_name == "Q6_K":
# scales int8 near zero; keep |scale| small so products stay finite
b[:, 0:128] = rng.integers(0, 256, size=(n_blocks, 128), dtype=np.uint8)
b[:, 128:192] = rng.integers(0, 256, size=(n_blocks, 64), dtype=np.uint8)
b[:, 192:208] = rng.integers(60, 68, size=(n_blocks, 16)).astype(np.uint8)
b[:, 208:210] = _fp16_bytes(
0.01 + np.abs(rng.standard_normal(n_blocks)) * 0.01
).reshape(n_blocks, 2)
else:
raise ValueError(f"craft_kquant_blocks: unsupported type {qtype_name}")
return b
def write_synthetic_gguf(path, spec, seed: int = 0, flat_pool: set = None):
"""Write a .gguf file from ``spec`` and return ``path``.
Args:
path: Target file path (str or pathlib.Path).
spec: Iterable of ``(name, qtype_str_or_float, shape)`` tuples where
shape is the TORCH-logical shape (out, in) for weights.
Float types: 'F32' / 'F16' / 'BF16'. Quant types: 'Q8_0' /
'Q4_K' / 'Q5_K' / 'Q6_K'.
seed: Deterministic content seed.
flat_pool: Optional set of tensor names to lay out as a quantui-rs
style FLAT block pool (whole-block C-order bytes, header rows
below the block size). Mimics third-party converters that
quantize small-kernel conv weights.
Returns:
The written path (as given).
"""
import numpy as np
import gguf as _gguf
from gguf.constants import GGMLQuantizationType as T
flat_pool = flat_pool or set()
writer = _gguf.GGUFWriter(str(path), "vibevoice")
try:
for i, (name, kind, shape) in enumerate(spec):
n_elem = int(np.prod(shape))
if kind == "F32":
rng = np.random.default_rng(seed * 1000 + i)
writer.add_tensor(name, (rng.standard_normal(shape) * 0.05).astype(np.float32),
raw_dtype=T.F32)
elif kind == "F16":
rng = np.random.default_rng(seed * 1000 + i)
writer.add_tensor(name, (rng.standard_normal(shape) * 0.05).astype(np.float16),
raw_dtype=T.F16)
elif kind == "BF16":
rng = np.random.default_rng(seed * 1000 + i)
f32 = (rng.standard_normal(shape) * 0.05).astype(np.float32)
u16 = (f32.view(np.uint32) >> 16).astype(np.uint16)
rows = shape[0]
writer.add_tensor(name, u16.view(np.uint8).reshape(rows, -1),
raw_dtype=T.BF16)
elif kind in _GGUF_BLOCK_SHAPES:
block_size, type_size = _GGUF_BLOCK_SHAPES[kind]
if name in flat_pool:
# quantui-rs layout: header keeps the LOGICAL ggml dims
# (ne[0] = kernel size, possibly < block size) while the
# payload is a flat pool of whole blocks. gguf-py's writer
# cannot express sub-block rows (it derives logical dims
# from the byte shape), so bypass its byte-shape
# conversion for this one tensor exactly like quantui-rs
# writes raw headers.
if n_elem % block_size != 0:
raise ValueError(
f"{kind} flat pool requires a whole-block "
f"element count, got {shape}"
)
bytes_flat = craft_kquant_blocks(kind, n_elem, seed=seed * 1000 + i)
import gguf.gguf_writer as _gw
_orig_conv = _gw.quant_shape_from_byte_shape
_gw.quant_shape_from_byte_shape = (
lambda shp, qtype: tuple(shp)
)
try:
# raw_shape = torch-logical shape; the writer stores
# header dims REVERSED, giving ggml ne order with
# ne[0] = last torch dim (the kernel size).
writer.add_tensor(name, bytes_flat.reshape(-1),
raw_shape=shape,
raw_dtype=getattr(T, kind))
finally:
_gw.quant_shape_from_byte_shape = _orig_conv
elif n_elem % block_size != 0 or shape[-1] % block_size != 0:
raise ValueError(
f"{kind} requires last dim multiple of {block_size}: {shape}"
)
else:
bytes_flat = craft_kquant_blocks(kind, n_elem, seed=seed * 1000 + i)
rows = shape[0]
bytes_per_row = shape[-1] // block_size * type_size
writer.add_tensor(name, bytes_flat.reshape(rows, bytes_per_row),
raw_dtype=getattr(T, kind))
else:
raise ValueError(f"write_synthetic_gguf: unknown kind {kind!r}")
writer.write_header_to_file()
writer.write_kv_data_to_file()
writer.write_tensors_to_file()
finally:
writer.close()
return path
@pytest.fixture
def make_gguf_file(tmp_path):
"""Factory fixture: write_synthetic_gguf(tmp_path/'<tag>.gguf', spec)."""
def _make(spec, tag="model", seed=0):
return write_synthetic_gguf(tmp_path / f"{tag}.gguf", spec, seed=seed)
return _make
def build_stub_vv(n_layers: int = 1, hidden: int = 64, ffn: int = 128,
vocab: int = 96, seed: int = 0):
"""Build a tiny real-parameter module tree mirroring VibeVoice topology.
Module paths match the HF pass-through naming seen in real checkpoints:
model.language_model.layers.N.{self_attn.{q,k,v,o}_proj,
mlp.{gate,up,down}_proj,
input_layernorm, post_attention_layernorm}
model.language_model.embed_tokens / .norm
lm_head (untied here for simplicity)
model.prediction_head.cond_proj / final_layer.linear
Returns an eager (non-meta) nn.Module in eval mode.
"""
import torch.nn as nn
class _Lin(nn.Linear):
def __init__(self, i, o):
super().__init__(i, o, bias=False)
class _SelfAttn(nn.Module):
def __init__(self):
super().__init__()
self.q_proj = _Lin(hidden, hidden)
self.k_proj = _Lin(hidden, hidden // 4)
self.v_proj = _Lin(hidden, hidden // 4)
self.o_proj = _Lin(hidden, hidden)
class _MLP(nn.Module):
def __init__(self):
super().__init__()
self.gate_proj = _Lin(hidden, ffn)
self.up_proj = _Lin(hidden, ffn)
self.down_proj = _Lin(ffn, hidden)
class _Norm(nn.Module):
def __init__(self):
super().__init__()
self.weight = nn.Parameter(torch.ones(hidden))
def forward(self, x):
# RMSNorm-shaped residual-scale (matches real model semantics
# closely enough for pipeline tests).
return x * self.weight
class _Layer(nn.Module):
def __init__(self):
super().__init__()
self.self_attn = _SelfAttn()
self.mlp = _MLP()
self.input_layernorm = _Norm()
self.post_attention_layernorm = _Norm()
class _LM(nn.Module):
def __init__(self):
super().__init__()
self.embed_tokens = nn.Embedding(vocab, hidden)
self.layers = nn.ModuleList([_Layer() for _ in range(n_layers)])
self.norm = _Norm()
class _Model(nn.Module):
def __init__(self):
super().__init__()
self.language_model = _LM()
self.prediction_head = nn.Module()
self.prediction_head.cond_proj = _Lin(hidden, hidden)
self.prediction_head.final_layer = nn.Module()
self.prediction_head.final_layer.linear = _Lin(hidden, hidden // 4)
self.speech_scaling_factor = nn.Parameter(torch.tensor(float("nan")))
self.speech_bias_factor = nn.Parameter(torch.tensor(float("nan")))
class _VV(nn.Module):
def __init__(self):
super().__init__()
self.model = _Model()
self.lm_head = _Lin(hidden, vocab)
torch.manual_seed(seed)
m = _VV().eval()
# Give every linear distinct float32 weights so parity checks are strict.
g = torch.Generator().manual_seed(seed)
for name, p in m.named_parameters():
if p.dtype.is_floating_point:
p.data = torch.randn(p.shape, generator=g) * 0.02
m.model.speech_scaling_factor.data.fill_(float("nan"))
m.model.speech_bias_factor.data.fill_(float("nan"))
return m
def stub_vv_gguf_spec(n_layers: int = 1, hidden: int = 64, ffn: int = 128,
vocab: int = 96, qtype: str = "Q8_0"):
"""GGUF writer spec for :func:`build_stub_vv` linears (HF naming).
Quantizable linears use ``qtype``; everything else stays float
(F32 norms / BF16 embeddings + remaining linears), mirroring the real
vibevoice-1.5b-q8_0.gguf layout.
"""
spec = [
("model.language_model.embed_tokens.weight", "BF16", (vocab, hidden)),
("model.language_model.norm.weight", "F32", (hidden,)),
]
for i in range(n_layers):
p = f"model.language_model.layers.{i}"
spec += [
(f"{p}.input_layernorm.weight", "F32", (hidden,)),
(f"{p}.post_attention_layernorm.weight", "F32", (hidden,)),
(f"{p}.self_attn.q_proj.weight", qtype, (hidden, hidden)),
(f"{p}.self_attn.k_proj.weight", qtype, (hidden // 4, hidden)),
(f"{p}.self_attn.v_proj.weight", qtype, (hidden // 4, hidden)),
(f"{p}.self_attn.o_proj.weight", qtype, (hidden, hidden)),
(f"{p}.mlp.gate_proj.weight", qtype, (ffn, hidden)),
(f"{p}.mlp.up_proj.weight", qtype, (ffn, hidden)),
(f"{p}.mlp.down_proj.weight", qtype, (hidden, ffn)),
]
spec += [
("model.prediction_head.cond_proj.weight", "BF16", (hidden, hidden)),
("model.prediction_head.final_layer.linear.weight", "F32", (hidden // 4, hidden)),
("lm_head.weight", "BF16", (vocab, hidden)),
]
return spec
@pytest.fixture
def make_stub_vv():
return build_stub_vv