Files
Ewald a4154ccf05 Fix P0 bugs, remove Append V2/Lens Artifacts, and fix crop reload reset
- Fix batch bypass tuple handling, memory budget device-awareness, grain
  RNG device, roto payload limits, preview temp-file cleanup, and route
  double-registration.
- Add native VIDEO output (fps/audio) to effect nodes that previously
  only returned IMAGE/MASK when fed a VIDEO input.
- Split the oversized src/preview/ops/geometry.ts and add a starting
  golden-fixture test harness (spherize, transform) comparing backend
  and frontend math.
- Remove ImageOpsAppendV2 (Autogrow) and ImageOpsLensArtifacts; inline
  Append's helper back into its single caller.
- Collapse the Color Correct curve widget behind a toggle so the node
  no longer grows too tall.
- Fix Crop/generic-node video preview playback running at the RAF rate
  instead of the source video's real fps.
- Fix Crop losing its saved output size after a workflow reload:
  syncCropWidgets recomputed height from width and a preset aspect
  ratio on every resync (including onConfigure on load), discarding
  the restored value. It now only refreshes aspect-ratio bookkeeping
  on a resync.
2026-09-30 12:49:42 +02:00

168 lines
6.6 KiB
Python

"""Tests for core/memory.py — memory budget contract."""
from __future__ import annotations
import pytest
import torch
import types
import sys
from nodes.core.memory import MemoryBudgetError, check_budget, estimate_bytes
class TestEstimateBytes:
def test_basic_float32(self):
# 1 frame, 64x64, 3 channels, float32 (4 bytes), multiplier 1
est = estimate_bytes(1, 64, 64, 3, torch.float32, 1.0)
assert est == 1 * 64 * 64 * 3 * 4
def test_multiplier(self):
base = estimate_bytes(1, 64, 64, 3, torch.float32, 1.0)
doubled = estimate_bytes(1, 64, 64, 3, torch.float32, 2.0)
assert doubled == base * 2
def test_fp16(self):
est = estimate_bytes(1, 64, 64, 3, torch.float16, 1.0)
assert est == 1 * 64 * 64 * 3 * 2
def test_batch_scaling(self):
one = estimate_bytes(1, 64, 64, 3, torch.float32, 1.0)
four = estimate_bytes(4, 64, 64, 3, torch.float32, 1.0)
assert four == one * 4
class TestCheckBudget:
def test_within_budget(self):
# 1 frame, 64x64, 3ch, float32 — tiny allocation
result = check_budget(1, 64, 64, 3, budget_mb=100.0, label="test")
assert result > 0
def test_exceeds_budget_raises(self):
# 1000 frames, 8192x8192, 4ch, float32 — absurd allocation
with pytest.raises(MemoryBudgetError) as exc_info:
check_budget(
1000, 8192, 8192, 4,
budget_mb=1.0,
label="ImageOps Constant",
)
assert "ImageOps Constant" in str(exc_info.value)
assert "budget" in str(exc_info.value).lower()
def test_error_attributes(self):
with pytest.raises(MemoryBudgetError) as exc_info:
check_budget(1000, 4096, 4096, 4, budget_mb=1.0, label="test")
err = exc_info.value
assert err.estimated_mb > 1.0
assert err.budget_mb == 1.0
assert err.label == "test"
def test_custom_multiplier(self):
# Should pass with multiplier 1 but fail with multiplier 100
check_budget(100, 1024, 1024, 4, multiplier=1.0, budget_mb=2048.0, label="test")
with pytest.raises(MemoryBudgetError):
check_budget(100, 1024, 1024, 4, multiplier=100.0, budget_mb=2048.0, label="test")
def test_cuda_probe_failure_falls_back_to_static_budget(self, monkeypatch):
mm = types.ModuleType("comfy.model_management")
def _raise_assertion():
raise AssertionError("Torch not compiled with CUDA enabled")
mm.get_torch_device = _raise_assertion
mm.get_free_memory = lambda _device: 0
comfy = types.ModuleType("comfy")
comfy.model_management = mm
monkeypatch.setitem(sys.modules, "comfy", comfy)
monkeypatch.setitem(sys.modules, "comfy.model_management", mm)
result = check_budget(1, 64, 64, 3, budget_mb=100.0, label="test")
assert result > 0
def test_device_param_checks_free_memory_for_that_device_not_compute_device(self, monkeypatch):
# Regression: check_budget used to always ask for the compute device's
# (GPU) free memory even when the allocation was actually going to land on
# CPU, so a CPU-bound run could be rejected/accepted based on GPU headroom.
mm = types.ModuleType("comfy.model_management")
seen_devices = []
def _get_free_memory(device):
seen_devices.append(device)
return 100 if device == torch.device("cpu") else 10 ** 12
mm.get_torch_device = lambda: torch.device("cuda")
mm.get_free_memory = _get_free_memory
comfy = types.ModuleType("comfy")
comfy.model_management = mm
monkeypatch.setitem(sys.modules, "comfy", comfy)
monkeypatch.setitem(sys.modules, "comfy.model_management", mm)
# 1 frame, 64x64, 3ch, float32 fits easily under the GPU's fake 10^12 bytes free...
check_budget(1, 64, 64, 3, label="test", device=torch.device("cuda"))
# ...but the CPU device is deliberately starved to ~100 bytes free, so passing
# it explicitly must make the same allocation fail against CPU headroom instead.
with pytest.raises(MemoryBudgetError):
check_budget(1, 64, 64, 3, label="test", device=torch.device("cpu"))
assert torch.device("cpu") in seen_devices
assert torch.device("cuda") in seen_devices
def test_device_param_omitted_preserves_default_behavior(self, monkeypatch):
mm = types.ModuleType("comfy.model_management")
seen_devices = []
def _get_free_memory(device):
seen_devices.append(device)
return 10 ** 12
mm.get_torch_device = lambda: torch.device("cuda")
mm.get_free_memory = _get_free_memory
comfy = types.ModuleType("comfy")
comfy.model_management = mm
monkeypatch.setitem(sys.modules, "comfy", comfy)
monkeypatch.setitem(sys.modules, "comfy.model_management", mm)
check_budget(1, 64, 64, 3, label="test")
assert seen_devices == [torch.device("cuda")]
class TestComputeDevice:
@pytest.fixture
def fake_mm(self, monkeypatch):
mm = types.ModuleType("comfy.model_management")
mm.free = 10 ** 12
mm.get_torch_device = lambda: torch.device("meta")
mm.get_free_memory = lambda _device: mm.free
mm.intermediate_device = lambda: torch.device("cpu")
comfy = types.ModuleType("comfy")
comfy.model_management = mm
monkeypatch.setitem(sys.modules, "comfy", comfy)
monkeypatch.setitem(sys.modules, "comfy.model_management", mm)
return mm
def test_moves_when_working_set_fits(self, fake_mm):
from nodes.core.memory import to_compute_device
assert to_compute_device(torch.zeros(2, 8, 8, 3), 4.0).device.type == "meta"
def test_stays_when_working_set_does_not_fit(self, fake_mm):
from nodes.core.memory import to_compute_device
fake_mm.free = 2 * 8 * 8 * 3 * 4 * 3
assert to_compute_device(torch.zeros(2, 8, 8, 3), 4.0).device.type == "cpu"
def test_env_override_keeps_input_device(self, fake_mm, monkeypatch):
from nodes.core import memory
monkeypatch.setattr(memory, "COMPUTE_DEVICE", "cpu")
assert memory.to_compute_device(torch.zeros(1, 4, 4, 3), 4.0).device.type == "cpu"
def test_no_comfy_keeps_input_device(self, monkeypatch):
from nodes.core.memory import to_compute_device, to_intermediate_device
monkeypatch.setitem(sys.modules, "comfy", None)
monkeypatch.setitem(sys.modules, "comfy.model_management", None)
tensor = torch.zeros(1, 4, 4, 3)
assert to_compute_device(tensor, 4.0) is tensor
assert to_intermediate_device(tensor) is tensor