The five review findings of 2026-10-23 for phase P5: 1. The P5.5 fallback branch (a checkpoint without flownet.* keys) died with a bare RuntimeError at the strict load: the arch always owns the flownet (keep_arch.py:882), so the state dict of the net always carries the flownet.* keys - and the GMFlow weights the arch had just loaded were discarded. The flownet tensors of the built net (the trained ones - the arch was built with flownet_path) are now merged into the checkpoint state dict before load_state_dict(..., strict=True). The load stays strict (an unexpected key still raises) and the trained flow weights survive. The stub arches of the tests behave like nn.Module now - a no-op recorder had hidden the defect. 2. P5.4's acceptance is no longer unmet: the GPU acceptance run was executed (real weights, real ComfyUI modules, RTX 4070 Ti SUPER) and is green (5 passed, exit code 0; the run log is in the audit doc section 5). KEEP_MODEL_DIR now really decides where the weights come from - it is the models root of the run (folder_paths.models_dir), the harness fixture no longer isolates the gpu_acceptance tier, and the run needs KEEP_NO_COMFY_MOCKS=1 so the real ComfyUI modules are in charge. 3. The "the flow weights are not random" assertion of the acceptance test could not distinguish random weights from trained ones. It now compares the flownet tensors with the tensors of their source (the KEEP checkpoint or the GMFlow file) bit for bit. 4. The superseded download helper load_file_from_url_comfy is gone (src/keep_paths.py only resolves paths; the download lives in src/keep_download.py since P5.2). The stale docstrings, the stale comment in the vendored facelib and the two tests that called the dead helper are fixed. 5. The risk list mentioned a pending deletion of assets/keep-preview.png that does not exist - the asset is committed and present (93279 bytes). The rule in the implementation workflow now states the truth. Checks: the gate (pytest -q -m "unit or mock_integration") is green (234 passed, 1 skipped, 1 xfailed); the opt-in acceptance run is green (5 passed); the acceptance tier still skips cleanly without its environment variables.
362 lines
14 KiB
Python
362 lines
14 KiB
Python
"""P5.4 - the opt-in GPU acceptance test (real weights, real GPU).
|
|
|
|
The test is **opt-in**: it needs ``RUN_KEEP_E2E=1`` **and**
|
|
``KEEP_MODEL_DIR=<absolute path>`` (the **models root** with the real
|
|
weights - ``keep_models/KEEP/``, ``facedetection/``, ``gmflow/``) and
|
|
``COMFYUI_ROOT=<path to the ComfyUI checkout>`` (the real ``comfy`` modules -
|
|
the headless bootstrap of P1.4 drives the pack **without a server**) and
|
|
``KEEP_NO_COMFY_MOCKS=1`` (the harness must not replace the real ComfyUI
|
|
modules with its mocks - an acceptance run against the mocks would prove
|
|
nothing).
|
|
|
|
Without those variables the test **skips cleanly**, naming them - the
|
|
acceptance gate runs without environment variables and must never pull a
|
|
gigabyte from upstream.
|
|
|
|
What it proves (one factor varied per case, a fixed seed):
|
|
|
|
* the single image node restores a synthetic face-like frame - the output has
|
|
the expected shape and dtype, carries no NaN and differs from the input by a
|
|
measurable margin (the regression class of the random-weight flownet);
|
|
* the sequence node restores five synthetic frames - the same assertions on
|
|
the frames;
|
|
* the VRAM returns to its baseline after the release (``unload`` +
|
|
``soft_empty_cache``);
|
|
* the optical flow net carries the **trained** weights: the tensors of the
|
|
flownet are compared with the tensors of their source (the KEEP checkpoint
|
|
or the GMFlow file) - random weights cannot pass that comparison.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
#: The environment variable that arms the test.
|
|
RUN_E2E_ENV = "RUN_KEEP_E2E"
|
|
|
|
#: The environment variable with the directory of the real weights.
|
|
MODEL_DIR_ENV = "KEEP_MODEL_DIR"
|
|
|
|
#: The environment variable with the ComfyUI checkout.
|
|
COMFYUI_ROOT_ENV = "COMFYUI_ROOT"
|
|
|
|
#: The environment variable that keeps the harness mocks out of the way.
|
|
NO_MOCKS_ENV = "KEEP_NO_COMFY_MOCKS"
|
|
|
|
#: The values of :data:`RUN_E2E_ENV` that count as "on".
|
|
TRUTHY = frozenset({"1", "true", "yes", "on"})
|
|
|
|
#: The seed of the synthetic frames (the run must be reproducible).
|
|
SEED = 20261023
|
|
|
|
#: The size of the synthetic (aligned) face frames.
|
|
FRAME_SIZE = 512
|
|
|
|
pytestmark = pytest.mark.gpu_acceptance
|
|
|
|
|
|
def _load_helper():
|
|
"""The headless bootstrap helper (loaded by path, like P1.4 does)."""
|
|
path = Path(__file__).resolve().parent / "_headless.py"
|
|
spec = importlib.util.spec_from_file_location("keep_headless_helper", path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _missing_environment() -> list[str]:
|
|
"""The environment variables that are missing (empty when it may run)."""
|
|
missing: list[str] = []
|
|
if (os.environ.get(RUN_E2E_ENV) or "").strip().lower() not in TRUTHY:
|
|
missing.append(f"{RUN_E2E_ENV}=1")
|
|
if not (os.environ.get(MODEL_DIR_ENV) or "").strip():
|
|
missing.append(f"{MODEL_DIR_ENV}=<absolute path to the KEEP weights>")
|
|
if not (os.environ.get(COMFYUI_ROOT_ENV) or "").strip():
|
|
missing.append(f"{COMFYUI_ROOT_ENV}=<path to the ComfyUI checkout>")
|
|
if (os.environ.get(NO_MOCKS_ENV) or "").strip().lower() not in TRUTHY:
|
|
missing.append(
|
|
f"{NO_MOCKS_ENV}=1 (the acceptance run needs the **real** ComfyUI "
|
|
"modules, not the harness mocks)"
|
|
)
|
|
return missing
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def e2e():
|
|
"""The headless environment of the acceptance run (or a clean skip)."""
|
|
missing = _missing_environment()
|
|
if missing:
|
|
pytest.skip(
|
|
"the GPU acceptance test is opt-in - set " + " and ".join(missing)
|
|
)
|
|
|
|
helper = _load_helper()
|
|
root = helper.resolve_comfyui_root()
|
|
# the real ComfyUI modules have to be importable before the package is
|
|
# executed (its entry point imports ``comfy_api.latest``)
|
|
if str(root) not in sys.path:
|
|
sys.path.insert(0, str(root))
|
|
helper.load_extra_model_paths(str(root))
|
|
package_dir = Path(__file__).resolve().parent.parent
|
|
alias = helper.register_package(package_dir, execute=True)
|
|
helper.stub_ui()
|
|
|
|
# P5.4 (review fix): ``KEEP_MODEL_DIR`` must really decide where the
|
|
# weights come from. The pack resolves every weight through ComfyUI's
|
|
# ``folder_paths`` (the primary root is ``folder_paths.models_dir``), so
|
|
# the models root of the run **is** the models root of the pack.
|
|
import folder_paths # noqa: PLC0415 - the real ComfyUI module
|
|
|
|
model_dir = helper.resolve_model_dir()
|
|
folder_paths.models_dir = str(model_dir)
|
|
paths_module = importlib.import_module(
|
|
f"{alias.__name__}.src.keep_paths"
|
|
)
|
|
paths_module.register_model_folders()
|
|
|
|
return {
|
|
"root": root,
|
|
"model_dir": model_dir,
|
|
"package": alias,
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def keep_pack(e2e):
|
|
"""The real model pack (real weights, real GPU)."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
loader_module = importlib.import_module(
|
|
f"{e2e['package'].__name__}.src.keep_model_loader"
|
|
)
|
|
loader = loader_module.KEEPModelLoader()
|
|
pack = loader.load_keep_model_pack("KEEP", "retinaface_resnet50")
|
|
return pack
|
|
|
|
|
|
def _face_like_frame(index: int = 0):
|
|
"""A deterministic synthetic face-like frame ``(1, 512, 512, 3)``.
|
|
|
|
The frames are *aligned* faces (the ``has_aligned_face`` path), so the
|
|
test does not depend on a detector finding a face in a synthetic image.
|
|
"""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
generator = torch.Generator().manual_seed(SEED + index)
|
|
size = FRAME_SIZE
|
|
frame = torch.rand((1, size, size, 3), generator=generator, dtype=torch.float32)
|
|
# a simple face-like structure: an oval, two eyes and a mouth
|
|
yy, xx = torch.meshgrid(
|
|
torch.arange(size, dtype=torch.float32),
|
|
torch.arange(size, dtype=torch.float32),
|
|
indexing="ij",
|
|
)
|
|
centre_x, centre_y = size / 2, size / 2
|
|
oval = (((xx - centre_x) / (size * 0.30)) ** 2 + ((yy - centre_y) / (size * 0.40)) ** 2) < 1.0
|
|
for eye_x in (centre_x - size * 0.11, centre_x + size * 0.11):
|
|
eye = ((xx - eye_x) ** 2 + (yy - (centre_y - size * 0.08)) ** 2) < (size * 0.035) ** 2
|
|
frame[0, ..., 0][eye] = 0.05
|
|
mouth = (((xx - centre_x) / (size * 0.10)) ** 2 + ((yy - (centre_y + size * 0.18)) / (size * 0.02)) ** 2) < 1.0
|
|
frame[0, ..., 2][mouth] = 0.05
|
|
frame[0, ..., 1][oval] = frame[0, ..., 1][oval] * 0.9 + 0.1
|
|
return frame
|
|
|
|
|
|
def _assert_sane(output, expected_shape):
|
|
"""The shape / dtype / NaN assertions of the restored frames."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
assert tuple(output.shape) == tuple(expected_shape), output.shape
|
|
assert output.dtype == torch.float32, output.dtype
|
|
assert not torch.isnan(output).any(), "the output carries NaN"
|
|
assert float(output.min()) >= -1e-3 and float(output.max()) <= 1.0 + 1e-3
|
|
|
|
|
|
def _margin(input_frame, output_frame) -> float:
|
|
"""The mean absolute difference of the two frames."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
return float((input_frame - output_frame).abs().mean())
|
|
|
|
|
|
@pytest.mark.gpu_acceptance
|
|
def test_the_weights_come_from_the_model_dir(e2e, keep_pack):
|
|
"""``KEEP_MODEL_DIR`` really decides where the weights come from.
|
|
|
|
The pack resolves every weight through ComfyUI's ``folder_paths`` (the
|
|
primary root is ``folder_paths.models_dir``), and the models root of the
|
|
run **is** ``KEEP_MODEL_DIR``. A weight from anywhere else (the ComfyUI
|
|
checkout, a stale extra model root) would mean that the environment
|
|
variable has no effect - the defect the P5.4 review found.
|
|
"""
|
|
configs = importlib.import_module(
|
|
f"{e2e['package'].__name__}.src.keep_model_configs"
|
|
)
|
|
download = importlib.import_module(
|
|
f"{e2e['package'].__name__}.src.keep_download"
|
|
)
|
|
entry = configs.weight_entry_for("KEEP")
|
|
path = download.find_weight(entry)
|
|
assert path is not None, "the KEEP checkpoint is not in the model roots"
|
|
|
|
model_dir = Path(e2e["model_dir"]).resolve()
|
|
assert Path(path).resolve().is_relative_to(model_dir), (path, model_dir)
|
|
|
|
|
|
@pytest.mark.gpu_acceptance
|
|
def test_the_single_image_node_restores_a_face(e2e, keep_pack):
|
|
"""The single image path restores the frame measurably."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
node_module = importlib.import_module(
|
|
f"{e2e['package'].__name__}.nodes.keep_face_upscale_image"
|
|
)
|
|
frame = _face_like_frame()
|
|
try:
|
|
output = node_module.KEEP_FaceUpscaleImageNode.execute(
|
|
image=frame,
|
|
keep_model=keep_pack,
|
|
final_upscale_factor=1.0,
|
|
has_aligned_face=True,
|
|
only_center_face=True,
|
|
draw_bounding_box=False,
|
|
).result[0]
|
|
finally:
|
|
keep_pack.offload()
|
|
|
|
_assert_sane(output, frame.shape)
|
|
# the restoration must change the frame (the random-weight regression
|
|
# class would leave it untouched)
|
|
assert _margin(frame, output) > 1e-3, "the restored frame is identical"
|
|
|
|
|
|
@pytest.mark.gpu_acceptance
|
|
def test_the_sequence_node_restores_five_frames(e2e, keep_pack):
|
|
"""The sequence path restores five frames measurably."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
node_module = importlib.import_module(
|
|
f"{e2e['package'].__name__}.nodes.keep_process_image_sequence"
|
|
)
|
|
frames = torch.cat([_face_like_frame(index) for index in range(5)], dim=0)
|
|
try:
|
|
output = node_module.KEEP_ProcessImageSequenceNode.execute(
|
|
images=frames,
|
|
keep_model=keep_pack,
|
|
final_upscale_factor=1.0,
|
|
has_aligned_frames=True,
|
|
only_center_face=True,
|
|
draw_bounding_box=False,
|
|
max_clip_length=20,
|
|
).result[0]
|
|
finally:
|
|
keep_pack.offload()
|
|
|
|
_assert_sane(output, frames.shape)
|
|
margins = [_margin(frames[i], output[i]) for i in range(frames.shape[0])]
|
|
assert min(margins) > 1e-3, margins
|
|
|
|
|
|
@pytest.mark.gpu_acceptance
|
|
def test_the_vram_returns_to_the_baseline(e2e, keep_pack):
|
|
"""The release gives the VRAM back (unload + ``soft_empty_cache``)."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
from comfy import model_management # noqa: PLC0415 - the real ComfyUI
|
|
|
|
if not torch.cuda.is_available():
|
|
pytest.skip("no CUDA device - the VRAM assertion needs a GPU")
|
|
|
|
torch.cuda.empty_cache()
|
|
baseline = torch.cuda.memory_allocated()
|
|
keep_pack.load_device()
|
|
peak = torch.cuda.memory_allocated()
|
|
keep_pack.offload()
|
|
model_management.soft_empty_cache()
|
|
after = torch.cuda.memory_allocated()
|
|
|
|
assert peak > baseline, "the models did not move to the GPU"
|
|
assert after <= baseline + 1024 * 1024, (baseline, after)
|
|
|
|
|
|
@pytest.mark.gpu_acceptance
|
|
def test_the_flow_weights_load_strictly(e2e, keep_pack):
|
|
"""The optical flow net carries the **trained** weights (P5.5).
|
|
|
|
The flow weights come from the KEEP checkpoint (the shipped one carries
|
|
them) or from the GMFlow file. The guarantee is the **strict** load of
|
|
the whole checkpoint (the loader raises otherwise) - and this test proves
|
|
the second half of it: the tensors of the flownet are the tensors of
|
|
their source, bit for bit. Randomly initialised weights (the
|
|
regression class) cannot pass that comparison - a "the weights are not
|
|
all zero" assertion would, because a random net is not all zero either.
|
|
"""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
flownet = keep_pack.flownet
|
|
assert flownet is not None, "the KEEP net has no flownet"
|
|
|
|
# the tensors of the flownet, with the prefix they have in the net
|
|
actual = {
|
|
"flownet." + key: tensor for key, tensor in flownet.state_dict().items()
|
|
}
|
|
assert actual, "the flownet has no weights"
|
|
|
|
if keep_pack.flownet_from_checkpoint:
|
|
source = _checkpoint_flownet_tensors(e2e)
|
|
else:
|
|
path = keep_pack.flownet_path
|
|
assert path, (
|
|
"neither the checkpoint nor the GMFlow file provided the flow weights"
|
|
)
|
|
source = _gmflow_tensors(path)
|
|
|
|
assert set(source) == set(actual), sorted(set(source) ^ set(actual))[:10]
|
|
for key, tensor in source.items():
|
|
assert torch.equal(actual[key].cpu(), tensor.cpu()), key
|
|
|
|
|
|
def _checkpoint_flownet_tensors(e2e) -> dict:
|
|
"""The ``flownet.*`` tensors of the KEEP checkpoint (the source of truth).
|
|
|
|
The checkpoint is resolved through the **production** path (the manifest
|
|
and the downloader), so the test compares against the file the pack has
|
|
really loaded.
|
|
"""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
configs = importlib.import_module(
|
|
f"{e2e['package'].__name__}.src.keep_model_configs"
|
|
)
|
|
download = importlib.import_module(
|
|
f"{e2e['package'].__name__}.src.keep_download"
|
|
)
|
|
entry = configs.weight_entry_for("KEEP")
|
|
path = download.find_weight(entry)
|
|
assert path, "the KEEP checkpoint is not in the model roots"
|
|
|
|
try: # the memory-mapped load keeps the peak small
|
|
checkpoint = torch.load(
|
|
path, map_location="cpu", weights_only=True, mmap=True
|
|
)
|
|
except (RuntimeError, ValueError): # pragma: no cover - old save format
|
|
checkpoint = torch.load(path, map_location="cpu", weights_only=True)
|
|
state = checkpoint.get("params_ema", checkpoint.get("params", checkpoint))
|
|
return {
|
|
key: value for key, value in state.items() if key.startswith("flownet.")
|
|
}
|
|
|
|
|
|
def _gmflow_tensors(path: str) -> dict:
|
|
"""The tensors of the GMFlow file (the source of truth)."""
|
|
import torch # noqa: PLC0415 - the opt-in environment
|
|
|
|
checkpoint = torch.load(path, map_location="cpu", weights_only=True)
|
|
weights = checkpoint.get("model", checkpoint)
|
|
return {"flownet.model." + key: value for key, value in weights.items()}
|