-
Notifications
You must be signed in to change notification settings - Fork 89
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
Massive memory leak with awkward.Array #1127
Comments
Thanks for catching this, The expression a shared pointer with an action to release the array when the reference count goes to zero: When it gets attached to a Python object, the C++ I tried testing it, too, though I added a counter to see how many times an array is created: >>> import awkward as ak
>>> counter = 0
>>> while True:
... tmp = ak.Array([1])
... counter += 1
...
^CTraceback (most recent call last):
File "<stdin>", line 3, in <module>
KeyboardInterrupt
>>> counter
1140231 If it wasn't deleting that array, then we'd have 1140231 × 8 kB = 9 GB of memory leak, which isn't the case. Looking at While running the above Python code, I watched the memory go down with % while (true); do free -m | fgrep Mem: | awk '{print $4}' ; sleep 1; done Here's a plot: the horizontal axis is seconds; the vertical axis is megabytes (according to With garbage-collected languages, we have to be careful about interpreting this because the garbage collector will let the memory use grow up to a point and then invoke collection. Maybe we're just not seeing it reach that point—after all, only 300 MB have been used in this minute. (On my Linux box, a little more than the rate you saw on MacOS.) However, CPython only uses the garbage collector for the data that it can't eliminate via reference counting, which I had thought was anything with a cycle in it. I tried to construct some pure Python examples with cycles, but Python (3.9) is too smart: it figured out that the self-referential data was cyclic and deleting it without waiting for the garbage collector. So I might as well ask, does the garbage collector find this data, if it gets invoked? In Python, we can manually invoke it: >>> import awkward as ak
>>> import gc
>>> while True:
... tmp = ak.Array([1])
... tmp2 = gc.collect()
...
^CTraceback (most recent call last):
File "<stdin>", line 3, in <module>
KeyboardInterrupt The difference is striking: note the much smaller vertical axis: The memory usage is jumping around (not very high or low) because we're sampling memory usage at random times with respect to when the garbage collector is running, but there clearly isn't the 300 MB steady decline that we saw without the garbage collector. So one conclusion is that there's no permanent leak: when the garbage collector runs, the objects do get cleaned up. Why, with our non-cyclic data, is Python not cleaning them up right away? I'm not sure, but I had a hunch that it might be because the reference is going into a compiled extension. We can do the same sort of thing with NumPy, creating a reference cycle through NumPy arrays, which is also a compiled extension: >>> import numpy as np
>>> while True:
... tmp = np.array([None], dtype=object)
... tmp[0] = tmp
...
^CTraceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt Here, the memory loss is dramatic: whatever NumPy's doing is leaving a lot more data to be cleaned up only at garbage collection time. It uses up all the memory on my computer until it has only a few hundred MB left, then the garbage collector finally decides to get to work and the usage levels out. (The flat part at the bottom is before I stopped the loop.) Just for completeness, let's do the NumPy example with an explicit garbage collector invocation: >>> import numpy as np
>>> import gc
>>> while True:
... tmp = np.array([None], dtype=object)
... tmp[0] = tmp
... tmp2 = gc.collect()
...
^CTraceback (most recent call last):
File "<stdin>", line 4, in <module>
KeyboardInterrupt Indeed, it keeps it clean from the start: The bottom line is that this isn't a problem with Awkward Array; it's just how Python garbage collection works with data in extension libraries. You shouldn't be making millions of little There's another point that I could have raised at the beginning of all of this, though: we're working on Awkward 2.0, which removes the C++ layer in favor of Python that calls out to zero-memory-allocation C functions with ctypes, so even if there was a real memory leak here, it would be gone in the upcoming pure Python version. I could have led with that, though it feels like a cop-out: a memory leak in the present version would be bad news. Anyway, for these two reasons, this is not an issue that needs action. Thanks for your diligence in reporting it, though! |
Thanks for the awesome analysis. I was using ak.Array together with pytorch so at first I thought maybe the problem was there. In the end I walked around it by converting the arrays to regular lists with |
You can also work around it by calling But since you're developing an application, you know the exact speed-to-memory tradeoffs and can decide where you want the garbage collector to collect (in addition to the times it would decide to do so on its own). PyTorch also has compiled extensions, so the above may apply to it, too. If you're calling
Well, all garbage collected languages. CPython's hybrid of reference counting and garbage collection makes the garbage collection penalties less significant than, say, PyPy or the Java Virtual Machine. Instead, the cost is an extra 8 bytes in every object... |
I was also observing this effect, but I'm not fully convinced this can be explained or solved garbage collector invocation, which is why I'm replying to this rather old issue. You are calling import awkward as ak
import gc
i = 0
while True:
tmp = ak.Array([1])
i += 1
if i > 100000:
print("collect")
gc.collect()
i = 0 I get steady increase in memory usage, even though garbage collection is being done. |
I can reproduce growing memory for your trivial example. If I change the Array object to the |
@agoose77 - how long before see it? I'm running on master + PR #1560 for ~15 min. It looks like it v1 memory oscillates between 250 - 560 M: |
@ianna good question. I gave it a while, and observed memory growing from |
For me, it climbs past 1GB (5 minutes). I don't actually have a huge amount of spare memory right now with all of my open tasks, so I won't be able to let it run for any longer 😅 |
We've moved from C++ owning the data (ArrayBuilder based on So memory management for data created by ArrayBuilder ( |
Good news! PR #2311 apparently fixes this memory leak, too! I'm reopening this issue just so that it can be closed by the PR, for record-keeping. |
Version of Awkward Array
1.5.1
Python version
Python 3.6 and 3.8
OS
Ubuntu 18.04 and macOS Big Sur
Problem
Using
ak.Array
causes memory to be leaked. The following program causes the memory (RAM) consumption to grow indefinitelyyes, that is the whole program.
Rate of leakage
Memory increase can be tracked with utilities (e.g. htop) or from Python itself. Using
psutil
, memory usage in megabytes can be printed with the following codewhich (for me) shows a steady RAM usage increase at a rate of about 2 MB/s.
I briefly tried to find the cause for this with various memory-profiling tools but could not find the cause (I tried three tools and none of them even saw any change in object counts/sizes)
Expected behavior
I see no good reason for why this should be happening, I would expect
ak.Array
to cause no memory leakage.The text was updated successfully, but these errors were encountered: