You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Every uproot.dask call increases memory footprint by 30 MB (it's in dask.base.function_cache) #1093
This particular instance leaks ~30MB per open. This adds up very quickly if you need to extract the form of hundreds of files in a remote process as evident from scikit-hep/coffea#1007 where this bug manifested pretty nastily.
>>>importgc>>>importpympler.tracker>>>importuproot>>>>>>summary_tracker=pympler.tracker.SummaryTracker()
>>>>>># run it once to get past the necessary first-time things (filling uproot.classes, etc.)>>>lazy=uproot.dask(
... {"https://github.com/CoffeaTeam/coffea/raw/master/tests/samples/nano_dy.root": "Events"}
... )
>>>dellazy>>>gc.collect()
0>>># run print_diff enough times to get to the quiescent state>>>summary_tracker.print_diff()
...
>>>summary_tracker.print_diff()
types|# objects | total size=======|===========|============>>>>>># what does an Uproot Dask array bring in?>>>lazy=uproot.dask(
... {"https://github.com/CoffeaTeam/coffea/raw/master/tests/samples/nano_dy.root": "Events"}
... )
>>>summary_tracker.print_diff()
types|# objects | total size=================================================|===========|============dict|72059|11.54MBbytes|3|5.66MBlist|24007|1.73MBnumpy.int64|37491|1.14MBuproot.source.cursor.Cursor|21000|984.38KBnumpy.ndarray|4501|492.30KBstr|4886|436.10KBuproot.models.TObject.Model_TObject|5999|281.20KBtuple|3385|147.00KBuproot.models.TObjArray.Model_TObjArray|3000|140.62KBuproot.models.TNamed.Model_TNamed|2999|140.58KBfrozenset|1|128.21KBint|3492|95.51KBawkward._nplikes.typetracer.TypeTracerArray|1878|88.03KBuproot.models.TTree.Model_ROOT_3a3a_TIOFeatures|1500|70.31KB>>>>>># what goes away when we delete it?>>>dellazy>>>gc.collect()
14>>>gc.collect()
0>>>summary_tracker.print_diff()
types|# objects | total size==============================================|===========|============code|0|37Baiohttp.helpers.TimerContext|-1|-48Bawkward.contents.recordarray.RecordArray|-1|-48Bdask.highlevelgraph.HighLevelGraph|-1|-48Bdask_awkward.utils.LazyInputsDict|-1|-48Bdask.blockwise.BlockwiseDepDict|-1|-48Bawkward.highlevel.Array|-1|-48Bdask_awkward.layers.layers.AwkwardInputLayer|-1|-48Bdask_awkward.lib.core.Array|-1|-48Basyncio.trsock.TransportSocket|-2|-80Bssl.SSLObject|-2|-96Baiohttp.streams.StreamReader|-2|-96Basyncio.sslproto.SSLProtocol|-2|-96Basyncio.sslproto._SSLPipe|-2|-96Bbytearray|-2|-112B
Hardly anything goes away when lazy is deleted! That's not good!
This TTree has 1499 TBranches. So having approximately that many TIOFeatures, TypeTracerArray, twice that many Model_TNamed, Model_TObjArray (TBranch and TLeaf), and four times as many Model_TObject make sense.
There are only 3 bytes objects, but they comprise 5.66 MB. I don't know, offhand, what they could be, but I think they're more likely Uproot than Dask. There are a lot of big dicts, which is not too surprising, and I can't say offhand whether I expect more in Uproot or more in Dask.
The one, major problem is that del lazy followed by gc.collect() does not get rid of as many objects as were brought in. It may be reasonable for the dask-awkward array of a large TTree to be 30 MB, but it's not reasonable for it to still be around after deleting.
Who gets a reference to it and doesn't let go? It might be possible to find out with gc.get_referrers, but it might not be at the level of lazy (the ak.Array and dak.Array are listed as objects that go away). Let me think about that...
I'll be looking at lists and one reference will be the list I'm using to look at it, so I make that a special class that's easier to ignore in a print-out of type names.
In the Pympler output, we saw that the TypeTracerArrays were not deleted when lazy went out of scope (and gc.collect() was called). So this is a good starting point to walk outward and find out who's holding a reference to it.
Now I'll just walk along the graph of its referrers, ignoring the IgnoreMeList because that's the follow list itself, and seeing what else is in the list.
Nope. All of this is either Python's infrastructure or the infrastructure I set up in the __main__ namespace.
So what gives? I didn't see any other referrers along the way. Who's holding a reference to this object? If nobody is, why isn't it deleted (why is it not negative in the Pympler list) when the ak.Array that holds it is deleted?
If it's referenced in dask.base.function_cache, I would have thought that gc.get_referrers would have shown us that. Also, clearing this function cache doesn't show the allocated data getting removed in Pympler:
>>>importgc>>>importpympler.tracker>>>importdask.base>>>importuproot>>>>>>summary_tracker=pympler.tracker.SummaryTracker()
>>>>>>lazy=uproot.dask(
... {"https://github.com/CoffeaTeam/coffea/raw/master/tests/samples/nano_dy.root": "Events"}
... )
>>>dellazy>>>gc.collect()
3>>>gc.collect()
0>>>summary_tracker.print_diff()
# ... several times ... #>>>summary_tracker.print_diff()
types|# objects | total size=======|===========|============>>>lazy=uproot.dask(
... {"https://github.com/CoffeaTeam/coffea/raw/master/tests/samples/nano_dy.root": "Events"}
... )
>>>summary_tracker.print_diff()
types|# objects | total size=============================================|===========|============dict|72060|11.54MBbytes|3|5.66MBlist|24007|1.73MBnumpy.int64|37491|1.14MBuproot.source.cursor.Cursor|21000|984.38KBnumpy.ndarray|4501|492.30KBstr|4886|436.10KBuproot.models.TObject.Model_TObject|5999|281.20KBtuple|3385|147.00KBuproot.models.TObjArray.Model_TObjArray|3000|140.62KBuproot.models.TNamed.Model_TNamed|2999|140.58KBfrozenset|1|128.21KBint|3490|95.45KBawkward._nplikes.typetracer.TypeTracerArray|1878|88.03KBuproot.models.TAtt.Model_TAttFill_v2|1500|70.31KB>>>dellazy>>>dask.base.function_cache.clear() # clearing Dask's function cache>>>gc.collect()
221296>>>gc.collect()
0>>>summary_tracker.print_diff()
types|# objects | total size==============================================|===========|============code|0|37Bawkward.highlevel.Array|-1|-48Bdask.blockwise.BlockwiseDepDict|-1|-48Baiohttp.helpers.TimerContext|-1|-48Bawkward.contents.recordarray.RecordArray|-1|-48Bdask_awkward.layers.layers.AwkwardInputLayer|-1|-48Bdask.highlevelgraph.HighLevelGraph|-1|-48Bdask_awkward.lib.core.Array|-1|-48Bdask_awkward.utils.LazyInputsDict|-1|-48Basyncio.trsock.TransportSocket|-2|-80Basyncio.sslproto._SSLPipe|-2|-96Bfsspec.caching.BytesCache|-2|-96Bssl.SSLObject|-2|-96Buproot._dask.TrivialFormMappingInfo|-2|-96Buproot.models.TTree.Model_TTree_v20|-2|-96B
It's still the case that creating lazy adds 30 MB of TTree metadata objects and deleting it and the functions from the function cache doesn't make them appear with a minus sign in Pympler.
So that is what's holding all of the memory. It must be some connection that Python doesn't see—maybe it goes through a reference in an extension module? (Maybe it goes through a NumPy object array? numpy/numpy#6581)
Since this is a Dask feature, what do we want to do about it? @lgray, would it be sufficient to have Coffea clear the Dask function cache?
One thing I noticed in function_cache is that it's holding the uncompressed pickle of the function and the dask.base.tokenize for the function key changes with every uproot.dask open.
That's probably why we don't see a connection to lazy above.
That's it then! That's why it costs memory, but can't be seen as objects of the expected types.
So, in the end, the recommendation for everyone is to check their Dask function cache. I'll convert this into a Discussion as a way for others to find this conclusion.
Actually, it could—possibly—be fixed in Uproot by replacing the TTree metadata data structure with a streamlined data structure containing only that which is necessary to fetch arrays. (Mostly the fBasketSeek, etc.)
i.e. something like 0.7 MiB for this file, but larger if it had more baskets. It's likely that I'm forgetting some other essential metadata, which would bring this figure up.
changed the title [-]uproot.dask leaks memory on every open[/-][+]Every `uproot.dask` call increases memory footprint by 30 MB (it's in `dask.base.function_cache`)[/+]on Jan 24, 2024
Activity
jpivarski commentedon Jan 23, 2024
This is true, and the memory use in
uproot.daskis over and aboveuproot.open(and getting the TTree metadata).reports
Change the
tasktoso that it leaks file handles, and it's
We'd eventually run out of file handles this way, but apparently not memory (on the MB scale).
Now
This is a problem. (Also, it's noticeably slower, though there might be good reasons for that.)
Using Pympler,
Hardly anything goes away when
lazyis deleted! That's not good!This TTree has 1499 TBranches. So having approximately that many TIOFeatures, TypeTracerArray, twice that many Model_TNamed, Model_TObjArray (TBranch and TLeaf), and four times as many Model_TObject make sense.
There are only 3 bytes objects, but they comprise 5.66 MB. I don't know, offhand, what they could be, but I think they're more likely Uproot than Dask. There are a lot of big dicts, which is not too surprising, and I can't say offhand whether I expect more in Uproot or more in Dask.
The one, major problem is that
del lazyfollowed bygc.collect()does not get rid of as many objects as were brought in. It may be reasonable for the dask-awkward array of a large TTree to be 30 MB, but it's not reasonable for it to still be around after deleting.Who gets a reference to it and doesn't let go? It might be possible to find out with
gc.get_referrers, but it might not be at the level oflazy(theak.Arrayanddak.Arrayare listed as objects that go away). Let me think about that...jpivarski commentedon Jan 23, 2024
Okay, setting up to follow this object with
gc.get_referrers,I'll be looking at lists and one reference will be the list I'm using to look at it, so I make that a special class that's easier to ignore in a print-out of type names.
In the Pympler output, we saw that the TypeTracerArrays were not deleted when
lazywent out of scope (andgc.collect()was called). So this is a good starting point to walk outward and find out who's holding a reference to it.Now I'll just walk along the graph of its referrers, ignoring the
IgnoreMeListbecause that's thefollowlist itself, and seeing what else is in the list.Okay! The dict and the module are just
__main__:So what about the function?
Nope. All of this is either Python's infrastructure or the infrastructure I set up in the
__main__namespace.So what gives? I didn't see any other referrers along the way. Who's holding a reference to this object? If nobody is, why isn't it deleted (why is it not negative in the Pympler list) when the
ak.Arraythat holds it is deleted?Does anyone have any ideas?
jpivarski commentedon Jan 23, 2024
Even more to the point, following the advice of https://stackoverflow.com/a/28406001/1623645
The TypeTracerArray goes away. I don't know why this disagrees with Pympler (and the fact that 30 MB of USS doesn't go away).
agoose77 commentedon Jan 24, 2024
It looks to me like this might be in dask. Add the following to the loop body:
I notice that the total memory usage remains fairly stable.
jpivarski commentedon Jan 24, 2024
If it's referenced in
dask.base.function_cache, I would have thought thatgc.get_referrerswould have shown us that. Also, clearing this function cache doesn't show the allocated data getting removed in Pympler:It's still the case that creating
lazyadds 30 MB of TTree metadata objects and deleting it and the functions from the function cache doesn't make them appear with a minus sign in Pympler.Oh, but the total USS memory usage does go down:
results in
whereas removing the
function_cache.clear()results inSo that is what's holding all of the memory. It must be some connection that Python doesn't see—maybe it goes through a reference in an extension module? (Maybe it goes through a NumPy object array? numpy/numpy#6581)
Since this is a Dask feature, what do we want to do about it? @lgray, would it be sufficient to have Coffea clear the Dask function cache?
lgray commentedon Jan 24, 2024
That'll certainly fix it for coffea.
One thing I noticed in function_cache is that it's holding the uncompressed
pickleof the function and the dask.base.tokenize for the function key changes with every uproot.dask open.That's probably why we don't see a connection to
lazyabove.jpivarski commentedon Jan 24, 2024
That's it then! That's why it costs memory, but can't be seen as objects of the expected types.
So, in the end, the recommendation for everyone is to check their Dask function cache. I'll convert this into a Discussion as a way for others to find this conclusion.
Actually, it could—possibly—be fixed in Uproot by replacing the TTree metadata data structure with a streamlined data structure containing only that which is necessary to fetch arrays. (Mostly the
fBasketSeek, etc.)i.e. something like 0.7 MiB for this file, but larger if it had more baskets. It's likely that I'm forgetting some other essential metadata, which would bring this figure up.
[-]uproot.dask leaks memory on every open[/-][+]Every `uproot.dask` call increases memory footprint by 30 MB (it's in `dask.base.function_cache`)[/+]