-
Notifications
You must be signed in to change notification settings - Fork 223
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
[MRG] Efficient synapse creation #600
Conversation
I created a new file First, take the condition and break it into disjunctive normal form. That is, an expression like Now, we are thinking about iterating over j and computing the set of i matching the condition. We do this by serially finding all the i matching each of the subexpressions in turn, and then taking the union of these. So, in the example above we'd first find all i matching Now consider just one of these subexpressions, it will be of the form We analyse each atom to see if it is of a standard form (or can be transformed into a standard form). The standard forms are inequalities on i, e.g. Now when we generate the set of i for the subexpression, we first compute So far I've implemented this and written a comparison script equivalent to what the numpy codegen target would do (but not integrated it with the rest of Brian). Here are some timings (for len(source)==len(target)==N, where N goes up to 10k in some examples, 100k in others): 1% connectivity. Better for N>1000, around 2.5x better for N=10k. 0.1% connectivity. Better for N>1000, around 8x better for N=10k. 10% connectivity. Uniformly worse. Connectivity where abs(i-j)<10. A bit better for N=10k but 20x better for N=100k.
A complex expression, almost equal at low N and around 3x better for large N=10k. I think these timing differences will be much bigger for e.g. C++ standalone where the overheads will be much lower. Still, even here it shows that in almost all cases for N>1000 it's considerably better to use this optimised version, with timings for N>=1000 varying from a low point of 2x slower to a high point of 20x faster. |
Oh, and this branch is based off cython performance, that's why there are so many commits. As of this comment, the only relevant commit is the last one. I'd be interested in feedback on this approach (timings, code, etc.). I think it's promising enough to be worth including, and not too complicated (around 200 lines of code). Oh, it might also be interesting to have an example of the generated numpy code for a particular expression (
|
For the case of |
I am still not convinced that the analysis of expressions is the right approach to follow... I am worried that this will end up as another huge chunk of complex code and in the end for quite a few examples it might actually slow things down since we take a long time to do calculations like finding bounds in sympy, but in the end the bounds are 0 and N-1 or something like that (we could switch the whole system off for small numbers of synapses but then this would be a completely arbitrary heuristic). Also, it will still not allow us to do some essential things like generating a random number of targets per source. Wouldn't allowing the user to be more explicit be more the Brian way[TM]? E.g. instead of writing The whole process of coming up with a consensus and a working solution that we have sufficient confidence in for something more complex might take too long for 2.0 -- a short term solution would be to just document it (i.e. tell people about the inefficiency for |
I think it's worth doing this for 2.0, otherwise there's no efficient way of doing So here are some suggestions for syntax:
What I want to achieve with the syntax for the standard connect is to make it clear that these conditions are in addition to the condition. Not sure if they achieve that. For the Well, that's just a few ideas, I suspect we can come up with something better than any of that. |
Yes, I think coming up with a good syntax is the hardest challenge... But note that for the probability we already have the |
OK, so syntax suggestion:
In general, condition could be any string. This would cover a lot of what we talked about, including both our ideas about what the syntax should be, and I think would cover a lot of relevant use cases. What do you think? I think it's worth doing and the syntax above may be good enough or nearly so. |
Ok great, this is getting really close to what I had in mind. I think I'd prefer a fixed name (and probably not |
I think step is important for a connectivity pattern that is not uncommon, namely if you have a 2D array of neurons (indexed in 1D). I am OK with |
Some feedback on the proposed syntax: def connect(self, pre_or_cond, post=None , ...) we should rather use def connect(self, condition=None, i=None, j=None) I think this would be clearer by forcing you to use keyword arguments when you specify synapses directly: syn.connect(i=[1, 2, 3], j=[3, 2, 1]) But this is a minor point. I think we can all agree on the syntax for just restricting the range of the targets (and optionally of the source as well, maybe) relatively quickly. Just one thing came up: it is natural to define the range inclusively (as you did above), but the For the syntax where you specify the targets with a running variable, Romain had the idea of introducing some new string syntax for that variable. Something along the lines of syn.connect(j='i+k', foreach='k in [-3, 3]')
syn.connect(j='i+k', foreach='k in [-3, 4[')
syn.connect(j='i+k', foreach='k in {-3, 0, 3}') I have to admit there is something to it, even though I am not sure it is worth the additional complication. I think the trickiest thing syntax-wise is to include the |
I agree!
Good point, but how about we just keep it as range but follow Python semantics? For the explicit connections, I had thought about something along those lines too but decided that we are generally trying to avoid introducing new syntax in strings. How about
The nice thing about this is that it is actually valid Python (albeit in a string). It would be much more restricted than Python obviously but allows us to expand its possibilities in the future if we wanted to, and is anyway nicely self-descriptive. Generally speaking, I prefer to stick to Python semantics, but we could go with intervals as an alternative, in which case we should use a consistent syntax based around intervals (e.g. One last option, how about we go even more full on mimicking Python generator syntax:
Not sure how I feel about this one, but just putting it out there. |
OK, the last commit implements a first go at doing binomial synapse creation for the numpy target. I don't think it's maximally efficient yet, it will probably break some tests because I didn't think carefully about all the possible conditions, and it relies on the sklearn package which we probably want to avoid for the future, but it does make quite a speed up. Here are the results for the old synapse creation method (O(N^2)): And for the new method: The graph to look at is the total time because it runs for a very short duration. As you can see, for the old version, B2 is much slower at large N than B1 for all codegen targets. However, with the new numpy method, they're about the same. This will need some work to make it fully general and also fit in all the other changes, but I think it shows that it doesn't need to be too much work to implement these ideas. |
I did not yet have time to look at this in detail, but just about the syntax question:
The problem with that is that it feels somewhat unnatural, e.g. don't you think that many users would fall into the trap of writing: S.connect(condition='(i-100)%3==0', j_range=('i-100', 'i+100'), p=0.01) instead of S.connect(condition='(i-100)%3==0', j_range=('i-100', 'i+101'), p=0.01) ? I am a bit undecided about the full generator syntax: on the one hand I think it is great, it is readable and has clear semantics and you can quickly try it out in real code to see whether it does what you want it to do. On the other hand, we already have the problem that users think that abstract code is Python code and start using all kind of Python features in it (say, if clauses or array indexing) -- I worry that this would happen more often with this generator syntax. I agree with coming up with a consistent syntax, of course. |
Yeah, you're right about range. Well, I guess I'm also very unsure about the generator syntax. As you say, it has a lot of nice properties but also may lead to confusion. I guess if we're very clear in our error messages then maybe it's OK? Another advantage of it is that by using Python syntax we're allowing ourselves lots of room to add new features in the future (we just expand which bits of Python syntax we support). |
[ci skip]
One more datapoint. Parsing the generator syntax is super easy:
gives output
Handling all the error conditions will of course add a bit more to this, but the point is that this is not going to lead to complex parsing code. |
OK, so now I'm getting more and more convinced that the generator syntax is the best. The reason is that by its very nature, this problem requires a fairly deep syntax to express all the possible options and combinations of options. If we stick to simple keywords, we will have very many of them or overload them a lot, which will be very verbose and confusing, and require you to look up what they mean. On the other hand, the generator syntax is, as you said, completely clear and unambiguous. It's also very flexible: it can do everything we want to do and leaves loads of space for new stuff later. If we go down this route, I'd leave open a couple of simple options too. So, the full syntax would be:
|
Actually, even with the full error checking it's not very long. See |
I am still a bit unsure about this... Parsing certainly isn't the problem, as your script shows. What I am more worried about is that we are introducing another form of code strings with subtle differences to both our standard abstract code and to Python code. The similarity to Python code already leads to confusion sometimes ("why can't I use k for k in fixed_sample(0, N_post, 20) if sqrt((x_pre - x_post)**2 + (y_pre - y_post)**2) < 200*umetre Here, we don't know how to get Hmm, this needs some further thought... |
Good point about indexing. I don't see any way that we could express "Have each cell connect to 20 random cells in its vicinity (say distance < 200 um)" in any of the schemes we discussed so far though, that's a new use case. As far as I can tell, there's no way to do it efficiently: you'd have to compute all the points satisfying the distance condition first (i.e. O(N^2)) and then do random selections. I think it might be worth considering this kind of use case in the future, but maybe not for 2.0? For the case of picking some number of random targets and then discarding the ones that don't satisfy the distance condition I think your example should work fine. For me, the semantics of that would be as follows. Since you're writing |
I think it could be done efficiently, but it would mean that the sampling function would have to know about the condition. Something like
Seems to make sense indeed, I don't know why I was confused... I think I'd be happy with a (for now, fairly restrictive) generator syntax. One more point to consider: how would the new functions ( |
Interesting. I think in that case, whether or not it can be done efficiently depends on the scaling. If for a given i there are an approximately fixed fraction k (independent of N) of neurons j that satisfy the condition, then if you want to find a random selection of F such neurons then your method would be, I think, O(FN/k)=O(N). However, if given i there is an upper bound K (independent of N) neurons j satisfying the condition then this gives k=K/N so the method would be O(FN^2/K)=O(N^2). For the case of a spatial locality condition I guess this second case is more likely (since increasing the number of neurons probably means simulating a larger area rather than increasing the density of neurons in the area), but indeed there may be some situations where the former situation happens too. Actually there is an efficient way of doing it in the bad case too using a spatial subdivision algorithm, but that's very specific to the case of spatial locality conditions. I think the functions Do you want to run this by @romainbrette? |
I think it is a tricky problem in the general case -- a simple "re-draw if condition is not fulfilled" would work great for a condition that is almost always true (say, Ok, about the functions, that's what I thought. It would be kind of nice if the system were extensible but that's something we can always add later.
I think for that it would be a nice thing to have a wiki page that has everything in it, i.e. a list of the syntax options after the change (from the user point of view) -- would you mind creating such a page? |
OK, done: https://github.com/brian-team/brian2/wiki/Synapse-creation Do you think that's a fair representation of the options so far? Feel free to edit if not. |
That might be a good idea actually, it could make the formulation of many connection schemes easier if you don't have to worry about the borders. |
OK, code, tests, documentation and modified examples done for |
Nice, this makes complicated expressions like the Gaussian example quite a bit clearer. |
Oh, good spot! |
the test you wrote was actually failing for numpy ;) |
[ci skip]
….connect` This will make things clearer for users using the outdated way of connecting with indices (e.g. `syn.connect([0,1], [1, 2])`)
…for `i` and `j` in `Synapses.connect`
I fixed some small last issues, otherwise I am fine with everything. All tests and examples work on my machine. After waiting for the test runs to finish (which will unfortunately still take quite some time), this should be finally ready to merge! This will be a major new feature, worthy of a point release by its own :) |
Good news! It passes the statistical tests for I'm now happy for this to be merged once tests pass. I did a long and standalone run on my computer and it was fine. Do you want to do the same on linux? |
Thanks, it is looking fine on my machine as well, I had one "Overall fail" once, but nothing that seemed to indicate a systematic problem. I hacked together a quick solution to run the test in standalone (compiling it once and then re-running the main file which will always start with a new seed) and it passed as well.
I had already run this, so all should be good. The tests are not shown as passing on the github issue because the last commit has |
Standalone passed? Wow! I'm impressed. :) Anyway, great that this is finally done! What a relief. |
This branch is for work on issue #65.