mirror of
https://github.com/Comfy-Org/comfy-aimdo.git
synced 2026-11-04 23:37:21 +00:00
[GH-ISSUE #117] vbar_fault() resident detection for a VAE degrades irreversibly after a multi-segment continuity generation (26x H2D memcpy blowup, GPU compute unaffected) #44
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-aimdo#44
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @lilam9110 on GitHub (Sep 17, 2026).
Original GitHub issue: https://github.com/Comfy-Org/comfy-aimdo/issues/117
Summary
vbar_fault()'s residency detection for a VAE model degrades irreversibly (until process restart) after a multi-segment "continuity" generation completes. Once triggered, every subsequent decode on that VAE — regardless of task type — sees its resident-cache hit rate collapse from ~100% to ~0%, causing the same weights to be re-transferred Host→Device on nearly every forward pass. GPU kernel compute time is unaffected; the entire slowdown is H2D memcpy volume. We've root-caused this down to thevbar_fault()boundary (compiledaimdo.so, no source access) across an 11-round investigation and can supply the full evidence trail.Environment
SelectModelDevice/SelectCLIPDevice/SelectVAEDevice)--use-ck-attention --disable-cuda-malloc, no--cache-ramoverride (defaultRAMPressureCache)ComfyUI_MiniMaxH3_Director), VAE =minimax_h3_video_vae_fp16.safetensors, VBAR/DynamicVRAM enabled (default)Symptom
Same VAE, same resolution, same frame count, in the same long-lived ComfyUI process: after one "continuity" submission (multi-segment generation with a motion-context latent pin carried in-graph across segments) completes, decode time on every following submission — regardless of whether it uses continuity again — jumps from baseline ~15-25s (90-141 frames) / ~43s (243 frames) to 130-285s+, and keeps escalating across subsequent submissions. This persists for the remaining life of the process (observed >12h / dozens of submissions without self-recovery) and only clears on restart.
Reliable repro sequence (confirmed 3x independently, near-identical numbers each time)
41.534s → 62.919s → 129.623sacross 3 internal segments of that single submission, then207.4s / 284.4son the next.Multiple independent repro runs (different days, fresh processes) landed within a few seconds of each other on segment 3 (~129.6-130.9s), strongly suggesting a deterministic trigger, not noise.
Root-cause chain (what we verified, with tooling)
py-spy (8x stack dumps + flamegraph during a 130s degraded decode): 100% of sampled time is in normal decode code paths (
TransformerBlock/Attention/rms_norm/ops.py::forward_comfy_cast_weights/cast_modules_with_vbar). No Python-level deadlock, no stuck loop. Bottoms out atcomfy_kitchen's_int8_attention_cuda— a compiled extension py-spy can't see into.nsys (kernel-level comparison, healthy vs degraded window, same process): GPU compute kernel time is essentially unchanged (230.838s vs 230.334s total, ratio 0.998). But
[CUDA memcpy Host-to-Device]:cudaHostRegister+ 98x non-trivialcudaMalloccalls (0 in healthy window) — i.e. fresh host-side buffer allocation/registration, not reuse.Static read of
comfy/ops.py::cast_modules_with_vbar: the resident check isxfer_sourcein the slow path is always the module's own[weight, bias](or its pin) — confirmed no extraneous data is being transferred; it's the same weights being re-transferred on every fault.Debug-probe instrumentation (temporary logging around the resident check, isolated worktree):
cuda:1VAE resident hit rate is 100% (5874/5874) in a clean/healthy window, then collapses monotonically across the degraded submission's 3 internal segments: 40.5% → 25.4% → 0.2%, tracking almost perfectly with the decode-time escalation (41.7s / 63.0s / 129.6s) in the same run.Ruled out (each with a controlled A/B on the exact same repro sequence, real numbers, not just code reading):
comfy_extras/nodes_multigpu.py::_retarget_patcherrepeatedly reassigningoffload_device— UNet goes through the identical code path every call and never degrades; also confirmedoffload_deviceis discarded (ModelPatcherDynamic.unpatch_modelhardcodesdevice_to=None) and never read by the native layer.comfy/model_patcher.py:2072-2122's "AIMDO free" logging never fires once across the whole trigger+degrade window — 0 hits in a 341k-line DEBUG log).nvml_pressure(--disable-nvml-pressure): degradation still reproduces at the same magnitude.--disable-pinned-memory(tried based on a similar-looking closed issue, #104/comfy-org/ComfyUI#16062): degradation still reproduces, if anything slightly worse (52.5/77.3/162.2s vs historical 41.5-52.5/62.9-77.3/129.6-162.2s) — this tells us our case is a different mechanism from #104's pinned-memory-pool sizing issue, even though the surface symptom (VAE-decode-stage slowdown that gets worse on repeat) looks similar.comfy/model_management.py:1386's process-wideLARGEST_AIMDO_CASTED_WEIGHT(not device-scoped, read/written cross-device byops.py:143/146-147) — plausible cross-device-interference candidate, but disproved by code flow:ensure_offload_stream(the only reader/writer) is only ever called afterresidenthas already been determinedFalse, so it structurally cannot be the cause of the resident check itself failing. Confirmed with the same debug probe: resident hit-rate collapse is identical regardless of whether this global gets clobbered by the other GPU's UNet.Where we're stuck
vbar_fault()itself (.venv/…/comfy_aimdo/model_vbar.py→lib.vbar_fault, a ctypes call into the compiledaimdo.so/aimdo_rocm.so) is where the actual "is this signature still resident" decision is made. We have no source access to that boundary. We also tried--verbose DEBUGhoping the nativeset_log_callbackhook would surface fault/evict diagnostics (per_LOG_LEVELSmapping native levels 5-7 →logging.DEBUG) — across the full trigger+degrade window we got exactly two one-time init messages and nothing else; no fault/evict/resident diagnostic logging exists at any verbosity we could reach.What we haven't isolated yet
What we can provide on request
Full evidence trail — 11(+1) rounds of logs,
nsys.nsys-repfiles + CLI stats exports,py-spydumps/flamegraph, debug-probe output, GPU/host-memory traces — is organized and citable by round number. Happy to share the relevant subset (or all of it) if useful for triage. This report intentionally leaves out an unrelatedLARGEST_AIMDO_CASTED_WEIGHTcross-device-scoping cleanup we noticed along the way (confirmed harmless to this bug,comfy/model_management.py:1386isn't device-keyed) — mentioning only in case it's an easy separate fix.@34262315716 commented on GitHub (Sep 19, 2026):
Same shape of failure, different stage — at the text encoder instead of the VAE. And a note about where the source code has been living this whole time.
Before the technical part, three observations offered in the spirit of friendly needle rather than complaint:
1. @lilam9110's report says "we have no source access to that boundary." That boundary is
src/model-vbar.c— in this repository. It has been sitting here the entire time. We spent a large part of an evening treatingaimdo.dllas an opaque black box before it occurred to us to open the repo the DLL came from. To be fair, we got there in the end; we just took a fifteen-hour scenic route past a file that was one click away. Consider that a shared character-building exercise.2.
vbar_free_memory()is extremely polite. When it does nothing, it returns0. It does not tell you it did nothing. It does not tell you it cannot do anything. It does not mention thatwatermarkhas already reachedwatermark_limit— which, after avbars_reset_watermark_limits(), means it can never do anything again. Onelog(WARNING, "watermark at limit, nothing to free")would have saved us several hours and one process abort. A silent no-op that looks identical to a successful release is a rough thing to debug from the outside.3. @rattus128 — you wrote in the ComfyUI issue thread, "I have no reproducer of that reported hang to do anything about it." Your own tracker had issue #117 open for two days, with nsys-CLI exports, py-spy flamegraphs, an eleven-round investigation and a monotonic degradation curve. The reproducer was, so to speak, already in the building. We'd have found it much sooner if the trail had pointed here first — which is on us, but the signpost was available.
With that out of the way, the actual content, which we hope is useful.
What we observe
comfy-aimdo 0.5.3, ComfyUI 0.36.0, Windows, RTX 4070 Laptop (8 GB VRAM / 32 GB RAM), MiniMax H3 via
ComfyUI_MiniMaxH3_Director.Same shape as the original report, different stage — text encoder rather than VAE:
comfy_aimdo.control.get_total_vram_usage()sits at 4.02–4.83 GB and does not move across segment boundariestrue_free_vramcollapses from 6.77 GB to 2.72 GButilization.gpu100 % butpower.draw27 W out of 140 W, memory-controller utilization 3 %, disk ~0 — spinning, not computingread_file_to_device→cast_modules_with_vbar→cast_bias_weight, i.e. the H2D slow path taken for the same weights over and overWhat we found in
src/model-vbar.c1.
vbar_fault()gates on the watermark before it consults residency at all:Python-side that becomes
fault() -> None→ops.pycomputesresident = False→ the layer takes the full host→device copy. Any vbar whose watermark has been driven below the pages being requested therefore reports "not resident" permanently, regardless of what is actually mapped.2.
vbar_free_memory()is a permanent no-op once the watermark reaches zero:0 > 0is false, so the loop body never executes and it returns 0.vbars_reset_watermark_limits()setswatermark_limit = 0on every vbar, andvbar_free_memory(large)itself drainswatermarkdown to that limit. So callingfree_memory(1 << 62)after avbars_reset_watermark_limits()is a state change that callingfree_memory()again cannot undo.3. Only
vbar_prioritize()restores the watermark:ComfyUI calls it from
model_patcher.pyon model load. Anything kept loaded across a submission boundary is never re-prioritized, so a drained watermark stays drained.That matches the reported "degrades irreversibly (until process restart)" and the 100 % → 40.5 % → 25.4 % → 0.2 % hit-rate curve: pages get evicted, the watermark drops with them, and from then on the fault returns OOM before residency is even consulted.
What drains them for us: the prefetch queues
The pages get pinned.
vbar_fault()ends with:and
mod1(..., do_free=true, do_unpin=false)will not free a page withpin_count != 0. Meanwhilecomfy/model_prefetch.pykeeps queues holding module references and faulted_v_blockranges. In our setup those queues were never cleared between segments, so the pinned pages stayed pinned, the encoder's watermark got drained by the nextfree_memory()attempt, and every subsequent fault on that vbar returned OOM.Workaround that fixed it for us (one line)
ComfyUI's per-node
finally(execution.py:546-554) does four things:A node that loops over segments inside a single node execution gets that block once for the whole job, not between segments. So we call the second line ourselves at each segment boundary.
Result, three segments, same process:
aimdo_vramat segment boundariestrue_free_vramFor calibration against the original report's escalation (41.5 → 62.9 → 129.6 → 207.4 → 284.4 s), our segment 3 — the one that should have blown up — came in at 41.9 s.
One loose end we couldn't explain
With every vbar reporting
watermark=0andloaded_size() == 0,get_total_vram_usage()still reports 4.02 GB. If every page is unmapped andresident_countis 0, we can't account for that number fromsrc/model-vbar.calone —mod1()decrementstotal_vram_usageon every free, so it shouldn't be vbar pages. Possibly a different pool counted by the same function; we ran out of evening before readingcontrol.cproperly. Not blocking for us, but if it is a real accounting leak it may matter here.Happy to supply logs, the full command lines of the runs that did and didn't reproduce, or re-run anything on request.