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.
A self-review of the device-output canvas found the rare near-black
branch calling the numpy _background_max_outside with a device tensor -
it happened to work through the numpy/torch API overlap, but it made an
undocumented per-frame device read inside the branch and would break on
a torch-only API change. The branch is rare by construction, so this one
frame is downloaded explicitly, exactly like the full-canvas fallback
next to it.
Also adds the device-output pin of the near-black branch itself
(test_the_device_canvas_keeps_the_near_black_full_canvas_fallback): the
rare path now runs in the canvas mode in the gate instead of only in
production.
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 paste stage spent its host time on three full-canvas passes per frame. The face warp now runs on the device with cv2's own fixed-point convention (the matrix inversion of warpAffine, the 1/1024 pre-scale, the 1/32 tap grid, the (+512)>>10 rounding), the mask warp reuses the one batched grid sliced to the face ROI, and the blend and the quantizing tail run inside that ROI. The frame-global maximum branch is kept: a near-black frame falls back to the full-canvas path, which stays bit-exact by construction.
Measured over 48 frames at 720p on CUDA: the paste call's process CPU time falls 20% (38.74 -> 30.92 ms/frame), the host-device transfers shrink 6.66x (the blended canvas download by 85%), and the result is bit-identical to the full-canvas path on the smooth and the noise corpus, at interior and edge-touching ROIs.
New src/keep_gpu_paste.py: the chunked ParseNet forward (chunk cap 8, the measured VRAM sweet spot), the batched parse-mask postprocess, the batched blend with the vendored quantizing tail, one tiled_scale call for the whole background group, is_gray without the PIL round trip, and VRAM-aware chunk sizes from model_management.get_free_memory. The frames the batched paste cannot handle (draw_box, face upsampler, grayscale, no parse net, no face) keep the vendored paste_faces_to_input_image. Equivalence against the real vendored paste: <=1/255 on the parse mask, blend and tail bit-exact.