Skip to content
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] Float32 support #981

Merged
merged 33 commits into from
Aug 29, 2018
Merged

[MRG] Float32 support #981

merged 33 commits into from
Aug 29, 2018

Conversation

mstimberg
Copy link
Member

Due to summer vacations this will still take a while to merge I think, but I'm putting it out here since it is basically ready from my side and it should be of high interest to the @brian-team/brian2cuda and @brian-team/brian2genn backends.

There are a few tests (mostly the multicompartmental stuff) that test numerical results vs. theoretical predictions that are just off too far with single precision, so I decided to simply skip them when single precision is used. In the long run, we should also probably split these type of tests from the more basic unit/integration tests.

Travis and appveyor run tests with single precision (but only for Python 2.7, the test suits already take way too long...) and everything seems to work fine.

A remark about the clock: The variables t and dt always use double precision regardless of the preference setting, because everything gets very imprecise otherwise. To avoid problems, other variables or subexpressions that are direct functions of time (e.g. lastspike) are using the same dtype.

…loating point variables and lower required accuracy in some cases
# Conflicts:
#	brian2/tests/test_functions.py
#	brian2/tests/test_refractory.py
#	brian2/tests/test_subgroup.py
#	brian2/tests/test_timedarray.py
@denisalevi
Copy link
Member

Hi @mstimberg. Does brian2genn already work out of the box with this brian2 preference by any chance? :) or would it be possible to add it for our comparisons (I imagine it to be easy to translate the brian pref to genn pref?)

@mstimberg
Copy link
Member Author

No, it doesn't work with Brian2GeNN (it's not even merged in Brian 2 yet :) ). In principle, it should be easy to translate, but I see potential problems with variables like lastupdate that we force to be double precision in Brian.

Copy link
Member

@thesamovar thesamovar left a comment

Choose a reason for hiding this comment

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

This looks good to me except for the doctest issue which would be good to address if we can?

I'm actually amazed by how few changes were needed to the actual code (not to the tests) to make this work. Great work!

@@ -14,16 +14,14 @@ def dtype_repr(dtype):


def default_float_dtype_validator(dtype):
return dtype is float64
return dtype in [float32, float64]
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need it as a priority, but I wonder how much work it would be to allow for other types such as float16 or float128?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think we'd need any additional changes for that, it's just more effort for testing. But we could certainly think about it again when a use case arises.

@@ -237,16 +237,18 @@ def values(self, var):
Examples
--------
>>> from brian2 import *
>>> G = NeuronGroup(2, """dv/dt = 100*Hz : 1
>>> G = NeuronGroup(2, """dv/dt = 100.0001*Hz : 1
Copy link
Member

Choose a reason for hiding this comment

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

This is really ugly because it's actually in the documentation. Is there no way around this?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll have a look, we can certainly come up with a different kind of example.

Copy link
Member

Choose a reason for hiding this comment

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

There are quite a few examples like this (not just this one).

array([ 0.5, 0.5, 0.5, 0.5])
>>> v_values[1]
array([ 1., 1.])
>>> np.set_printoptions(precision=4) # show fewer digits than default
Copy link
Member

Choose a reason for hiding this comment

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

Same here. I hate to have stuff like this actually in our docs.

assert not all(trace_conventional.v[0]==trace_GSL.v[0]), \
('output of GSL stateupdater is exactly the same as Brians stateupdater (unlikely to be right)')
# assert not all(trace_conventional.v[0]==trace_GSL.v[0]), \
# ('output of GSL stateupdater is exactly the same as Brians stateupdater (unlikely to be right)')
Copy link
Member

Choose a reason for hiding this comment

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

Why is this commented out?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, right. The test was a bit weird, checking that the results of two state updaters for the same equations were not exactly the same. It was more a "during-development" test, Charlee used it to make sure that the simulation was actually using her state updater. I think this test failed with float32, i.e. the results were exactly the same. We can simply delete it.

@@ -654,7 +655,7 @@ def test_threshold_reset():
threshold='v > 1', reset='v=0.5')
G.v = np.array([0, 1, 2])
run(defaultclock.dt)
assert_equal(G.v[:], np.array([0, 1, 0.5]))
assert_allclose(G.v[:], np.array([0, 1, 0.5]))
Copy link
Member

Choose a reason for hiding this comment

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

Comment not about this particular instance, but I wonder if we lose anything by weakening our tests like this? My feeling is probably not, but it makes me mildly anxious. I guess you've already pretty much covered this with the float-type dependent version of allclose.

