End-to-end validation on RTX 5090 (ComfyUI 0.24.0, Hunyuan3DWrapper master, Hunyuan3D-2mini fp16, 30-step image-to-mesh workflow via the ComfyUI API): - patch engages exactly on schedule (hermite i3: 11 computed + 19 skipped; dmd i5: 7 + 23), sampling 16.1 -> 43.1 it/s (2.68x) for hermite i3 - quality A/B same seed vs unaccelerated mesh: hermite i3 F1@0.05 0.825 (different-seed floor 0.751); dmd i5 0.719, at the floor on mini, so the upstream DMD lossless claim is not repeated for this node - repeat runs in one session reset per-run state correctly Two bugs found by the validation, both fixed with regression tests: 1. ComfyUI cache aliasing: the node mutated and returned the same pipeline object, so a cached node output could silently run a different config after an intervening run re-patched the shared pipeline (observed live: a cached hermite-i3 output ran dmd-i5). apply/remove now return a shallow copy that owns its patch; the input pipeline is never mutated. 2. Run-boundary reset missed back-to-back single-step runs (t=0 then t=0); the boundary check is now non-increasing instead of strictly decreasing. Defaults switch to the best-measured config (hermite, interval 3). Tests 35 -> 38. README validation section rewritten from measurements; REGISTRY-SUBMIT.md documents the registry.comfy.org publish flow.
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
"""Node-pack registration tests — run standalone, no ComfyUI install.
|
|
|
|
ComfyUI loads a custom node pack by importing the package and reading
|
|
``NODE_CLASS_MAPPINGS`` / ``NODE_DISPLAY_NAME_MAPPINGS``; these tests verify
|
|
that contract plus the INPUT_TYPES schema shape ComfyUI expects.
|
|
"""
|
|
import importlib.util
|
|
import pathlib
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
PACK_DIR = pathlib.Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _load_pack():
|
|
"""Import the node pack the way ComfyUI does (as a package by path)."""
|
|
name = "comfyui_hicache_under_test"
|
|
if name in sys.modules:
|
|
return sys.modules[name]
|
|
spec = importlib.util.spec_from_file_location(
|
|
name, PACK_DIR / "__init__.py",
|
|
submodule_search_locations=[str(PACK_DIR)],
|
|
)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
sys.modules[name] = mod
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def test_pack_imports_without_comfy():
|
|
assert "comfy" not in sys.modules, "test must run without ComfyUI"
|
|
pack = _load_pack()
|
|
assert hasattr(pack, "NODE_CLASS_MAPPINGS")
|
|
assert hasattr(pack, "NODE_DISPLAY_NAME_MAPPINGS")
|
|
assert "comfy" not in sys.modules, "node pack must not import comfy internals"
|
|
|
|
|
|
def test_node_mappings_consistent():
|
|
pack = _load_pack()
|
|
assert "HiCacheAccelerate" in pack.NODE_CLASS_MAPPINGS
|
|
for key in pack.NODE_CLASS_MAPPINGS:
|
|
assert key in pack.NODE_DISPLAY_NAME_MAPPINGS
|
|
|
|
|
|
def test_input_types_schema():
|
|
pack = _load_pack()
|
|
cls = pack.NODE_CLASS_MAPPINGS["HiCacheAccelerate"]
|
|
schema = cls.INPUT_TYPES()
|
|
req = schema["required"]
|
|
|
|
# model input + the contract from the task spec: method/interval/warmup
|
|
assert req["pipeline"][0] == "HY3DMODEL"
|
|
assert set(req["method"][0]) == {"hermite", "dmd", "auto"}
|
|
assert req["method"][1]["default"] in req["method"][0]
|
|
assert req["interval"][0] == "INT"
|
|
assert req["interval"][1]["min"] >= 1
|
|
assert req["warmup_steps"][0] == "INT"
|
|
assert req["warmup_steps"][1]["min"] == 0
|
|
assert "enable" in schema["optional"]
|
|
|
|
# ComfyUI node contract
|
|
assert cls.RETURN_TYPES == ("HY3DMODEL",)
|
|
assert callable(getattr(cls, cls.FUNCTION))
|
|
assert isinstance(cls.CATEGORY, str)
|
|
|
|
|
|
def test_node_function_patches_and_unpatches():
|
|
import torch
|
|
|
|
pack = _load_pack()
|
|
node = pack.NODE_CLASS_MAPPINGS["HiCacheAccelerate"]()
|
|
|
|
class FakePipe:
|
|
model = torch.nn.Linear(2, 2)
|
|
|
|
pipe = FakePipe()
|
|
original = pipe.model
|
|
|
|
(out,) = node.patch(pipe, method="hermite", interval=4, warmup_steps=2)
|
|
assert out is not pipe, "node must return a copy, not mutate (ComfyUI caching)"
|
|
assert pipe.model is original, "input pipeline must stay unpatched"
|
|
assert getattr(out.model, "_hicache_is_patch", False)
|
|
assert out.model.inner is original
|
|
|
|
# re-patch with new params replaces (not nests) the patch
|
|
(out2,) = node.patch(out, method="dmd", interval=3, warmup_steps=1)
|
|
assert out2.model.inner is original
|
|
assert out2.model.method == "dmd" and out2.model.interval == 3
|
|
assert out.model.method == "hermite", "earlier output keeps its config"
|
|
|
|
# enable=False restores the original model
|
|
(out3,) = node.patch(out2, enable=False)
|
|
assert out3.model is original
|
|
|
|
# interval=1 means "no caching" -> also unpatched (and an unpatched
|
|
# input passes through unchanged)
|
|
(out4,) = node.patch(pipe, interval=1)
|
|
assert out4 is pipe and out4.model is original
|
|
|
|
|
|
def test_node_rejects_bad_pipeline():
|
|
pack = _load_pack()
|
|
node = pack.NODE_CLASS_MAPPINGS["HiCacheAccelerate"]()
|
|
with pytest.raises(TypeError, match="no `.model` attribute"):
|
|
node.patch(object())
|