Implement queries mechanism for edges#129
Conversation
|
Probable more tests are required. @tomdele Should we rather merge tests of queries from nodes and edges into one place? |
| unknown_props = properties - self.property_names | ||
| if raise_missing_prop and unknown_props: | ||
| raise BluepySnapError(f"Unknown edge properties: {unknown_props}") | ||
| data = self.get(None, properties - unknown_props) |
There was a problem hiding this comment.
I am quite scared this cannot be done. I allowed he None for the get because user is in full control but I am sure for huge circuit this will not be possible... Did you try ?
There was a problem hiding this comment.
I am quite scared this cannot be done.
Should I understand this as "I am quite scared that this can be done"?
I allowed he None
Should I understand this as "I allowed the None"?
Did you try ?
No but I agree that this would highly likely fail/stuck on edges of Tb size with many properties.
-
In general this should work when there are only several properties within the query. All group attributes in edges files are int or floats. Lets assume float64 => 8 bytes. Lets say we have 1e9 (billion) edges in one population. One property then would take 8Gb. I doubt that edges of that size will be processed on a desktop computer. So, we have a high performant server hardware. Even for 10 properties it will take 80Gb of Ram which is not much for server hardware.
-
I don't want to introduce Dask until we are sure that libsonata will not support it for 100%.
There was a problem hiding this comment.
I was not clear enough.
What I meant is:
Back in the days, the edges "queries" : get, afferent_nodes, efferent_nodes forced as entry a list of edge ids. Then I chose to allow the value None instead of a list of edge ids. And so I allowed : use all the edge_ids.
I did that because this is not hidden to the user and he clearly needs to call it:
Ex :
edge.get(None)
And the None is not a default value so a "bare" get() will raise.
Using the queries I am a bit scared because this is a bit more hidden and unclear for the user if he fills his memory.
I totally agree with the hardware but maybe we can do it a bit differently without dask.
What about doing the property queries by chunk ?
Something like :
res = []
ids = self.ids(None)
critical_size = 60000000 # can be something computed using the dtypes of the properties
for chunk in np.array_split(ids, len(ids) // critical_size):
data = self.get(chunk, properties - unknown_props)
res.extend(query.resolve_ids(data, self.name, queries))
?
doing so you reduce a lot the memory consumption because you don t load everything in memory at the same time and I guess with the change I made in #128 you should not have problems with the positional queries.
There was a problem hiding this comment.
Great idea with chunking, I like it. Let see how it behaves. The current approach failed for 1.6Tb files (9.12 billion edges) with option --mem=100g and a single property. Around 74Gb is needed for single property in this case. The error was expected: Out Of Memory.
There was a problem hiding this comment.
Thanks a lot for the check !
We will need to find a cleaver way of doing this so it can be usable ... I hope chunking can help.
There was a problem hiding this comment.
--mem=0 worked for this 1.6Tb case but it took 1109.78255582 seconds.
There was a problem hiding this comment.
yeah I did not expect insane perfs anyway.
Maybe we will be able to improve afterwards with multi processing.
There was a problem hiding this comment.
ok, chunking is working and helps. It worked for 1.6Tb with --mem=200gb. Ran actually faster for 799.776455279999 seconds. --mem=100g is still not enough. In case when query is too broad and basically includes all of edges with multiple properties then chunking won't help.
| for chunk in np.array_split(ids, 1 + len(ids) // chunk_size): | ||
| data = self.get(chunk, properties - unknown_props) | ||
| res.extend(chunk[query.resolve_ids(data, self.name, queries)]) | ||
| return res |
There was a problem hiding this comment.
We should use numpy array with IDS_DTYPE instead. I wrote the snippet with a list but I forgot we return arrays ...
There was a problem hiding this comment.
btw wdyt about
res = []
ids = self.ids(None)
chunk_size = int(1e8)
for chunk in np.array_split(ids, 1 + len(ids) // chunk_size):
data = self.get(chunk, properties - unknown_props)
res.append(chunk[query.resolve_ids(data, self.name, queries)])
return np.concatenate(res, dtype=IDS_DTYPE)
There was a problem hiding this comment.
This needs to be benchmarked. I did this housand times but keep forgetting about it. What s the best between what you did and concatenate at every chunk :
res = np.empty(dtype=IDS_DTYPE)
ids = self.ids(None)
chunk_size = int(1e8)
for chunk in np.array_split(ids, 1 + len(ids) // chunk_size):
data = self.get(chunk, properties - unknown_props)
res.concatenate([res, chunk[query.resolve_ids(data, self.name, queries)]])
return res
(or something equivalent I did not run this piece of code just writing it live) ?
There was a problem hiding this comment.
Did you manage to contain the memory consumption with the chunks?
There was a problem hiding this comment.
Did you manage to contain the memory consumption with the chunks?
Well, it requires less memory for sure. 1.6Tb edges can be processed with --mem=200g and chunked version. The non-chunked version can process only with --mem=0 which is around 500g depending on the machine.
There was a problem hiding this comment.
This needs to be benchmarked.
Not much difference in terms of memory. Used the below benchmark
pip install memory_profiler
from memory_profiler import profile
import numpy as np
np.random.seed(666)
@profile
def benchmark_append():
res = []
for i in range(10):
start = np.random.randint(-10000, 10000)
end = np.random.randint(100000, 1000000)
res.append([i for i in range(start, end)])
return np.concatenate(res)
@profile
def benchmark_extend():
res = []
for i in range(10):
start = np.random.randint(-10000, 10000)
end = np.random.randint(100000, 1000000)
res.extend([i for i in range(start, end)])
return np.array(res)
if __name__ == '__main__':
benchmark_append()
benchmark_extend()| for chunk in np.array_split(ids, 1 + len(ids) // chunk_size): | ||
| data = self.get(chunk, properties - unknown_props) | ||
| res.extend(chunk[query.resolve_ids(data, self.name, queries)]) | ||
| return np.array(res, dtype=IDS_DTYPE) |
There was a problem hiding this comment.
do you think we could have better perfs using smaller but more chunks and using multiprocessing (not dask just multiprocess)?
There was a problem hiding this comment.
I don't know. The problem here is the layer of libsonata between us and HDF5. If we go directly via h5py then such optimization would work. Otherwise, I am not sure.
Another thought is that such optimization can be a bottleneck. Bluepysnap is tool to access circuits. It will be used a lot in projects that already are parallelized. It is highly likely that there will be no available proc cores for multiprocessing. So all this setting up of multiprocessing will be unnecessary and make things slower in the end.
There was a problem hiding this comment.
Hum good points ... Ok we can keep it like this.
There was a problem hiding this comment.
It is not necessary to merge it. We can abandon. I've implemented rather for history so we can have this discussion. If you still have some doubts, lets postpone or even abandon.
There was a problem hiding this comment.
No I think this is good to have this feature and we can improve it once it is here already.
My only concern is to crash people pc when they are doing something they don t intend to. So the best would be to have something that can evaluate the size of the returned property dataframe in the function and compare it to the size of the user memory. So we can throw and clean memory before the ram + swap are full.
| raise BluepySnapError(f"Unknown edge properties: {unknown_props}") | ||
| res = [] | ||
| ids = self.ids(None) | ||
| chunk_size = int(1e8) |
There was a problem hiding this comment.
We can keep it like this for the moment but do you think we can keep it close to ~10Gb of memory usage in the future with a cleaver use of the chunk_size ?
There was a problem hiding this comment.
Do you mean to detect the amount of available RAM by cleaver usage?
There was a problem hiding this comment.
yes I was writing it here :
#129 (comment)
What do you think ?
There was a problem hiding this comment.
I don t have a super strong opinion. I just hate when my pc freeze when it was not clearly expected.
There was a problem hiding this comment.
Please confirm
- detect available RAM memory
- detect size of edges file
- raise an exception if RAM is too low
- otherwise calculate chunk size, and start processing
There was a problem hiding this comment.
Thanks for the check.
I must say I am not sure what we can do to avoid filling memory at the moment. So let s merge it like this and we will see.
Maybe reducing the chunk size to 1e7 ?
There was a problem hiding this comment.
ok but I want to emphasize again that if there is not enough memory => we fail at libsonata usage before reaching this code. Basically when we in def _edge_ids_by_filter, we already have enough memory.
Maybe reducing the chunk size to 1e7 ?
1e8 of bytes is 100Mb. Are you sure to reduce it to 10Mb?
There was a problem hiding this comment.
ahhh ok. Sorry that s all good then !
There was a problem hiding this comment.
Thank you so much. It was very helpful and useful.
|
Thanks a lot for this is really nice ! |
No description provided.