mirror of
https://github.com/Comfy-Org/comfy-aimdo.git
synced 2026-11-04 23:37:21 +00:00
[GH-ISSUE #129] malloc graph: a rogue candidate freed before the root pop permanently poisons the graph → RuntimeError("aimdo memory compile error") #49
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#49
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 @lonelystarCX on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/Comfy-Org/comfy-aimdo/issues/129
Summary
On a long single-graph lifetime (a 7-forward sequence inside one sampler node) the aimdo
memory compiler stops accepting work mid-sampling:
The failing call is
malloc_graph_pause(), but the graph was already dead before it:g->failedhad been set because one rogue candidate was freed beforefinalize_rogues()could adopt it. From that point on
alloc/free/pause/push/pop/set_streamall returnfalse, i.e. the whole recording is unusable, and the only recovery is destroying the graph.
Everything below is measured on 7 reruns of the same API prompt (5 baseline, 2 controls,
all deterministic); the mechanism is read from
mastersource and cross-checked against theshipped 0.5.6 binary.
Environment
edb3db00(2026-10-20), Linuxg08b37eec3)cudaMallocAsync, dynamic VRAM enabled,--async-offload 2python3 main.py --listen 127.0.0.1 --port 8188(no compiler/VRAM flags)Artifact note (please read before dismissing as third-party):
comfy-kitchenis not thestock wheel — the Spark-H3 v0.1.5 installer replaces it with
0.2.39+spark.h3.sm89.cu130.1. Graph isMiniMax-H3+ComfyUI-Spark-H3(block-sparseattention patch) +
selflift-Avatar(two-pass sampler with 4-tile high-res spatial tiling)LoraLoaderBypassModelOnly. I have accounted for all of those in the controlsbelow; the aimdo-side behaviour is independent of them.
Mechanism (from master source)
malloc_graph_pop()severs every allocation that is still alive in the scope beingpopped (
src/malloc-graph.c,collect_rogue_candidates()), registering each in theglobal candidate list and bumping
g->rogue_count:malloc_graph_pop()calls it unconditionally. So any pop that happens while tensorsfrom that scope are still referenced by the application mints rogue candidates. In this
workload that is ~50 per model forward.
Freeing one of those candidates before adoption marks it freed —
free_rogue()insrc/malloc-rogue.cfalls into the "still a candidate" branch and setscandidate->freed = true.graph_failed()turns that into a permanent, whole-graph flag, and it runs on theentry path of every API call:
Candidates are only meant to be resolved at the root pop —
if (!g->state->next && !finalize_rogues(g))— i.e. after the entire forward unwinds.Any candidate freed between a nested pop and the root pop poisons the graph.
Consequence for callers: there is no reset.
malloc_graph_abort()refuses oncehandoff_attemptedis set,malloc_graph_destroy()is the only way out, and ComfyUI onlydestroys at cleanup — so the prompt dies with
RuntimeError.Note on allocations while paused:
malloc_graph_alloc()returns false wheng->paused(and whenstream != g->stream), so paused-window allocations are never trackedand never become candidates. This matters for triage — see the controls below.
Runtime evidence
A
sitecustomizeprobe (PYTHONPATH only, no ComfyUI file modified) wrappedMallocGraph.pause/pop/pushand read the nativefailedbyte directly athandle + 0x5E0D0(the offsetgraph_failed()writes; verified against the 0.5.6disassembly of
graph_failedat0x8790andmalloc_graph_pauseat0xa880).failedis 0 at the last pop and 1 on the very nextpause()a few ms later:the flip happens between two block-boundary calls, i.e. exactly at the free of a
candidate, and the pause only reports it.
in_MALLOC_GRAPHS=True/_comfy_active=True: this is not theg != active_graphbranch — it isgraph_failed().rogue_countladder over one graph lifetime (malloc_graph_stat(3), cumulative):48 → 54 → 104 → 154, jumping only at forward boundaries, flat afterwards. So severinghappens at pops, and the fatal early-free comes later, from a candidate severed earlier.
Controls (each one a full rerun of the same API prompt)
pause_malloc_graph()narrowed to wrap only the nativerms_ropekernel (attn.qkv_projandindex_selectmoved out of the paused window)comfy_kitchen.set_allocation_contextreplaced with a no-op, so kitchen never pauses around its opsBoth controls leave the rogue ladder byte-identical → the candidates are not produced by
any Python-side pause window, consistent with point 5 above. The failure is bound to the block
sequence (always the same
pop#762), not to the number of pauses.Minimal shape of the trigger
(2 low-res steps + transition + 4 tiled high-res forwards);
referenced → ~50 candidates/forward;
A stock
MiniMax-H3graph with fewer forwards never reaches the plateau; the tiled/loopedcaller is what makes it deterministic (7/7 runs, 3 different configurations).
Proposed fixes (any of these unblocks callers)
rogue_candidate_freed()is true, drop thatcandidate and fence its VA with the existing
black_holes/small_unusablemachineryinstead of setting
g->failedfor the whole graph. The graph can keep planning around ahole; it cannot continue with a global fatal flag.
finalize_rogues()at theend of
malloc_graph_pop()would adopt/fence candidates while their owners are stillalive, shrinking the window between "severed" and "adopted" to zero.
malloc_graph_reset()), so ComfyUI can recreate the graphmid-prompt instead of failing the run. Related: ComfyUI PR #16883 already stops cleanup
from calling
abort()on a failed graph — a reset would let the caller retry instead ofdying.
_callmaps push/pop/pause/abort/set_stream/create failures onto one string) makes remote triage very expensive. A closed
enum of failure reasons from the C side would be enough.
I'm happy to test a patch on the reproducer (7/7 deterministic) and report pass/fail per line.
Related
pop()failing at 0 % sampling (Qwen Image 2.1 cache), fixed via PR #16667allocation inside
pause_malloc_graph()could not start recording(still open)abort()on an already-failed graphWorkaround currently in use
--disable-comfy-compiler(no malloc graph → nograph_failed()), at the cost of the flatVRAM profile. Keeping the compiler is what this report is about.
Local verification of the proposed direction (same machine, same prompt)
Built
comfy-aimdov0.5.6 (the installed version, commit08b37eec3) from source withgcc 13 / nvcc 13.0 (funchook 1.1.3 via the repo's own
scripts/build-linux-aimdo.sh) andswapped the resulting
comfy_aimdo/aimdo.sointo site-packages (shipped wheel backed up,sha256
8764ede0…), then replayed the same API prompt:node 328 SelfLiftAvatarH3Sampler,aimdo memory compile errorgraph breaks: 21, rogues: 235, writes1312×736 HEVC 24 fps + AAC 32k stereo, 10.125 sSo the local rebuild reproduces the defect and the change below removes it end to end.
Patch actually tested (49 lines,
src/malloc-graph.c)Rationale for it being sufficient rather than merely silencing:
g->rogue_candidates,rogue_va()/rogue_phys()keep its address range out of any newly planned allocation, so the compiler cannot hand the
range out again;
handoff_rogues()reports asROGUE_HANDOFF_FREEDandfinalize_rogues()handles at the root pop (it is not adopted, then scrubbed);g->failedis still set by the genuine failure paths(
RETURN_G_FAILED,handoff_rogues()returningROGUE_HANDOFF_ERROR, remap failures).Caveats I can see, in the interest of a fair review: the successful run reports more breaks
(21) and candidates (235) than the fatal one (9 / 154), i.e. the graph keeps living with an
increasing number of severed ranges; whether the fenced-VA bookkeeping should eventually
recycle them (instead of holding them for the graph's lifetime) is your call — from the
caller's side, finishing the prompt beats dying at 60 % either way.
Happy to send this as a PR (rebased onto master —
graph_failed()there is byte-identical)with
AIMDO_ROGUE_FREE_IS_FATALas the A/B switch, if the direction is acceptable.