Copy link
Member Author

Choose a reason for hiding this comment

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

In general I think we need to do a better job separating our tests for basic functionality from the tests for mathematical/numerical correctness. Currently, we only have a few tests (e.g. for STDP) that compare the exact results for non-trivial simulations.

Copy link
Member

Choose a reason for hiding this comment

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

Worth creating an issue for? I'm not entirely sure it's necessary, I feel like allclose is probably as good as equal practically. Can we think of an example where it might be a problem that it passes allclose but not equal?

@thesamovar
Copy link
Member

Oh, by the way do the CI tests now run for float32 and float64?

@mstimberg
Copy link
Member Author

Oh, by the way do the CI tests now run for float32 and float64?

They do, or almost: we are testing float32 on all codegen targets and on all platforms, but only for Python 2 (our test suite is already huge as it is...)

@thesamovar
Copy link
Member

If I just run_nose_tests.py locally will I get float64? How can I run with float32 locally?

@mstimberg
Copy link
Member Author

If I just run_nose_tests.py locally will I get float64? How can I run with float32 locally?

By default it uses float64, to get float32 you'll have to pass float_dtype=np.float32 to brian2.test

@mstimberg
Copy link
Member Author

Sorry, I have to leave... Can you check that you have the line I linked in my previous comment? Just before running the "codegen-independent" tests it sets set_device('runtime'), therefore I don't understand why it tries to run the tests with standalone.

@thesamovar
Copy link
Member

That line is there, yep. Might need a bit of help debugging this one. Will be around tomorrow afternoon.

@mstimberg
Copy link
Member Author

For debugging this, can you see whether you see the same problem when you run the run_nose_tests_long_and_standalone.py but replace:

success = [brian2.test(long_tests=True, test_standalone='cpp_standalone', float_dtype=np.float32),
           brian2.test(long_tests=True, test_standalone='cpp_standalone', float_dtype=np.float64)]

by

success = [brian2.test([], test_standalone='cpp_standalone', float_dtype=np.float32),
           brian2.test([], test_standalone='cpp_standalone', float_dtype=np.float64)]

If yes, that should at least make the debugging much faster!

@tnowotny
Copy link

About the GeNN question: We allow variables to be declared as "scalar x" and then x gets the type that was selected with the preference setPrecision(GENN_FLOAT) etc. Any type that is explicitly set to float or double will remain what it is set to. At least this is the intended behaviour. I won't vouch for these mechanisms to have undergone thorough testing.

@mstimberg
Copy link
Member Author

Thanks for clearing this up @tnowotny . I don't see us using scalar x anywhere in the Brian2GeNN code, so I think it should work just fine even without us setting the precision. Still, it makes sense to set it to avoid confusion.

@thesamovar
Copy link
Member

Yep that works! Will open brian dev gitter if you want to debug together.

@mstimberg
Copy link
Member Author

Will open brian dev gitter if you want to debug together.

I'm in the gitter chat (not sure whether it's the right one :) )

@tnowotny
Copy link

However, I just remembered a couple of other uses of the setPrecision information:

  • we replace literal constants with their appropriate single precision float versions in model code, e.g. 3.1415 becomes 3.1415f
  • we replace math function names with explicit precision variants, e.g. sin() becomes sinf()
  • the global variable t that handles time (even though as derived from a long integer variable iT that does the actual counting) is defined according to the current precision.

All of these make it a good idea to set the precision.

Copy link
Member

@thesamovar thesamovar left a comment

Choose a reason for hiding this comment

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

Tests now pass!

@mstimberg
Copy link
Member Author

Tests now pass!

Same here, I'll merge it right away even though the appveyor tests did not pass yet (this will take > 5h still...)

@mstimberg
Copy link
Member Author

the global variable t that handles time (even though as derived from a long integer variable iT that does the actual counting) is defined according to the current precision.

This is a problem, unfortunately, since in Brian t is always stored as a double variable... I don't think we will change this, since t is so ubiqutous in user expressions and with single precision, stuff like mon.v[mon.t>=1*ms] will often not do what the user thinks it should do (since 1*ms is a double). Even just for display, things get ugly:

>>> Quantity(1e-3, dtype=np.float32, dim=second.dim)
1.00000005 * msecond

I see two solutions:

  1. always use GENN_DOUBLE as the precision for GeNN -- individual variables will still have the dtype that Brian defines
  2. fix Brian2GeNN so that it can work with GeNN's single precision t

