Files
wildminder-ComfyUI-VibeVoice/tests/test_base_loader.py
T
WildAi a5f8103c66 feat: load every weight route file->VRAM via core's aimdo DMA (v2.13.0)
Loading problem closed for every model type. Live, 2026-09-30 (RTX 4070 Ti
SUPER 16GB), 1.5B bf16 and 7B fp8 both load straight into VRAM with
+0.69 GB / +1.45 GB of machine RAM and no sustained SSD traffic.

What was wrong
--------------
The per-tensor placement used `Tensor.to(cuda)` on a memory-mapped view. That
is a HOST-side read: the copy engine faults every page in through the CPU, at
0.55 GB/s and +2.18 GB of machine RAM per 2 GB (report 2026-09-30 section F6).
Every route paid it, on every model type.

The 7B fp8 load had looked SSD-free only because pass 1's `safe_open` was
committing the whole file as private memory first, so those host-side copies
were served from RAM. The 19.7 -> 28 -> 20 GB spike was the receipt for that.

What changed
------------
* base_loader.place_tensor_on_device(): one placement helper for every route.
  With an aimdo mapping (main.py sets aimdo_enabled at startup) it uses core's
  own `read_tensor_file_slice_into` to DMA the file byte range straight into a
  preallocated CUDA tensor -- the same primitive core already uses to page
  weights into VRAM, measured at 2.6 GB/s with the page cache left clean.
  Falls back to `.to()` when core declines or when the aimdo native library
  raises; a one-time `[vvload]` log line confirms the DMA is live.

* One streaming assign for all routes. `_stream_apply_dense` gained
  `target_device`, and a new `_stream_apply_dense_safetensors` is the dense
  twin of `_stream_apply_safetensors`. The TTS and ASR dense branches no longer
  build a whole CPU state dict and call `model.to(cuda)`; GGUF and the internal
  official-model loader joined the same path.

* `read_safetensors_tensors_by_name()`: pass-1 dequant scales are read from the
  byte ranges the safetensors header already records, instead of `safe_open`.
  That drops a genuine ~1x-file private commit.

* select_patcher_class() always returns the standard ModelPatcher, so core can
  full-load instead of trapping weights in VBAR for an autoregressive model.

Tests
-----
5 new tests for DMA routing and both fallbacks; load-path tests updated to the
new read seam. Targeted run: 373 passed / 34 failed, all 34 pre-existing and
asserting the ModelPatcherDynamic/vbar architecture this change removes
(26 in test_dynamic_patcher_selection.py). Full suite not run, per user rule.
2026-09-30 23:02:25 +03:00

