- R1: the face helper is constructed on the pack device (was the offload
device); load_device() explicitly places the detector and the parser with
a logged CPU fallback, offload() mirrors the restore, the YOLO .device
skew is closed (P-D1..P-D5).
- R2: detection streams per max_clip_length group (no full-clip convert-in
retention), crop is fused into a chunk-bounded restore loop with
preallocated staging, the paste tail keeps one contractual float32
buffer, the helper is released after every run; golden fixtures pin the
outputs bitwise (P-M1..P-M7).
- The diagnosis: the OOM was O(frames) whole-clip host buffers, not the
faces (the detector/landmark path was already GPU-placed);
max_clip_length never bounded convert-in or the paste tail.
- Gate: 523 passed, 6 skipped, 7 deselected, 0 failed (baseline 505).
The round-1 device canvas disengaged past its fixed 1 GiB budget, so at
final_upscale_factor=4 (~44 MB/frame at 720p) typical clips fell back to
the host assembly at 16x the pixels. The engagement is now three-way:
- device_canvas_budget: a share of the free VRAM (0.25) under the hard
cap, the cap when free memory is unavailable - the mirror of
paste_chunk_size.
- device_output_mode: full | chunked | host. The chunked mode assembles
frame groups on the device (the same group size the full mode derives
internally, so the batch compositions coincide) and downloads the
per-group convert-outs into one preallocated host IMAGE buffer - the
device footprint stays bounded for any clip length.
- comfy_convert_out: the BGR->RGB float convert-out runs on the device
in budget-sized sub-chunks (the out-of-place chain peaks at ~9x the
canvas bytes of transient), so the budget bounds what it gates.
- a "host" verdict on a CUDA device is logged - the silent fallback
that hid the regression must not be silent again.
Also: the paste loop's NaN-landmark short-circuit never recorded its
frame in fallback_indices - in a device-output mode the tail's indexed
assignment length-mismatched (pinned red before the fix; the branch is
defensive today, the production interpolation fills NaN landmarks).
8 new bit-exactness pins (budget, selector, convert-out sub-chunking,
factor-4-scale, chunked assembly incl. group-boundary independence,
sentinels, end-to-end modes); the instrument gains the factor-4-scale
output-mode scenario. Verified on the CPU device only - no GPU run (the
CUDA before/after stays owner-gated). Gate: 461 passed, 6 skipped,
7 deselected, 0 failed.
The instrument now times the same paste_faces_batch call in the host and
the device output mode, one session, one fixture, and asserts the two
are bit-identical. On the CPU fixture (8 frames, 360x640, stub nets):
the output assembly's host<->device traffic is gone (1.06 MB each way
becomes 0, the ROI-sized staging recorded as roi_staged_bytes), wall
-8.8%, process CPU -8.7%. The delta dilutes the CUDA effect - on the
owner's GPU the kernel work stays on the GPU and the removed portion is
pure host time - so the CUDA measurement of the same comparison stays
the owner-gated V2.
The paste-stage attribution (tools/prof_paste_host_cpu.py, the CPU-device
stub run) showed the batch always takes the device path - the vendored
per-frame fallback never runs in the default configuration - and that the
remaining host cost of the step is the output assembly: the per-frame ROI
download, the whole-canvas host copy, the whole-stack np.stack and the
float math of the convert-out, all of which exist only because the output
stack is assembled in host memory.
F1 keeps the stack on the device: batched_bg_frames(return_device=True),
paste_faces_batch(device_output=True) scatters the blended ROI patches
into one uint8 device canvas, and the paste step assembles the batch with
one indexed copy, one stacked upload of the vendored-fallback frames and
a single bgr_u8_batch_to_comfy on the device. Nothing is downloaded or
copied per frame. The mode is off on a CPU device, over the 1 GiB
PASTE_DEVICE_STACK_MAX_BYTES budget and when nothing is eligible, so the
host path - and every existing pin - runs byte-for-byte as before; the
bytes that would have travelled are kept as roi_staged_bytes.
The new pin test_the_device_output_stack_is_bit_identical_to_the_host_stack
pins the canvas against the host stack on a CPU device. Gate: 451 passed,
6 skipped, 7 deselected.
The owner's route decision (the tolerance-accepted GPU resample):
resize_lanczos_u8 implements PIL's three-lobe Lanczos with the
scale-aware support and the drop-and-renormalize edge rule as dense
axis matrices + matmul, chunked by LANCZOS_CHUNK_PIXELS pixels.
batched_bg_frames keeps the result of _tiled_scale_batch on the device,
resizes with the new op when CUDA and downloads once at the end; a CPU
device keeps the per-frame cv2 loop (pixel-identical there - the
equivalence pin still holds exactly).
Pinned in test_image_op_equivalence.py against PIL (the semantic
reference: mean <= 0.3, max <= 12) and against cv2 (the accepted drift:
upscale mean <= 1.0 / max <= 8, downscale mean <= 8.5 / max <= 54),
plus chunk transparency and a GPU-vs-CPU pin; the profiler wraps the
op as p6_bg_resize.
The probe now measures the paste stage's sub-stages (the wall time, the process CPU time and the core count of every step) and counts the host-device copies of the paste, so a change of the paste can be judged on the CPU load it leaves, not on the wall time alone.
Times every stage of process_image_sequence on synthetic frames with a same-session control clone of the base implementation, plus micro benches for the parse chunk sweep and the KEEP-stage allocator question. The evidence behind the redesign ranking and the before/after tables of the internal design review.
The tracked files still pointed at the development documents, which now live
in the untracked .dev tree: the usage headings promised the embedded
interpreter while the examples are interpreter agnostic, and the provenance
notes named a local document. The headings are reworded and the provenance
notes name the tool that produces the record instead.
The tools named a local drive, a local interpreter and local ComfyUI trees.
They now take the tree from the command line or from the environment only -
a machine-local default would make a scan run against the wrong tree - and
the usage examples are interpreter agnostic. The exported example workflow
no longer carries the absolute path of the machine it was exported on.
The audit artifacts and the plan moved to the local .dev/docs tree, so the
references in the code, the tools and the tests follow them there. The
license file names the tool that produces the record instead of a path.
The five review findings of 2026-10-24 for phase P6:
1. Gate 2 (`ruff check .`) was RED on the P6 tree: F841 - the local `size`
of `_anchor_in` in tools/render_workflow_preview.py:95 was assigned but
never used, so the lint job of .github/workflows/tests.yml would have
gone red on this branch. The unused local is gone; the render output is
unchanged (the regenerated png has the same sha256 as the committed
one). The gate was recorded as "not runnable" although ruff IS
installed on this machine (0.15.22; the CI pin 0.8.6 reproduces it via
`uvx ruff@0.8.6`) - the embedded env is only one of the environments.
The gate records (audit D6, the progress doc, the status doc, the plan)
now state the truth.
2. The per-node parameter tables of the README were not checked by any
test, although the README calls the widget names, the defaults, the
ranges and the order compatibility guarantees. tools/docs_tables.py
renders what a V3 schema says (documented_parameters) and
test_the_parameter_tables_of_the_readme_match_the_node_schemas compares
the README with it. The check is mutation-checked: a wrong
`max_clip_length` default/range, a wrong `final_upscale_factor` range,
a wrong `only_center_face` default and two swapped widget rows each
fail the test (the values in the README are correct - the gap was the
coverage).
3. tools/docs_tables.py carried REPO_FILE_EXTENSIONS and
EXTERNAL_PATH_ROOTS without using them while the test re-defined them.
The test imports both from tools/docs_tables.py now - one copy, no
drift between the generator and the checker.
4. The rendered preview of the sequence workflow was never shown in the
README (only the shipped workflow's preview was). The README shows
example_workflows/keep-sequence-workflow.png in the Example workflows
section, captioned as a rendered graph diagram and not a screenshot.
5. The committed progress doc called its own review file untracked and
"stays uncommitted" although it is committed (0066001, 185c289). The
gate-7 row names `.dev` as the only untracked path now.
Checks (run on 2026-10-24): ruff check . -> All checks passed! (0.15.22)
and uvx ruff@0.8.6 check . -> All checks passed! (the version CI pins);
pytest -q -m "unit or mock_integration or headless" -> 262 passed, 6
skipped, 5 deselected; pytest -q tests/test_structure.py -> 21 passed;
pytest -q tests/test_example_workflows.py tests/test_docs_claims.py
tests/test_examples_dir.py -> 29 passed; pytest -q -m gpu_acceptance -rs
-> 5 skipped (opt-in); the headless tier against the real ComfyUI source
-> 4 passed, 1 failed (the pre-existing D5 mock-fidelity failure - P6
touched neither conftest.py nor that test).
Seven review findings of the P1 phase, each fixed at its root:
1. (high) The CI install list omitted PyYAML, but the vendored tree imports
yaml eagerly (modules/deps/basicsr/utils/options.py:5), so the CI job
failed on its first run. pyyaml is now installed and asserted by
tests/test_ci_workflow.py.
2. (medium) tests/test_ci_workflow.py skipped the whole module without
PyYAML. The module never skips now: a minimal indentation parser is the
fallback, and it is cross-checked against the PyYAML parse.
3. (medium) Guard (e)'s self-test asserted that the tree still contains
breakage (it would break exactly at P2.1). It now proves the guard
instead; P2.1's work list is the scanner output and
test_guard_e_flip_owner_covers_every_current_finding keeps plan and tree
in sync.
4. (medium) The mock-fidelity tripwire skipped the comfy_api.latest
symbols. Every mocked symbol is now checked against the real source
(re-export / definition / instance attribute), including io.ComfyNode,
ComfyExtension, ComfyAPI and ComfyAPISync, plus the reverse check that
the mock invents nothing.
5. (low) The phase review listed committed work as outstanding and
misstated the ruff ignore list; both are corrected.
6. (low) The reported whole-suite count was impossible; the phase review
now carries the measured numbers.
7. (low) The root conftest installed the mocks unconditionally. The
KEEP_NO_COMFY_MOCKS=1 opt-out lets the headless tier import the real
comfy/comfy_api modules in-process.
tools/ci_env_sim.py runs the gate in an environment without the packages
the CI install list omits.
All 7 P0 review findings were verified by execution and fixed.
F1 (medium) - the compat scanner modelled only the basicsr/archs eager scan.
The vendored basicsr also scans data/, losses/ and models/ eagerly
(basicsr/__init__.py:3-11 imports all of them), so the closure missed 32
files that the repaired package really imports (incl. models/keep_model.py and
models/keep_gan_model.py, both with 'import pdb' at line 4). The scanner now
models all four scans plus 'from <package> import <submodule>': closure
100 -> 132 files, findings 8 -> 10, and 0 runtime-loaded vendored files are
missing from the closure (verified on a temp copy of modules/deps).
The audit doc's opposite claim is corrected and P2.4 in the plan now has to
rewrite basicsr/__init__.py and prove 'import basicsr' still succeeds after
pruning (otherwise ModuleNotFoundError is swallowed into DummyArchRegistry).
F2 (medium) - the Asian manifest entry is now decided (P0.4 section 4.1):
url 404, sha256 null, sha256_prefix '4765ebe0', unverified true, available
false, required FALSE (a required entry would break the default KEEP model
through P5.3's fail-fast rule). Plan P5.0/P5.1/P5.3 and the weights scanner
were aligned.
F3 (low) - the plan's section 8 decision 1, the B3 baseline row, Goal 5, the
risk table and P5.5 carried the superseded GMFlow premise; they now match the
P0.4 decision (the shipped checkpoint carries the flownet weights; the GMFlow
file is defensive and resolved only when the checkpoint has no flownet.* keys).
F4 (low) - the P0.1 fixture asserted rc == 0 before checking the probe's fatal
report, so the documented skip was unreachable. It now handles the probe's
exit code 2 + fatal report and skips with a reason.
F5 (low) - the baseline-import doc claimed all eight stub symbols fire in the
ffmpeg-blocked run; it is 7 of 8 (loader_face_restore_helper_is_dummy does not
fire - the real facelib imports fine).
F6 (low) - the scanner's exit-code contract (0 = no findings, 1 = findings) is
documented in the tool docstring and in the audit doc.
F7 (low) - the root conftest registered '__init__' and the invalid directory
basename; it now registers the plan's alias 'ComfyUI_KEEP' and provides
execute_package_init() so the V3 entrypoint is executed rather than bypassed.
P1.2 is specified to replace that bootstrap.
Gates: pytest tests/ -q -> 37 passed, 1 xfailed; scan_compat.py --quiet ->
closure 132, 10 findings, exit 1; baseline_probe.py -> import ok / model build
fails (unchanged baseline); scan_weights.py -> 0 of 8 present.