-
-
Notifications
You must be signed in to change notification settings - Fork 22
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
benchmarking infrastructure #24
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this!
I think the parameters for number of nodes and edges can be passed into the test function in a similar way you have done with algo_type
. The values are specified in params, and they get passed into the test function as input parameters.
I have a comment below too. :)
def time_betweenness_centrality(self, algo_type): | ||
num_nodes, edge_prob = 300, 0.5 | ||
G = nx.fast_gnp_random_graph(num_nodes, edge_prob, directed=False) | ||
if algo_type == "parallel": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we should also have a third algo_type, where we run it using a keyword instead of converting the graph to a parallel_graph. That is, the call would be something like:
- = nx.betweenness_centrality(G, backend=‘nx-parallel’)
I guess we better make sure that works first, and gives the same results. But it should work and it’d be nice to know if they are the same speed. Of course, if they are almost identical all the time, then we probably should remove it again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dschult
the output is same for both the ways(i.e. converting into a parallel graph and using keyword argument)when I round the betweenness_centrality value up to 2 decimal places, otherwise, it does give different values sometimes.
and yes, both the ways are almost identical in terms of time, but I've still added it in the comments here if someone wants to try it. There is some difference for 500 nodes graphs as observed in the following ss :
But this difference seem negligible when we bring sequential into the picture(as seen in the following ss) :
also, i have added num_nodes and edge_prob and changed normal
algo_type to sequential
in this new commit
Thank you :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, should I create a PR to change the plugin name from parallel
to nx-parallel
, if that's required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for those time comparisons. I'm glad it doesn't change the time much between using the backend by object type and by using it via a keyword arg.
I think the naming conventions for these backend libraries is supposed to be, e.g. nx-cugraph
when the plugin name is cugraph
. I will check with other people though.
The additional parameter graph_type
s you mention are already obtained from the graph object. So the backend gets that info via, e.g. G.is_directed()
.
Graph generators will need to be done either in networkx before calling the function, or in nx-parallel by writing a parallel version of that function. I believe the PR with the graph generators getting @dispatch decorators has been merged. The weight parameter should be passed through to the backend function. If it is None then we use an unweighted approach.
I haven't looked at the new commit yet, but hope to soon. :}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some big picture questions:
- should we use a class here? Does asv require it? It seems like we are really just setting up a function (sorry if I am just forgetting what asv needs here...)
- It seems like a separate module for each benchmark will become cumbersome once we have lots of timings to do. How should we organize the timings? Is there another scientific-python oriented repo that has an established directory structure for benchmarks that we could follow?
Replying to Current benchmarks directory's structure
Advantage of the above structure:
I have used classes just to keep it all organised.
I will see if there is a better way of organizing. And, please let me know what you think of the current way. Thank you :) |
@dschult I've made the suggested updates pls let me know this looks good to you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for that summary -- and description of the file/class/name arrangement.
I think there may be functions where we want more than one timing method for that function. But we can do that by adding descriptive words to the time_*
method names.
All this big picture stuff looks good to me. We should revisit it after implementing a few more and make sure it is working well. :)
====== get_graph vs setup and how it fits with timing_func
The current PR creates the graph as part of the function call being timed. So we will be timing both the graph construction and the algorithm (I think).
Let's try using a setup
method to create the graph followed by a time_<name>
method to call the function on that graph.
For timing_func
, there seems to be a lot of renaming of information that doesn't add much. But again, I could be missing the point so if I'm missing the point, let me know. What I am seeing is that for vitality we use names get_graph
, func=
, Vitality
, Benchmark
and none of them seem to provide much beyond what we could just do ourselves inline. The code is:
class Vitality(Benchmark):
params = [(algo_types), (num_nodes), (edge_prob)]
param_names = ["algo_type", "num_nodes", "edge_prob"]
def time_closeness_vitality(self, algo_type, num_nodes, edge_prob):
timing_func(
get_graph(num_nodes, edge_prob), algo_type, func=nx.closeness_vitality
while we could have:
class VitalityBenchmark:
params = [algo_types, num_nodes, edge_prob]
param_names = ["algo_type", "num_nodes", "edge_prob"]
def setup(self, algo_type, num_nodes, edge_prob):
return nx.fast_gnp_random_graph(num_nodes, edge_prob, seed=42, directed=False)
def time_closeness_vitality(self, G, algo_type, num_nodes, edge_prob):
return nx.closeness_vitality(G, backend=algo_type)
Then when reading the code, we don't need to go look up what is in timing_func
, and (for other functions) we could include parameters that don't get changed during this timing run.
Notice that I moved the graph creation outside of the timing method into the setup method. That's so we only compare the computation time, and not the construction time.
Notice that with this formulation, the algo_type would be a backend name (with None
representing the sequential networkx code). What do you think?
@dschult thank you for the detailed and insightful review! yeah, it's better if we have graph creation and timing functions separate. I guess I just did it to make the code more compact, thinking that the graph creation time will be included for both the About the base Also, I agree using Also, I think it will be better to make it Thank you :) |
Also, I think we should keep the |
I expect that we will need many different types of graphs for the algorithms. So the Does caching work well with floating point inputs like In general, caching is nice for time consuming parts that either take a lot of time, or get repeated many times. We’ll see as we get more examples whether the timing is taking a long time and how much of that time is constructing the graphs. Let’s leave it in for now. |
if the combination of arguments is the same then the function is not implemented and the graph is returned from the cache directory, it is independent of the type of the argument. |
Ahh.. But is 0.3 and 0.29999999999999 the same? How does the caching software determine that the values are the same? It looks like @functools.lru_cache
def f(x):
print(x)
f.cache_info()
# CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)
f(3/10)
# 0.3
f(1/10*3)
# 0.30000000000000004 (round-off)
f.cache_info()
# CacheInfo(hits=0, misses=2, maxsize=128, currsize=2) <-- 3/10 and 1/10*3 are cached as different results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I approve this PR.
Are there other issues to discuss as part of this PR?
We can always updater things in a separate PR.
I think it's ready. Just made a few changes in README :) |
issue #12
here I implemented a basic benchmarking infrastructure using ASV for comparing the
parallel
andnormal
implementations of thebetweenness_centrality
function. I couldn’t figure out how to run it for different number of nodes and edge probabilities, we would probably have to define our own benchmarking classes for that.The steps to run the benchmarks are in README.md (result ss) :
Please give your feedback to better it.
Thank you :)
References: ASV docs, networkx benchmarking, numpy benchmarking