Files
wildminder-ComfyUI-VibeVoice/tests/test_base_loader.py
T
WildAi dc64c17c00 fix: cpu-first loading + neg-branch rope fix
- DF-001..006: loader builds the model entirely on CPU (state dict, load_state_dict, dtype cast, 4-bit); patcher owns the single H2D transfer after ComfyUI load_models_gpu arbitration. Removes the disk->VRAM->RAM->VRAM round-trip; peak load VRAM ~2x -> ~1x model size.

- BUG-011: negative CFG forward passed full-length position_ids with single-token inputs_embeds; transformers 5.x RoPE broadcast expanded q/k (not v) -> KV cache key/value desync -> SDPA crash [12,3] vs [12,2]. Now current-only position_ids.

- Tests: +36 device-flow, +4 neg-position-ids (red/green verified), +scheduler/e2e/mm-surface suites. Full suite 461p/5 pre-existing f/4s.

- Version 2.0.2; untrack .workbuddy-ai, ignore tmp/
2026-08-15 16:46:56 +03:00

129 lines
5.6 KiB
Python

"""Tests for modules/base_loader.py - shared BaseVibeVoiceLoader (IMP-004).
Exercises the GPU-free helpers (path resolution, snapshot download, tokenizer
repo, sharded/single state-dict loading) in isolation.
"""
import json
import torch
import pytest
from unittest.mock import patch, MagicMock
from ComfyUI_VibeVoice.modules.base_loader import BaseVibeVoiceLoader
class TestResolveOfficialModelDir:
def test_resolve_official_paths_uses_folder_paths(self, tmp_path):
with patch("ComfyUI_VibeVoice.modules.base_loader.folder_paths") as mock_fp:
mock_fp.get_folder_paths.return_value = [str(tmp_path)]
path = BaseVibeVoiceLoader._resolve_official_model_dir("VibeVoice-ASR")
assert path == str(tmp_path / "VibeVoice" / "VibeVoice-ASR")
def test_resolve_official_paths_falls_back_to_models_dir(self, tmp_path):
fake_fp = MagicMock()
# No "tts" folder registered → falls back to <models_dir>/tts
fake_fp.get_folder_paths.return_value = []
fake_fp.models_dir = str(tmp_path)
with patch("ComfyUI_VibeVoice.modules.base_loader.folder_paths", fake_fp):
path = BaseVibeVoiceLoader._resolve_official_model_dir("VibeVoice-ASR")
assert path == str(tmp_path / "tts" / "VibeVoice" / "VibeVoice-ASR")
class TestEnsureDownloaded:
def test_ensure_downloaded_triggers_snapshot(self, tmp_path):
local_dir = tmp_path / "model"
local_dir.mkdir()
repo_id = "microsoft/VibeVoice-ASR"
with patch("huggingface_hub.snapshot_download") as mock_dl:
BaseVibeVoiceLoader._ensure_downloaded(
repo_id=repo_id, local_dir=str(local_dir), model_name="VibeVoice-ASR"
)
mock_dl.assert_called_once()
_, kwargs = mock_dl.call_args
assert kwargs.get("repo_id") == repo_id
assert kwargs.get("local_dir") == str(local_dir)
# AUD-013: the symlinks kwarg is only passed when the installed
# huggingface_hub still accepts it (removed in >= 0.23 / 1.x).
import inspect
import huggingface_hub
supports_symlinks = "local_dir_use_symlinks" in inspect.signature(
huggingface_hub.snapshot_download
).parameters
if supports_symlinks:
assert kwargs.get("local_dir_use_symlinks") is False
else:
assert "local_dir_use_symlinks" not in kwargs
def test_ensure_downloaded_skips_when_config_present(self, tmp_path):
local_dir = tmp_path / "model"
local_dir.mkdir()
(local_dir / "config.json").write_text("{}")
with patch("huggingface_hub.snapshot_download") as mock_dl:
BaseVibeVoiceLoader._ensure_downloaded(repo_id="x/y", local_dir=str(local_dir))
mock_dl.assert_not_called()
def test_ensure_downloaded_no_repo_returns_early(self, tmp_path):
local_dir = tmp_path / "model"
local_dir.mkdir()
with patch("huggingface_hub.snapshot_download") as mock_dl:
BaseVibeVoiceLoader._ensure_downloaded(repo_id="", local_dir=str(local_dir))
mock_dl.assert_not_called()
class TestTokenizerRepo:
def test_tokenizer_repo_for_asr_uses_7b(self):
assert BaseVibeVoiceLoader.tokenizer_repo_for("VibeVoice-ASR") == "Qwen/Qwen2.5-7B"
def test_tokenizer_repo_for_small_uses_15b(self):
assert BaseVibeVoiceLoader.tokenizer_repo_for("VibeVoice-1.5B") == "Qwen/Qwen2.5-1.5B"
class TestLoadStateDictSharded:
def test_load_state_dict_sharded_single_file(self, tmp_path):
(tmp_path / "model.safetensors").write_text("dummy")
with patch(
"ComfyUI_VibeVoice.modules.base_loader.comfy.utils.load_torch_file",
return_value={"w": "v"},
):
sd = BaseVibeVoiceLoader.load_state_dict_sharded(str(tmp_path), device=torch.device("cpu"))
assert sd == {"w": "v"}
def test_load_state_dict_sharded_merges_shards(self, tmp_path):
(tmp_path / "model-00001-of-00002.safetensors").write_text("d1")
(tmp_path / "model-00002-of-00002.safetensors").write_text("d2")
index = {
"weight_map": {
"a": "model-00001-of-00002.safetensors",
"b": "model-00002-of-00002.safetensors",
}
}
(tmp_path / "model.safetensors.index.json").write_text(json.dumps(index))
def mock_load(path, device=None):
if "00001" in path:
return {"a": 1}
return {"b": 2}
with patch(
"ComfyUI_VibeVoice.modules.base_loader.comfy.utils.load_torch_file",
side_effect=mock_load,
):
sd = BaseVibeVoiceLoader.load_state_dict_sharded(str(tmp_path), device=torch.device("cpu"))
assert sd == {"a": 1, "b": 2}
def test_load_state_dict_sharded_empty_weight_map_raises(self, tmp_path):
(tmp_path / "model.safetensors.index.json").write_text(json.dumps({"weight_map": {}}))
with pytest.raises(ValueError, match="empty weight_map"):
BaseVibeVoiceLoader.load_state_dict_sharded(str(tmp_path), device=torch.device("cpu"))
def test_load_state_dict_sharded_missing_shard_raises(self, tmp_path):
(tmp_path / "model.safetensors.index.json").write_text(
json.dumps({"weight_map": {"a": "model-00001-of-00002.safetensors"}})
)
with patch(
"ComfyUI_VibeVoice.modules.base_loader.comfy.utils.load_torch_file",
return_value={},
):
with pytest.raises(FileNotFoundError, match="Shard file not found"):
BaseVibeVoiceLoader.load_state_dict_sharded(str(tmp_path), device=torch.device("cpu"))