wip: release readiness pass

This commit is contained in:
Archerkattri
2026-09-15 12:45:14 -04:00
parent ee513140f3
commit a65aa6aa1a
6 changed files with 292 additions and 16 deletions
+27
View File
@@ -72,6 +72,26 @@ Re-running the node with new parameters re-patches cleanly; `enable=false` (or
`interval=1`) restores the original model. State resets automatically at each
new sampling run.
## First-result acceptance checklist
Use a fixed small input and record the exact ComfyUI, wrapper, checkpoint, node
commit, method, interval, seed, and torch/CUDA versions. Before treating a run
as accelerated, verify all of the following:
* **Baseline:** run once with `enable=false` (or `interval=1`) and save the
output plus the unaccelerated DiT-forward count.
* **Accelerated:** run the same input and seed with the node enabled; confirm
`pipeline.model.branch_id` is `stacked_pre_cfg`, the `run_id` changes on a
retry, and `pipeline.model.telemetry` reports the actual decision and method
counters. DMD/auto history below their method-specific floors is rejected;
short or non-uniform windows remain visible as Hermite fallback counts.
* **Lifecycle:** repeat the run, including a one-step `t=0` retry, and confirm
the run starts with a fresh full decision rather than a stale forecast.
* **Comparison:** compare baseline and accelerated outputs and retain the raw
per-run counts. GPU timing/quality and a clean-host workflow import are
separate gates; this CPU package check does not establish a speed or quality
claim.
The node never mutates the pipeline it receives: it returns a shallow copy
whose `model` attribute is the patch (weights stay shared, so this costs no
VRAM). This matters because ComfyUI caches node outputs keyed on node inputs;
@@ -213,3 +233,10 @@ numbers transfer exactly, and that is what the defaults use.
## License
MIT. Not affiliated with Tencent (Hunyuan3D) or kijai (ComfyUI-Hunyuan3DWrapper).
## Current release status
The current adapter includes the shared HiCache++ budget/identity/manifest
bridge, timing and fallback accounting. Its CPU contract suite passes 45 tests.
ComfyUI execution with the real CUDA model workflow was not run in this
environment, so no image-quality or speedup claim is made.
+163 -13
View File
@@ -72,11 +72,16 @@ import torch
from hicache_pp import (
hicache_init,
hicache_decide,
hicache_telemetry,
hicache_update_derivatives,
hicache_forecast,
dmd_update_snapshots,
dmd_forecast_state,
auto_forecast_state,
CacheBudget,
CacheBudgetRuntime,
RunIdentity,
stable_digest,
)
logger = logging.getLogger("ComfyUI-HiCache")
@@ -101,8 +106,11 @@ def validate_config(method: str, interval: int, warmup_steps: int,
raise ValueError(f"max_order must be >= 1, got {max_order}")
if not (0.0 < sigma < 1.0):
raise ValueError(f"sigma must be in (0, 1), got {sigma}")
if dmd_history < 3:
raise ValueError(f"dmd_history must be >= 3, got {dmd_history}")
min_history = {"hermite": 3, "dmd": 4, "auto": 5}[method]
if dmd_history < min_history:
raise ValueError(
f"dmd_history must be >= {min_history} for {method}, got {dmd_history}"
)
class HiCacheModelPatch(torch.nn.Module):
@@ -116,12 +124,16 @@ class HiCacheModelPatch(torch.nn.Module):
def __init__(self, model: Optional[torch.nn.Module], *, method: str = "hermite",
interval: int = 3, warmup_steps: int = 2, max_order: int = 1,
sigma: float = 0.5, dmd_history: int = 5) -> None:
sigma: float = 0.5, dmd_history: int = 5,
budget: Optional[CacheBudget] = None,
max_horizon: Optional[int] = None,
max_memory_mb: Optional[float] = None,
audit_budget: int = 0) -> None:
validate_config(method, interval, warmup_steps, max_order, sigma, dmd_history)
super().__init__()
# ``model`` may be None: lazy / GGUF pipelines materialize the DiT after
# patching. A real nn.Module is registered as a submodule (so
# .to()/.state_dict() recurse); a None/lazy placeholder is kept as a plain
# .to()/.state_dict() recurse); a None/lazy deferred module is kept as a plain
# attribute so __getattr__ never dead-ends. See _set_inner / bind_inner.
self._set_inner(model)
self._hicache_is_patch = True # marker for apply/remove
@@ -131,19 +143,81 @@ class HiCacheModelPatch(torch.nn.Module):
self.max_order = int(max_order)
self.sigma = float(sigma)
self.dmd_history = int(dmd_history)
if budget is None:
budget = CacheBudget(
backend=method,
allowed_stages=("shape",),
max_horizon=max(1, interval - 1) if max_horizon is None else max_horizon,
quality_preset="adapter-default",
max_memory_mb=max_memory_mb,
audit_budget=audit_budget,
fallback="full",
)
elif not isinstance(budget, CacheBudget):
raise TypeError("budget must be a CacheBudget")
self.budget = budget
self._state: Optional[Dict[str, Any]] = None
self._last_telemetry: Dict[str, Any] = {}
self._last_budget_manifest: Dict[str, Any] = {}
self._budget_runtime: Optional[CacheBudgetRuntime] = None
self._last_budget_decision: Dict[str, Any] = {}
self._last_t: Optional[float] = None
self.last_decision: Optional[str] = None
# per-run stats (read by the node / logged at run boundaries)
self.computed_steps = 0
self.skipped_steps = 0
@property
def run_id(self) -> Optional[str]:
"""Identity of the currently active sampling run, if any."""
return None if self._state is None else self._state.get("run_id")
@property
def branch_id(self) -> Optional[str]:
"""Stable identity for this model-level, pre-CFG patch branch."""
return None if self._state is None else self._state.get("branch_id")
@property
def telemetry(self) -> Dict[str, Any]:
"""Detached actual-method/fallback counters for the active run."""
if self._state is not None:
return hicache_telemetry(self._state)
return copy.deepcopy(self._last_telemetry)
@property
def budget_manifest(self) -> Dict[str, Any]:
"""Portable budget/identity/decision report for the active run."""
if self._budget_runtime is not None:
return copy.deepcopy(self._budget_runtime.manifest.as_dict())
return copy.deepcopy(self._last_budget_manifest)
@property
def budget_decision(self) -> Dict[str, Any]:
"""The last budget decision without exposing mutable runtime state."""
return copy.deepcopy(self._last_budget_decision)
def save_budget_manifest(self, destination: str) -> None:
"""Save the latest portable manifest; no tensors or local paths are written."""
if self._budget_runtime is not None:
self._budget_runtime.manifest.write_json(destination)
elif self._last_budget_manifest:
import json
from pathlib import Path
Path(destination).write_text(
json.dumps(self._last_budget_manifest, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
else:
raise RuntimeError("no budget manifest exists; run the patched model first")
# -- inner storage / attribute passthrough ---------------------------------
def _set_inner(self, model: Optional[torch.nn.Module]) -> None:
"""Store the wrapped model so ``self.inner`` is always resolvable.
A real ``nn.Module`` is registered in ``_modules`` (so device moves and
``state_dict`` recurse into it). A ``None`` placeholder -- or any
``state_dict`` recurse into it). A ``None`` deferred value -- or any
non-Module -- is kept as a plain attribute in ``__dict__``. This avoids the
bug where ``nn.Module.__setattr__`` routes only real Modules into
``_modules``; a None assigned to ``self.inner`` lands in ``__dict__``, and
@@ -166,7 +240,7 @@ class HiCacheModelPatch(torch.nn.Module):
return self.__dict__.get("inner")
def bind_inner(self, model: torch.nn.Module) -> "HiCacheModelPatch":
"""Attach the real DiT to a patch created around a lazy (None) placeholder,
"""Attach the real DiT to a patch created around a deferred (None) binding,
registering it as a submodule. Lets lazy / GGUF pipelines that materialize
the model after patching still route through the cache."""
self._set_inner(model)
@@ -190,7 +264,7 @@ class HiCacheModelPatch(torch.nn.Module):
# -- state handling ----------------------------------------------------------
def _fresh_state(self) -> Dict[str, Any]:
# hicache_pp validates interval/order/sigma/backend again here.
return hicache_init(
state = hicache_init(
num_steps=_NO_END_WINDOW,
interval=self.interval,
max_order=self.max_order,
@@ -202,17 +276,25 @@ class HiCacheModelPatch(torch.nn.Module):
backend=self.method,
history=self.dmd_history,
)
state["branch_id"] = "stacked_pre_cfg"
return state
def reset(self) -> None:
"""Drop all cached anchors and per-run stats (new sampling run)."""
if self._state is not None and (self.computed_steps or self.skipped_steps):
self._last_telemetry = hicache_telemetry(self._state)
logger.info(
"[HiCache] run finished: %d computed + %d skipped DiT steps "
"(method=%s, interval=%d)",
self.computed_steps, self.skipped_steps, self.method, self.interval,
)
if self._budget_runtime is not None:
self._last_budget_manifest = self._budget_runtime.manifest.as_dict()
self._state = self._fresh_state()
self._budget_runtime = None
self._last_budget_decision = {}
self._last_t = None
self.last_decision = None
self.computed_steps = 0
self.skipped_steps = 0
@@ -222,13 +304,54 @@ class HiCacheModelPatch(torch.nn.Module):
return float(timestep.reshape(-1)[0].item())
return float(timestep)
def _forecast(self, state: Dict[str, Any]) -> torch.Tensor:
if self.method == "dmd":
def _forecast(self, state: Dict[str, Any], method: Optional[str] = None) -> torch.Tensor:
method = self.method if method is None else method
if method == "dmd":
return dmd_forecast_state(state)
if self.method == "auto":
if method == "auto":
return auto_forecast_state(state)
return hicache_forecast(state)
def _ensure_budget_runtime(
self, state: Dict[str, Any], latent_model_input: torch.Tensor
) -> CacheBudgetRuntime:
inner = self._inner()
shape = tuple(int(value) for value in latent_model_input.shape)
dtype = str(latent_model_input.dtype)
device = str(latent_model_input.device)
identity = RunIdentity(
model_id=type(inner).__name__ if inner is not None else "unloaded-model",
run_id=str(state["run_id"]),
schedule_digest=stable_digest({
"interval": self.interval,
"warmup_steps": self.warmup_steps,
"max_order": self.max_order,
"sigma": self.sigma,
"dmd_history": self.dmd_history,
}),
cfg_branch="stacked_pre_cfg",
conditioning_id="pre-cfg",
stage="shape",
token_layout_digest=stable_digest({"shape": shape}),
dtype=dtype,
device=device,
batch_id=stable_digest({"shape": shape, "dtype": dtype, "device": device}),
)
if self._budget_runtime is None or self._budget_runtime.identity.fingerprint != identity.fingerprint:
self._budget_runtime = CacheBudgetRuntime(
self.budget,
identity,
source_digest=stable_digest({"adapter": "ComfyUI-HiCache", "contract": "budget-v1"}),
config_digest=self.budget.digest,
)
return self._budget_runtime
@staticmethod
def _current_memory_mb(tensor: torch.Tensor) -> Optional[float]:
if not tensor.is_cuda:
return None
return float(torch.cuda.memory_allocated(tensor.device)) / (1024.0 * 1024.0)
# -- the patched forward ------------------------------------------------------
def forward(self, latent_model_input: torch.Tensor, timestep: Any,
*args: Any, **kwargs: Any) -> torch.Tensor:
@@ -243,8 +366,30 @@ class HiCacheModelPatch(torch.nn.Module):
self._last_t = t_val
state = self._state
if hicache_decide(state) == "forecast":
out = self._forecast(state)
decision = hicache_decide(state)
self.last_decision = decision
budget_runtime = self._ensure_budget_runtime(state, latent_model_input)
budget_decision = budget_runtime.decide(
"shape",
horizon=int(state.get("counter", 0)) if decision == "forecast" else 0,
method=self.method,
supported=self._inner() is not None,
memory_mb=self._current_memory_mb(latent_model_input),
controller_selected=self.method == "auto",
)
self._last_budget_decision = budget_decision.as_dict()
if budget_decision.mode == "fallback" and budget_decision.method == "full" and decision == "forecast":
# The legacy scheduler has already incremented its forecast counter;
# repair it so the measured state and budget manifest agree.
state["type"] = "full"
state["counter"] = 0
state["activated_steps"].append(state["step"])
telemetry = state["telemetry"]["decisions"]
telemetry["forecast"] = max(0, int(telemetry.get("forecast", 0)) - 1)
telemetry["full"] = int(telemetry.get("full", 0)) + 1
decision = "full"
if decision == "forecast" and budget_decision.mode in ("forecast", "fallback"):
out = self._forecast(state, budget_decision.method)
state["step"] += 1
self.skipped_steps += 1
return out
@@ -271,7 +416,10 @@ class HiCacheModelPatch(torch.nn.Module):
# ---------------------------------------------------------------------------
def apply_hicache(pipeline: Any, *, method: str = "hermite", interval: int = 3,
warmup_steps: int = 2, max_order: int = 1, sigma: float = 0.5,
dmd_history: int = 5) -> Any:
dmd_history: int = 5, budget: Optional[CacheBudget] = None,
max_horizon: Optional[int] = None,
max_memory_mb: Optional[float] = None,
audit_budget: int = 0) -> Any:
"""Return a shallow copy of ``pipeline`` whose ``model`` is patched.
The input pipeline is NOT mutated (ComfyUI caches node outputs keyed on
@@ -293,6 +441,8 @@ def apply_hicache(pipeline: Any, *, method: str = "hermite", interval: int = 3,
patched.model = HiCacheModelPatch(
inner, method=method, interval=interval, warmup_steps=warmup_steps,
max_order=max_order, sigma=sigma, dmd_history=dmd_history,
budget=budget, max_horizon=max_horizon, max_memory_mb=max_memory_mb,
audit_budget=audit_budget,
)
logger.info(
"[HiCache] patched copy of %s: method=%s interval=%d warmup=%d",
+22 -2
View File
@@ -66,7 +66,23 @@ class HiCacheAccelerate:
"high-order terms bounded."}),
"dmd_history": ("INT", {
"default": 5, "min": 3, "max": 16, "step": 1,
"tooltip": "DMD snapshot window length (dmd/auto methods)."}),
"tooltip": "Snapshot window length. Minimum effective history: "
"3 for hermite, 4 for dmd, 5 for auto; shorter "
"values are rejected for the selected method."}),
"max_horizon": ("INT", {
"default": 0, "min": 0, "max": 64, "step": 1,
"tooltip": "Maximum forecast distance from a paid anchor. "
"0 follows the selected interval; exceeding the "
"budget falls back to full compute."}),
"max_memory_mb": ("FLOAT", {
"default": 0.0, "min": 0.0, "max": 262144.0, "step": 1.0,
"tooltip": "Optional current-device memory cap for forecast "
"decisions. 0 disables the cap."}),
"audit_budget": ("INT", {
"default": 0, "min": 0, "max": 128, "step": 1,
"tooltip": "Maximum explicit paid forward-audit decisions "
"available to an integration; reports are exposed "
"on pipeline.model.budget_manifest."}),
},
}
@@ -81,7 +97,8 @@ class HiCacheAccelerate:
)
def patch(self, pipeline, method="hermite", interval=3, warmup_steps=2,
enable=True, max_order=1, sigma=0.5, dmd_history=5):
enable=True, max_order=1, sigma=0.5, dmd_history=5,
max_horizon=0, max_memory_mb=0.0, audit_budget=0):
if not enable or interval <= 1:
return (remove_hicache(pipeline),)
return (apply_hicache(
@@ -92,6 +109,9 @@ class HiCacheAccelerate:
max_order=max_order,
sigma=sigma,
dmd_history=dmd_history,
max_horizon=None if max_horizon <= 0 else max_horizon,
max_memory_mb=None if max_memory_mb <= 0 else max_memory_mb,
audit_budget=audit_budget,
),)
+1 -1
View File
@@ -14,7 +14,7 @@ keywords = ["comfyui", "diffusion-acceleration", "caching", "training-free", "hu
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.9"
dependencies = ["hicache-pp>=1.1.0"]
dependencies = ["hicache-pp>=1.2.1"]
classifiers = ["Development Status :: 5 - Production/Stable"]
[project.optional-dependencies]
+45
View File
@@ -0,0 +1,45 @@
"""Integration checks for the shared hicache-pp budget contract."""
import importlib.util
import pathlib
import torch
PACK_DIR = pathlib.Path(__file__).resolve().parents[1]
def _load_patch():
spec = importlib.util.spec_from_file_location("hicache_budget_bridge_patch", PACK_DIR / "hicache_patch.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class _DiT(torch.nn.Module):
def __init__(self):
super().__init__()
self.register_buffer("base", torch.ones(1, 2))
def forward(self, x, timestep, cond, guidance=None):
return self.base * (1.0 + float(timestep.reshape(-1)[0]))
class _Pipe:
def __init__(self):
self.model = _DiT()
def test_budget_cap_forces_full_fallback_and_manifest_counts_actual_decisions():
patch = _load_patch()
pipeline = patch.apply_hicache(_Pipe(), method="hermite", interval=4, max_horizon=1)
for timestep in torch.linspace(0.0, 1.0, 10):
pipeline.model(torch.ones(1, 2), timestep, None)
report = pipeline.model.budget_manifest
assert report["budget"]["max_horizon"] == 1
assert report["identity"]["stage"] == "shape"
assert report["counts"]["fallback"] > 0
assert report["counts"]["full"] + report["counts"]["forecast"] + report["counts"]["fallback"] == 10
assert all("path" not in event for event in report["events"])
+34
View File
@@ -287,8 +287,13 @@ def test_dmd_method_populates_snapshots_and_forecasts():
pipe(num_inference_steps=40)
st = pipe.model._state
assert st["backend"] == "dmd"
assert pipe.model.branch_id == "stacked_pre_cfg"
assert len(st["dmd_snapshots"]) >= 4, "DMD snapshot window never filled"
assert pipe.model.skipped_steps > 0
assert pipe.model.telemetry["decisions"]["forecast"] == pipe.model.skipped_steps
assert pipe.model.telemetry["method_counts"]["hermite"] > 0
assert sum(pipe.model.telemetry["fallbacks"].values()) > 0
assert pipe.model.telemetry["method_counts"]["dmd"] > 0
def test_hermite_method_keeps_snapshots_empty():
@@ -448,6 +453,28 @@ def test_single_step_runs_reset_at_equal_timestep():
assert patch.computed_steps == 1 and patch.skipped_steps == 0 # per-run stats
def test_run_identity_and_detached_telemetry_change_on_equal_timestep_reset():
dit = MockDiT()
patch = hp.HiCacheModelPatch(dit, method="dmd", interval=4,
warmup_steps=1, dmd_history=4)
lmi = torch.randn(2, 8, 4)
t0 = torch.zeros(2)
patch(lmi, t0, cond=None)
first_run_id = patch.run_id
assert first_run_id
assert patch.branch_id == "stacked_pre_cfg"
observed = patch.telemetry
assert observed["decisions"]["full"] == 1
observed["decisions"]["full"] = 99
assert patch.telemetry["decisions"]["full"] == 1
patch(lmi, t0, cond=None)
assert patch.run_id and patch.run_id != first_run_id
assert patch.telemetry["decisions"]["full"] == 1
assert patch.last_decision == "full"
# ---------------------------------------------------------------------------
# parameter validation
# ---------------------------------------------------------------------------
@@ -468,6 +495,13 @@ def test_bad_params_rejected(kwargs, match):
hp.validate_config(**base)
@pytest.mark.parametrize("method,history", [("dmd", 3), ("auto", 4)])
def test_history_floor_matches_stateful_forecaster(method, history):
with pytest.raises(ValueError, match="dmd_history"):
hp.validate_config(method=method, interval=4, warmup_steps=2,
max_order=1, sigma=0.5, dmd_history=history)
def test_apply_requires_model_attribute():
with pytest.raises(TypeError, match="no `.model` attribute"):
hp.apply_hicache(object(), method="dmd")