Skip to content
This repository was archived by the owner on Feb 26, 2025. It is now read-only.

Implement queries mechanism for edges#129

Merged
asanin-epfl merged 3 commits into
masterfrom
query-edges
Mar 4, 2021
Merged

Implement queries mechanism for edges#129
asanin-epfl merged 3 commits into
masterfrom
query-edges

Conversation

@asanin-epfl

Copy link
Copy Markdown
Contributor

No description provided.

@asanin-epfl

Copy link
Copy Markdown
Contributor Author

Probable more tests are required. @tomdele Should we rather merge tests of queries from nodes and edges into one place?

Comment thread bluepysnap/edges.py Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. 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.

  2. I don't want to introduce Dask until we are sure that libsonata will not support it for 100%.

@tomdele tomdele Mar 2, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--mem=0 worked for this 1.6Tb case but it took 1109.78255582 seconds.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I did not expect insane perfs anyway.
Maybe we will be able to improve afterwards with multi processing.

@asanin-epfl asanin-epfl Mar 2, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread bluepysnap/edges.py Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use numpy array with IDS_DTYPE instead. I wrote the snippet with a list but I forgot we return arrays ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you manage to contain the memory consumption with the chunks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Comment thread bluepysnap/edges.py
Comment on lines +510 to +513
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think we could have better perfs using smaller but more chunks and using multiprocessing (not dask just multiprocess)?

@asanin-epfl asanin-epfl Mar 3, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum good points ... Ok we can keep it like this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread bluepysnap/edges.py
raise BluepySnapError(f"Unknown edge properties: {unknown_props}")
res = []
ids = self.ids(None)
chunk_size = int(1e8)

@tomdele tomdele Mar 3, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to detect the amount of available RAM by cleaver usage?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I was writing it here :
#129 (comment)

What do you think ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don t have a super strong opinion. I just hate when my pc freeze when it was not clearly expected.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

@asanin-epfl asanin-epfl Mar 4, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@tomdele tomdele Mar 4, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh ok. Sorry that s all good then !

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much. It was very helpful and useful.

@asanin-epfl
asanin-epfl merged commit 36b522e into master Mar 4, 2021
@asanin-epfl
asanin-epfl deleted the query-edges branch March 4, 2021 09:22
@tomdele

tomdele commented Mar 4, 2021

Copy link
Copy Markdown
Contributor

Thanks a lot for this is really nice !

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants