Avoid reference cycles in graphs - #33
Merged
Merged
Conversation
Member
Author
@MridulS Is |
NetworkX caches report views such as `G.edges` or `G.out_degree` in `G.__dict__`, and each of those views holds a reference back to the graph. A graph a view was taken of is thus reclaimable only by the cyclic garbage collector, which is triggered by the number of allocated objects and may therefore not run for a long time when few but large objects are involved. Cyclebane creates and discards graphs in most of its operations and node attributes can hold large data, so applications that repeatedly update a graph (streaming workflows inserting chunks via `Graph.__setitem__`) grew without bound. Use a `nx.DiGraph` subclass that recreates the views on each access instead. Graphs derived by NetworkX operations inherit the subclass, so this also covers the intermediates built inside `nx.compose` and `nx.relabel_nodes`, as well as the graph returned by `to_networkx`. Verified with the reproducer from scipp/esslivedata#1264: 60 iterations of `pipeline[Chunk] = <67 MB array>; pipeline.compute(Result)` with the cyclic collector disabled stay flat at 126 MB instead of reaching 4.1 GB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SimonHeybrock
force-pushed
the
avoid-cyclic-garbage
branch
from
August 31, 2026 05:50
5cf257e to
f3fd0e4
Compare
Member
|
I remember this networkx/networkx#7697, and the hope was the CPython gc should take care of it. I'll look a bit more into this. |
MridulS
approved these changes
Sep 2, 2026
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
networkxcaches report views such asG.edgesorG.out_degreeinG.__dict__, and each of those views holds a reference back to the graph. Merely looking at a graph therefore places it in a reference cycle, so it can be freed only by the cyclic garbage collector. That collector is triggered by the number of allocated objects, so a handful of discarded graphs holding large node data can survive indefinitely. Cyclebane creates and discards graphs in most of its operations, and the graph returned byto_networkxbecomes self-referential as soon as a consumer inspects it, so applications that repeatedly update a graph grow without bound. This OOM-killed a 32 GB host in 34 minutes, see scipp/esslivedata#1264 and scipp/esslivedata#1266.Avoiding the view accesses in cyclebane itself does not work:
nx.relabel_nodesandnx.compose_allmaterialiseG.edgeson their inputs internally, and the graph handed to the caller byto_networkxgets views taken on it by the caller. Instead, use anx.DiGraphsubclass in which the six back-referencing views are recreated on each access. Graphs derived bynetworkxoperations are created viaG.__class__(), so all intermediates,to_networkxresults, subgraphs and copies inherit this, without changes at any of the call sites.Note that
Graph.__init__now copies a graph that is not already of this type, with the same semantics asnx.DiGraph.copy.Each of the added tests fails without the change.
Verification
With the reproducer from scipp/esslivedata#1264 (sciline + numpy only) and the cyclic collector disabled, 60 iterations of
pipeline[Chunk] = <67 MB array>; pipeline.compute(Result)now stay flat at 126 MB instead of reaching 4.1 GB. Withgc.DEBUG_SAVEALL, map/reduce/groupby/to_networkxproduce no cyclic garbage at all.Cost
Creating a view costs ~0.2 us, which is measurable only for code re-accessing a view per node, e.g.,
[G.degree(n) for n in G]is 2x slower. Completenetworkxalgorithms (topological_sort,dag_longest_path,pagerank,ancestors) are unaffected within noise, since they bindG.adjonce and the views that do not hold a graph reference (adj,succ,pred,nodes) are still cached.to_networkxis unchanged,__setitem__is slightly faster.The views are no longer identical across accesses. They compare equal where
networkxdefines equality for them, but the degree views do not, i.e.,G.degree == G.degreeis now False.