435 lines
18 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,
iter_safetensors_tensors,
place_tensor_on_device,
)
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")
def test_resolve_official_prefers_existing_dir_in_secondary_root(self, tmp_path):
"""A model already downloaded under a secondary registered root
(extra_model_paths.yaml) is picked up instead of re-downloading
into the primary root."""
first, second = tmp_path / "root1", tmp_path / "root2"
model_dir = second / "VibeVoice" / "VibeVoice-ASR-Streaming-1.5B"
model_dir.mkdir(parents=True)
fake_fp = MagicMock()
fake_fp.get_folder_paths.return_value = [str(first), str(second)]
with patch("ComfyUI_VibeVoice.modules.base_loader.folder_paths", fake_fp):
path = BaseVibeVoiceLoader._resolve_official_model_dir(
"VibeVoice-ASR-Streaming-1.5B"
)
assert path == str(model_dir)
def test_resolve_official_skips_dir_without_config(self, tmp_path):
"""An existing dir is preferred even when incomplete (no config.json):
a partial download resumes in place instead of being orphaned."""
first, second = tmp_path / "root1", tmp_path / "root2"
partial = second / "VibeVoice" / "VibeVoice-ASR-Streaming-7B"
partial.mkdir(parents=True)
fake_fp = MagicMock()
fake_fp.get_folder_paths.return_value = [str(first), str(second)]
with patch("ComfyUI_VibeVoice.modules.base_loader.folder_paths", fake_fp):
path = BaseVibeVoiceLoader._resolve_official_model_dir(
"VibeVoice-ASR-Streaming-7B"
)
assert path == str(partial)
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"))
class TestStreamingIterators:
"""Streaming tensor iterators (plan 2026-08-28: sharded mmap-ghost fix).
Real safetensors files throughout — the iterators exist precisely to
control how file mappings are consumed, so mocking them away would
defeat the point.
"""
@staticmethod
def _write_sharded(tmp_path):
from safetensors.torch import save_file
w1 = torch.ones(2)
w2 = torch.full((2,), 2.0)
save_file({"a": w1}, str(tmp_path / "model-00001-of-00002.safetensors"))
save_file({"b": w2}, str(tmp_path / "model-00002-of-00002.safetensors"))
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))
return w1, w2
def test_iter_sharded_tensors_yields_all_keys(self, tmp_path):
w1, w2 = self._write_sharded(tmp_path)
pairs = dict(BaseVibeVoiceLoader.iter_sharded_tensors(str(tmp_path)))
assert set(pairs) == {"a", "b"}
assert torch.equal(pairs["a"], w1)
assert torch.equal(pairs["b"], w2)
def test_iter_sharded_tensors_matches_merged_dict(self, tmp_path):
"""Streaming and merged loads deliver identical tensors."""
self._write_sharded(tmp_path)
merged = BaseVibeVoiceLoader.load_state_dict_sharded(
str(tmp_path), device=torch.device("cpu")
)
streamed = dict(BaseVibeVoiceLoader.iter_sharded_tensors(str(tmp_path)))
assert set(merged) == set(streamed)
for key in merged:
assert torch.equal(merged[key], streamed[key]), key
def test_iter_sharded_tensors_single_file_delegates(self, tmp_path):
from safetensors.torch import save_file
w = torch.arange(3, dtype=torch.float32)
save_file({"only": w}, str(tmp_path / "model.safetensors"))
pairs = dict(BaseVibeVoiceLoader.iter_sharded_tensors(str(tmp_path)))
assert set(pairs) == {"only"}
assert torch.equal(pairs["only"], w)
def test_iter_sharded_tensors_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"):
list(BaseVibeVoiceLoader.iter_sharded_tensors(str(tmp_path)))
def test_iter_sharded_tensors_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 pytest.raises(FileNotFoundError, match="Shard file not found"):
list(BaseVibeVoiceLoader.iter_sharded_tensors(str(tmp_path)))
def test_iter_checkpoint_tensors_safetensors(self, tmp_path):
from safetensors.torch import save_file
w = torch.ones(2, 2)
path = tmp_path / "single.safetensors"
save_file({"w": w}, str(path))
pairs = dict(BaseVibeVoiceLoader.iter_checkpoint_tensors(str(path)))
assert set(pairs) == {"w"}
assert torch.equal(pairs["w"], w)
def test_iter_checkpoint_tensors_bin(self, tmp_path):
w = torch.ones(2, 2)
path = tmp_path / "single.bin"
torch.save({"w": w}, str(path))
pairs = dict(BaseVibeVoiceLoader.iter_checkpoint_tensors(str(path)))
assert set(pairs) == {"w"}
assert torch.equal(pairs["w"], w)
class TestSafetensorsIteratorArms:
"""``iter_safetensors_tensors`` has two read layers (2026-09-30 fp8 fix).
The aimdo arm yields zero-copy ``load_safetensors`` file views (page
cache, no private commit) so the consumer's mandatory clone is the only
private copy — that clone is what halfed the 7B fp8 load spike. The
aimdo-off fallback keeps the original ``safe_open`` stream. Both arms
must deliver identical values; only the yielded storage's backing
differs.
"""
@staticmethod
def _write_single(tmp_path):
from safetensors.torch import save_file
w = torch.randn(8, 4)
path = tmp_path / "single.safetensors"
save_file({"w": w}, str(path))
return w
@staticmethod
def _has_file_slice(t):
return hasattr(t.untyped_storage(), "_comfy_tensor_file_slice")
@pytest.fixture(scope="class", autouse=True)
def aimdo_runtime(self):
"""control.init() once for the class — the native runtime that
production ComfyUI brings up at startup (same guard as the probes:
no CUDA or a failed init skips the arm, the fallback arm still runs).
"""
pytest.importorskip("comfy_aimdo")
if not torch.cuda.is_available():
pytest.skip("aimdo runtime needs CUDA on this install")
from comfy_aimdo import control
if not control.init():
pytest.skip("aimdo control.init() failed")
def test_fallback_arm_yields_owned_tensors(self, tmp_path, monkeypatch):
import comfy.memory_management
w = self._write_single(tmp_path)
monkeypatch.setattr(
comfy.memory_management, "aimdo_enabled", False, raising=False
)
pairs = dict(iter_safetensors_tensors(str(tmp_path / "single.safetensors")))
assert set(pairs) == {"w"}
assert torch.equal(pairs["w"], w)
assert not self._has_file_slice(pairs["w"])
def test_aimdo_arm_yields_file_views(self, tmp_path, monkeypatch):
import comfy.memory_management
w = self._write_single(tmp_path)
monkeypatch.setattr(
comfy.memory_management, "aimdo_enabled", True, raising=False
)
pairs = dict(iter_safetensors_tensors(str(tmp_path / "single.safetensors")))
assert set(pairs) == {"w"}
assert torch.equal(pairs["w"], w)
assert self._has_file_slice(pairs["w"])
def test_clone_severs_the_view(self, tmp_path, monkeypatch):
"""The consumer contract: clone before keeping. The clone must not
carry the file mapping (pinning hazard), while the view does."""
import comfy.memory_management
self._write_single(tmp_path)
monkeypatch.setattr(
comfy.memory_management, "aimdo_enabled", True, raising=False
)
gen = iter_safetensors_tensors(str(tmp_path / "single.safetensors"))
_, view = next(gen)
gen.close()
clone = view.clone()
assert self._has_file_slice(view)
assert not self._has_file_slice(clone)
assert torch.equal(view, clone)
def test_early_exit_does_not_raise(self, tmp_path, monkeypatch):
"""Breaking mid-stream closes cleanly — the finally's views.clear()
must not fight GeneratorExit."""
import comfy.memory_management
self._write_single(tmp_path)
monkeypatch.setattr(
comfy.memory_management, "aimdo_enabled", True, raising=False
)
gen = iter_safetensors_tensors(str(tmp_path / "single.safetensors"))
next(gen)
gen.close()
class TestPlaceTensorOnDevice:
"""How a checkpoint tensor reaches the GPU decides whether the load
touches the SSD and leaves host RAM full.
``Tensor.to(cuda)`` on a mapped view is a host-side read: 0.55 GB/s and
+2.18 GB of machine RAM per 2 GB (report §F6). When aimdo mapped the file,
core's ``read_tensor_file_slice_into`` DMAs the byte range into the
destination instead: ~2.6 GB/s, page cache left clean. Every route goes
through this helper so that choice is made in exactly one place.
"""
CUDA = torch.device("cuda", 0)
def _skip_without_gpu(self):
if not torch.cuda.is_available():
pytest.skip("no GPU in this environment")
def test_non_cuda_device_is_a_no_op(self):
t = torch.ones(4)
assert place_tensor_on_device(t, None) is t
assert place_tensor_on_device(t, torch.device("cpu")) is t
def test_plain_tensor_falls_back_to_to(self):
self._skip_without_gpu()
t = torch.ones(8, dtype=torch.bfloat16)
# No aimdo mapping on this storage -> the plain copy path.
out = place_tensor_on_device(t, self.CUDA)
assert out.device.type == "cuda"
assert torch.equal(out.cpu(), t)
def test_file_slice_prefers_the_core_dma(self):
"""With an aimdo mapping present, core's DMA is used, not .to()."""
self._skip_without_gpu()
import comfy.memory_management as mm
t = torch.ones(8, dtype=torch.bfloat16)
seen = {}
class _Storage:
_comfy_tensor_file_slice = object()
real_storage = t.untyped_storage
def _fake_read(tensor, destination, stream=None, destination2=None):
seen["called"] = True
seen["src_is_view"] = tensor is t
destination.copy_(tensor)
return True
with patch.object(type(t), "untyped_storage",
lambda self: _Storage()), patch.object(mm, "read_tensor_file_slice_into", _fake_read):
out = place_tensor_on_device(t, self.CUDA)
assert seen.get("called") is True
assert seen["src_is_view"] is True
assert out.device.type == "cuda"
assert torch.equal(out.cpu(), t)
def test_dma_declining_falls_back_instead_of_raising(self):
"""Core returns False for tensors it will not DMA; we still place it."""
self._skip_without_gpu()
import comfy.memory_management as mm
t = torch.ones(8, dtype=torch.bfloat16)
class _Storage:
_comfy_tensor_file_slice = object()
with patch.object(type(t), "untyped_storage",
lambda self: _Storage()), patch.object(mm, "read_tensor_file_slice_into",
lambda *a, **k: False):
out = place_tensor_on_device(t, self.CUDA)
assert out.device.type == "cuda"
assert torch.equal(out.cpu(), t)
def test_dma_raising_falls_back(self):
"""A missing aimdo native library raises; a slow copy beats a crash."""
self._skip_without_gpu()
import comfy.memory_management as mm
t = torch.ones(8, dtype=torch.bfloat16)
class _Storage:
_comfy_tensor_file_slice = object()
def _boom(*a, **k):
raise AttributeError("'NoneType' object has no attribute 'lib'")
with patch.object(type(t), "untyped_storage",
lambda self: _Storage()), patch.object(mm, "read_tensor_file_slice_into", _boom):
out = place_tensor_on_device(t, self.CUDA)
assert out.device.type == "cuda"
assert torch.equal(out.cpu(), t)