From 2c8f7f110cc4e6c73ac02e5b4f90eb3a3ced2160 Mon Sep 17 00:00:00 2001 From: ausboss <102309304+ausboss@users.noreply.github.com> Date: Sat, 10 Oct 2026 22:35:47 -0500 Subject: [PATCH] Tests: two checks give the same answer on every CPU The nightly ComfyUI job is the first place these two files ran with torch on a GitHub-hosted runner, and one runner failed both on unchanged code. - test_memory_helpers.py imported torch four times in one process: patch.dict(sys.modules) forgets whatever was first imported inside it. The fourth import ended the process without a word. Torch is now loaded before any test swaps modules. - test_stitch_classic_pins.py compared blend masks by hash. The feather's blur takes an exp, which CPUs round differently, so two masks moved by a float32 step. Masks are now compared with frozen release masks at the tolerance the images already have, and a new test holds that file to the pinned hashes. The nightly job also prints which CPU ran it. --- .github/workflows/comfyui_compat.yml | 2 + tests/fixtures/stitch_released_images.md | 46 ++++++++++++++++++++++- tests/fixtures/stitch_released_masks.npz | Bin 0 -> 28822 bytes tests/test_memory_helpers.py | 8 ++++ tests/test_stitch_classic_pins.py | 32 ++++++++++++---- 5 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/stitch_released_masks.npz diff --git a/.github/workflows/comfyui_compat.yml b/.github/workflows/comfyui_compat.yml index 5e59f61..a26f55c 100644 --- a/.github/workflows/comfyui_compat.yml +++ b/.github/workflows/comfyui_compat.yml @@ -97,6 +97,8 @@ jobs: python -m pip install -r ComfyUI/requirements.txt git -C ComfyUI log -1 --format='ComfyUI %h %cd %s' --date=short python -m pip list 2>/dev/null | grep -i -E "^(comfyui|torch) " || true + # Which CPU ran it: float results can differ by a last bit between CPUs. + lscpu | grep -E "^Model name" || true - name: Everything the pack takes from ComfyUI is there working-directory: ComfyUI-AusBoss run: AUSBOSS_COMFY_ROOT="$GITHUB_WORKSPACE/ComfyUI" python tests/test_comfy_core_contract.py diff --git a/tests/fixtures/stitch_released_images.md b/tests/fixtures/stitch_released_images.md index a3ed568..bab6e84 100644 --- a/tests/fixtures/stitch_released_images.md +++ b/tests/fixtures/stitch_released_images.md @@ -27,10 +27,29 @@ difference was 5.0068e-6; all masks were exact, and no rounded 8-bit channels changed. Thus images allow `atol=1e-5, rtol=0` (0.00255 of an 8-bit step). This is a CPU regression test, not a promise of bitwise GPU reproducibility. Shape, dtype and non-finite output errors still fail. A mutation check confirms -a single channel changed by 1/255 fails. Mask hashes remain byte-exact. +a single channel changed by 1/255 fails. The masks were byte-exact in all of +these runs, which shared one CPU; see Blend masks below. See [PyTorch numerical accuracy](https://docs.pytorch.org/docs/2.11/notes/numerical_accuracy.html) for why floating-point kernels need not agree bit for bit. +## Blend masks + +`stitch_released_masks.npz` holds the 11 float32 blend masks of the same runs: +`classic__mask` (one per case; the mask is the same at every strength) +and `blend_in__mask`. Each is the mask whose hash the test pins, and +`test_the_mask_references_are_the_pinned_masks` holds the file to those +hashes, so it carries the released code's masks byte for byte. + +A run is compared with these masks at `atol=1e-5, rtol=0`, like the images, +and no longer by hash. On a GitHub-hosted runner two masks came out with +another hash on unchanged code: the straight transform and Crop For Inpaint, +the two whose feather is a blur. The blur's bell curve is `torch.exp`, which +is accurate to under one float32 step but is not rounded the same way on every +CPU. Moving each value of that curve by one float32 step on the reference +machine changes the hash of exactly those two masks, moves them by at most +2.4e-7, and leaves the other four untouched. A blur made 2% wider still fails +both of those cases at every strength. + ## Reproducing the references Run from the repository root with the environment above. Never regenerate from @@ -84,3 +103,28 @@ for mode, ref in ( arrays.update({key: data[key] for key in data.files}) np.savez_compressed("stitch_released_images.candidate.npz", **arrays) ``` + +The masks file needs no old checkout: the pins already say what the releases +gave. Run the test's own builders and keep a mask only when its hash is the +pinned one. On a CPU that rounds the blur differently the assert fails and +nothing is written. + +```python +import runpy +import numpy as np + +m = runpy.run_path("tests/test_stitch_classic_pins.py") +arrays = {} +for index, (name, build) in enumerate(m["CASES"].items()): + for strength in m["STRENGTHS"]: + stitcher, patch = build() + _image, mask = m["stitch"](stitcher, patch, strength) + assert m["digest"](mask) == m["PINS"][(name, strength)][1], name + arrays[f"classic_{index}_mask"] = mask.numpy().copy() + if name in m["BLEND_IN_CASES"]: + stitcher, patch = build() + _image, mask = m["stitch"](stitcher, patch, 0.0, seam="blend in") + assert m["digest"](mask) == m["BLEND_IN_PINS"][name][1], name + arrays[f"blend_in_{index}_mask"] = mask.numpy().copy() +np.savez_compressed("stitch_released_masks.candidate.npz", **arrays) +``` diff --git a/tests/fixtures/stitch_released_masks.npz b/tests/fixtures/stitch_released_masks.npz new file mode 100644 index 0000000000000000000000000000000000000000..1de3d7207686df9fa36257d6d7b022adfe2f8855 GIT binary patch literal 28822 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 literal 0 HcmV?d00001 diff --git a/tests/test_memory_helpers.py b/tests/test_memory_helpers.py index 06771f4..8851eda 100644 --- a/tests/test_memory_helpers.py +++ b/tests/test_memory_helpers.py @@ -13,6 +13,14 @@ if "nodes" in sys.modules and not hasattr(sys.modules["nodes"], "__path__"): from nodes._memory_helpers import WILDCARD, free_gpu_memory +try: + # Loaded before any test swaps sys.modules: patch.dict forgets whatever + # was first imported inside it, and torch cannot be imported twice in + # one process (the second import can end the process without a word). + import torch # noqa: F401 +except Exception: # these tests also run where torch is not installed + pass + class WildcardTypeTests(unittest.TestCase): def test_it_is_the_star_string(self): diff --git a/tests/test_stitch_classic_pins.py b/tests/test_stitch_classic_pins.py index 782b625..9c110e1 100644 --- a/tests/test_stitch_classic_pins.py +++ b/tests/test_stitch_classic_pins.py @@ -13,9 +13,12 @@ the published Qwen Image 2.1 Rotate + Outpaint workflow stitches that way. Tone-matched float32 output varies slightly with CPU kernels and thread counts, including on unchanged releases. Images are compared with frozen release tensors at an absolute tolerance of 1e-5 (0.00255 of an 8-bit step), -with no relative tolerance. Masks still use exact hashes. The fixtures come -from released code, never the implementation under review; see -fixtures/stitch_released_images.md for provenance and regeneration. +with no relative tolerance. A feathered blend mask moves the same way, by a +float32 step or two: its blur takes an exp, and CPUs round that differently. +Masks are compared with frozen release masks at the same tolerance, and each +of those is held to its pinned hash. The fixtures come from released code, +never the implementation under review; see fixtures/stitch_released_images.md +for provenance and regeneration. """ from __future__ import annotations @@ -45,9 +48,11 @@ STRENGTHS = (0.0, 0.5, 1.0) IMAGE_ATOL = 1e-5 REFERENCE_IMAGES = ROOT / "tests" / "fixtures" / "stitch_released_images.npz" +REFERENCE_MASKS = ROOT / "tests" / "fixtures" / "stitch_released_masks.npz" -# Historical image hashes are retained for --print diagnostics. Only masks -# are byte-exact across CPU backends; images use the release fixtures below. +# Historical image hashes are retained for --print diagnostics; images use +# the release fixtures below. A mask hash says which mask the release gave: +# the mask fixture must have that hash, and a run is compared with the fixture. # (case, color_match) -> (image, blend_mask), first 20 hex digits of SHA-256. PINS = { ("load image + pad", 0.0): ("bceaded74f00fa7af3df", "da3307917bed14a6f6ac"), @@ -175,6 +180,12 @@ def reference_image(name, strength, seam="classic"): return torch.from_numpy(references[key]) +def reference_mask(name, seam="classic"): + key = f"{seam}_{tuple(CASES).index(name)}_mask" + with np.load(REFERENCE_MASKS, allow_pickle=False) as references: + return torch.from_numpy(references[key]) + + def assert_released_image(image, expected): # No relative tolerance: bright pixels get no extra allowance. Shapes, # dtypes and non-finite values must also match (equal_nan defaults false). @@ -190,7 +201,14 @@ class ClassicSeamPinTests(unittest.TestCase): stitcher, patch_image = build() image, blend_mask = stitch(stitcher, patch_image, strength) assert_released_image(image, reference_image(name, strength)) - self.assertEqual(digest(blend_mask), PINS[(name, strength)][1]) + assert_released_image(blend_mask, reference_mask(name)) + + def test_the_mask_references_are_the_pinned_masks(self): + # Reads the files only, so every machine gives the same answer. + for (name, _strength), (_image, mask) in PINS.items(): + self.assertEqual(digest(reference_mask(name)), mask) + for name, (_image, mask) in BLEND_IN_PINS.items(): + self.assertEqual(digest(reference_mask(name, "blend_in")), mask) def test_release_pixels_with_one_thread_and_native_convolution(self): # Exercise the reduction/kernel choices that break raw image hashes. @@ -265,7 +283,7 @@ class BlendInToneMatchOffPinTests(unittest.TestCase): stitcher, patch_image = CASES[name]() image, blend_mask = stitch(stitcher, patch_image, 0.0, seam="blend in") assert_released_image(image, reference_image(name, 0.0, "blend_in")) - self.assertEqual(digest(blend_mask), BLEND_IN_PINS[name][1]) + assert_released_image(blend_mask, reference_mask(name, "blend_in")) def test_release_pixels_without_mkldnn(self): with torch.backends.mkldnn.flags(enabled=False):