(1) is trivial to do, but of course we lose most of the performance benefits when every function call etc. still uses double precision.

I'll look into (2), maybe it is not that difficult to fix. We can still raise a warning to make users aware that Brian2GeNN is not calculating exactly the same thing as Brian.

@tnowotny
Copy link

tnowotny commented Aug 29, 2018

I can see the logic of using double precision for t. We made the "clean" float version originally for older GPUs which did not support doubles at all. In that case we had to use float for everything. I suspect today that is pure history - opinions @neworderofjamie?
I think solution (1) would be quite counter-productive even though I am also not sure how the different compilers handle calls to sin(x) where x is a single precision float. Do they auto-cast x to double or resolve to a float precision sin function "C++-style"?

@neworderofjamie
Copy link

neworderofjamie commented Aug 29, 2018

Woah this is prescient! In the past (genn-team/genn#183) we talked about abandoning floating point time altogether and I started implementing that. However it totally breaks backwards compatibility and ends up making the model code really ugly so I was thinking that adding an option to make time double precision is probably a pragmatic solution - single precision t gets totally mangled after running my STDP model for 2000s 😢

@mstimberg
Copy link
Member Author

I think solution (1) would be quite counter-productive even though I am also not sure how the different compilers handle calls to sin(x) where x is a single precision float. Do they auto-cast x to double or resolve to a float precision sin function "C++-style"?

C++11 (which I think is required by GeNN?) has overloaded math functions for single precision.

@neworderofjamie
Copy link

Sadly I think the CUDA maths functions are C-style - as in the float ones have an f at the end rather than relying on overloading

@tnowotny
Copy link

hum ... @neworderofjamie, how were you thinking to make a double precision time version? We could introduce a new GeNN flag for it and then just make t double precision based on it - something like that?

@tnowotny
Copy link

Brian2GeNN then could just set that GeNN flag and of we would go ... I am probably missing something.

@mstimberg
Copy link
Member Author

This sounds good to me. But a general question: is there a way to find out about the GeNN version "at runtime"? Even a simple thing like a version.txt file in the GeNN directory would be enough. This way we could check from Brian2GeNN what GeNN version is used and only set an option if it is supported, warn the user otherwise, etc.

@neworderofjamie
Copy link

neworderofjamie commented Aug 29, 2018 via email

@tnowotny
Copy link

@neworderofjamie, that is very simple to do. I guess we would rely on the compiler to auto-cast whenever t is used in some simulation code, but may not be the worst thing.
I guess we should do it.
@mstimberg about the version: I don't think we have anything in place but sounds like a really sensible thing to have.

@denisalevi
Copy link
Member

Sadly I think the CUDA maths functions are C-style - as in the float ones have an f at the end rather than relying on overloading

As far as I know, CUDA math functions are overloaded for single and double precision. At least in CUDA 9.0 (no idea about pervious versions). See here. Even though there is only an example for log given. Correct me if I'm wrong.

@tnowotny
Copy link

Looks like it in the documentation you referenced. In that case we could relax with our ensureFtype logic and not modify function names for single precision (at least from CUDA 9 onwards).
Independently, this would make @mstimberg's option 1) viable as well (at least from CUDA 9 onwards).

@neworderofjamie
Copy link

Sadly, for reasons best known to NVIDIA, that seems to only apply to log - see https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH__SINGLE.html#group__CUDA__MATH__SINGLE

@neworderofjamie
Copy link

Actually, apologies, that is just terrible documentation! In maths_functions.hpp there's lots of the following:

__host__ __device__ __cudart_builtin__ float     sin(float in)           { return sinf(in); }

I will check older CUDA's and see if this is consistent - I would IMAGINE it came in when CUDA started supporting C++.

@mstimberg
Copy link
Member Author

I don't think that's the case, their documentation is just... not that good. The log function is just an example (on the page you linked, there's also logf, and the double precision docs also pretend that log is double-only).

It seems that this has been in CUDA for a long time, I've found "Introduction to CUDA" slides from 2011 that mention that math functions are overloaded.

@mstimberg
Copy link
Member Author

Let's continue discussions about float32 support in Brian2GeNN here: brian-team/brian2genn#69

@mstimberg mstimberg merged commit b6a3a6c into master Aug 29, 2018
@mstimberg mstimberg deleted the float32_support branch August 29, 2018 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants