-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Exponential memory growth in UnionArray broadcasting #3118
Comments
@jpivarski @tcawlfield this is interesting! I think this is a leak / bug of some kind; even manually stepping through the loop eventually blows up my RSS and leads to paging. i.e. it feels non time-dependent (i.e. not gc; I checked manually!) and rather it feels stateful. I would suspect this might be our growable buffer (due to the stateful-memory aspect), but I haven't done anything beyond checking this reproduces for me on |
Here's a simpler reproducer. I found that the behaviors are unnecessary and the first level of list-nesting is unnecessary ( import awkward as ak
one_a = ak.Array([{"x": 1, "y": 2}], with_name="T")
one_b = ak.Array([{"x": 1, "y": 2}], with_name="T")
two_a = ak.Array([{"x": 1, "z": 3}], with_name="T")
two_b = ak.Array([{"x": 1, "z": 3}], with_name="T")
three = ak.Array([{"x": 1}, {"x": 1}], with_name="T")
first = ak.zip({"a": one_a, "b": one_b})
second = ak.zip({"a": two_a, "b": two_b})
cat = ak.concatenate([first, second], axis=0)
cat["another"] = three
for i in range(50):
cat["another", "w"] = three.x If you do fewer than 50 steps, you can examine this object. It takes a noticeably long time to show you its I don't see how GrowableBuffers could be involved. ArrayBuilder and GrowableBuffers aren't used in the broadcast-and-apply step in the |
I inserted import psutil
p = psutil.Process() at the top and print(i, p.memory_full_info().uss / 1024**2) in the loop and got
It doubles in every step. |
Just before the for loop,
type
and layout
The NumpyArray for >>> (
... cat.layout.contents[0].contents[2].contents[0].content
... is cat.layout.contents[1].contents[2].contents[0].content
... )
True None of that sounds wrong. |
Surrounding the call with Pympler summary tables unsurprisingly says that it's all in NumPy arrays:
|
Since all NumPy array creations must go through an nplike, here: awkward/src/awkward/_nplikes/array_module.py Lines 50 to 70 in 3317b0c
I added a line to raise an exception if the new array's length is greater than 1000 (because they grow exponentially). Here's the stack trace:
Going from bottom to top, awkward/src/awkward/contents/numpyarray.py Lines 506 to 524 in 3317b0c
which comes from the awkward/src/awkward/contents/indexedarray.py Lines 604 to 678 in 3317b0c
which are arrays in one of the slots of awkward/src/awkward/contents/recordarray.py Lines 675 to 745 in 3317b0c
which is one of the awkward/src/awkward/contents/unionarray.py Lines 252 to 371 in 3317b0c
I don't think the issue goes any higher. |
The exponential memory growth (not a memory leak) is not related to the internal cross-references (the fact that
the issue persists. Implicitly, this also rules out the IndexedArray part of the stack trace. I doubt it has anything to do with the NumpyArray or RecordArray parts, either. It's very likely an issue in |
Even if we use the above to project out the original IndexedArray, (
(See? The Then, running the for loop for 30 steps turns it into
The |
Projecting away all of the IndexedArrays, cat.layout.contents[0]._contents[0] = cat.layout.contents[0].contents[0].project()
cat.layout.contents[1]._contents[0] = cat.layout.contents[1].contents[0].project()
cat.layout.contents[0]._contents[1] = cat.layout.contents[0].contents[1].project()
cat.layout.contents[1]._contents[1] = cat.layout.contents[1].contents[1].project()
cat.layout.contents[0].contents[2]._contents[0] = cat.layout.contents[0].contents[2].contents[0].project()
cat.layout.contents[1].contents[2]._contents[0] = cat.layout.contents[1].contents[2].contents[0].project() and raising an exception the next time an IndexedArray is constructed provides this stack trace:
But that's not it—even with awkward/src/awkward/contents/unionarray.py Line 434 in 3317b0c
we get another IndexedArray creation with this stack trace:
And with awkward/src/awkward/contents/unionarray.py Line 705 in 3317b0c
we get
Turning off these two --- a/src/awkward/contents/unionarray.py
+++ b/src/awkward/contents/unionarray.py
@@ -431,7 +431,7 @@ class UnionArray(UnionMeta[Content], Content):
]
if len(contents) == 1:
- next = contents[0]._carry(index, True)
+ next = contents[0]._carry(index, False)
return next.copy(parameters=parameters_union(next._parameters, parameters))
else:
@@ -702,7 +702,7 @@ class UnionArray(UnionMeta[Content], Content):
nextcarry = ak.index.Index64(
tmpcarry.data[: lenout[0]], nplike=self._backend.index_nplike
)
- return self._contents[index]._carry(nextcarry, True)
+ return self._contents[index]._carry(nextcarry, False)
@staticmethod
def regular_index( is sufficient to stop the memory explosion. Turning them both off is also necessary: just one isn't enough to do it. So now I need to think. Should these be turned off? Well,
Maybe the solution should be highly targeted to this situation: identify and catch |
I decided to just set |
Version of Awkward Array
2.6.4
Description and code to reproduce
Hey!
To jump right in: I have recently switched from using awkward v1.10.3, to the newest version v2.6.4. I have, however, been running into some very weird issues when adding fields to union arrays.
So a bit of backstory:
I am working with electrons and muons, I then try and find dilepton pairs (e-e, mu-mu) and finally I am adding extra information about these dileptons (e.g. angle of dileptons to the missing energy of the event). For some reason, however, after switching to the newer version of awkward I am suddenly running into issues. Specifically, I am running into issues when I add the new fields to the dilepton array. For example, if I try and add new fields like this:
dileptons[("Dilepton", "mass")] = <Some array>
it will eventually crash from using too much memory, but this issue does not happen when I use awkward v1.10.3. Furthermore, even adding a single field without crashing seems to take longer in v2.6.4 than v1.10.3.
Here is a short snippet that hopefully highlights the issue that I am having. However, instead of having much larger arrays, to show what I mean I am using much smaller ones, and so I have to repeatedly apply the buggy line before the memory gets so big it crashes. In my actual code I am not just repeatedly trying to add the same field!
I should note that this doesn't crash if I just ran once:
It seems to be the creating of the field that retains something which causes memory to build up after repeated calls and then it crashes.
If I run the above code with the older awkward version then it doesn't crash.
If I do this in a jupyter notebook as well, I can't even run
because this ends up crashing before it finishes.
Finally, I should add why I specifically mentioned union arrays.
As you can see in the electron and muon fields, the electron array contains the field "WP90" which is not in muons, and the muon array contains the field "tightID" which is not in the electron array.
Therefore when you concatenate the ee_pairs and mm_pairs you get a union array:
If I now take out both the fields "WP90", "tightID" from the electron and muons arrays, respectively, I no longer get a union array (I guess because all the fields match):
Now if I run the code above, but with the "WP90", "tightID" removed, it no longer crashes! So I am guessing that it is something to do with the union bit? Also not sure exactly why it becomes a union because as far as I can tell the fields "WP90", "tightID" aren't even accessible through the dilepton array?
Not exactly sure what is going on here, but would be interesting to hear your thoughts!
Thanks!
The text was updated successfully, but these errors were encountered: