Skip to content

Speed up distances with massive neutrinos by 6-7x - #3814

Closed
aconley wants to merge 3 commits into
astropy:masterfrom
aconley:faster_nu
Closed

aconley wants to merge 3 commits into
astropy:masterfrom
aconley:faster_nu

Conversation

@aconley

@aconley aconley commented Jun 3, 2015

Copy link
Copy Markdown
Contributor
  • Follow on to Speed up distance calculations by ~10-20x #3767, which is partially addressing slow comoving_distance in astropy.cosmology #3764
  • Add scalar function to compute massive neutrino densities in
    integrals to get speedup for arbitrary number of neutrinos.
    This buys about 20% speedup.
  • Add 1, 2, and 3 massive neutrino specializations for much larger
    speedups by avoiding the (surprisingly) expensive numpy.sum
    operations. These should be by far the most common cases.
  • Some unit tests added for the new scalar functions

Alex Conley added 2 commits June 3, 2015 10:56
* Follow on to astropy#3767, which is partially addressing astropy#3764
* Add scalar function to compute massive neutrino densities in
  integrals to get speedup for arbitrary number of neutrinos.
  This buys about 20% speedup.
* Add 1, 2, and 3 massive neutrino specializations for much larger
  speedups by avoiding the (surprisingly) expensive numpy.sum
  operations.  These should be by far the most common cases.
* Some unit tests added for the new scalar functions
@aconley

aconley commented Jun 3, 2015

Copy link
Copy Markdown
Contributor Author

Again, I was surprised by just how expensive some numpy functions are that I thought were cheap (like sum), so the solution to making it faster (without Cython) seems to be specialized scalar versions.

The other way to go about this -- which maybe would be better -- would be Cython. But I decided this was simpler for a first attempt.

@mhvk

mhvk commented Jun 4, 2015

Copy link
Copy Markdown
Contributor

@aconley - the three nearly-the-same functions seem a bit excessive. Is there really much of a speed gain between the 1-neutrino version and one that simply does a python
for nu in nus: loop? (where nus can contain neutrino species up to three.) Also, I think the python sum function is very fast.

@aconley

aconley commented Jun 4, 2015

Copy link
Copy Markdown
Contributor Author

@mhvk -- a straight python loop is about the same speed for 1 massive neutrino, but 10/20% slower for 2/3. Which maybe is close enough -- it's still a big speedup. I would probably prefer the single function approach, but the last pull taught me that other people seem to have a higher tolerance for code ugliness than I do, so I figured I'd start at full ugly for speed. (PS: nus can contain more than 3 neutrinos -- for the fans of sterile neutrinos out there).

As for python sum -- yeah, I thought so too. But that's why we measure! And it turns out we were both just wrong.

To be precise, for a 3 element sum, python sum is 3x slower than just doing arr[0] + arr[1] + arr[2], and numpy sum is 2x slower still (at least on my machine). And for a one element array (so, the Planck cosmologies) the unwrapped loop is 11x faster than python sum, and 30x faster than numpy sum!

@mhvk

mhvk commented Jun 4, 2015

Copy link
Copy Markdown
Contributor

@aconley - I think at the few tens of % level, it is not worth making the code way more ugly...

As for sum: the problem is partially that we have np.float rather than python float:

a = [1., 2., 3.]
%timeit a[0] + a[1] + a[2]
# 10000000 loops, best of 3: 132 ns per loop
%timeit sum(a)
# 1000000 loops, best of 3: 194 ns per loop
def loopsum(a):
    r = a[0]
    for e in a[1:]:
        r += e
    return e

%timeit loopsum(a)
# 1000000 loops, best of 3: 347 ns per loop

# Now using an `ndarray`.
a = np.array([1., 2., 3.])
%timeit a[0] + a[1] + a[2]
# 1000000 loops, best of 3: 745 ns per loop
%timeit sum(a) 
# 100000 loops, best of 3: 1.97 µs per loop
%timeit loopsum(a)
# 100000 loops, best of 3: 2.03 µs per loop

And np.sum is much worse, as you say.

@aconley

aconley commented Jun 4, 2015

Copy link
Copy Markdown
Contributor Author

Interesting! I'm starting to understand there's a lot more overhead in numpy than I had imagined.

But note that unrolling still seems to win by ~ 50%. And np.floats are what we have.

PS: you shouldn't write loopsum that way. The a[1:] is making a copy of the array, unlike numpy, which uses views.

def loopsum(a):
      r = 0.0
      for e in a:
         r += e
      return r

is about 30% faster.

@aconley

aconley commented Jun 4, 2015

Copy link
Copy Markdown
Contributor Author

@mhvk - alas, it appears I messed up while testing the pure python loop, and left part of the calculation out. The 3-headed version is more than 10-20% faster, it's more like 40-70%.
Lesson: make sure your speed hack test still passes all the unit tests!

astropy HEAD: 520ms
astropy HEAD with simple scalar z version: 490ms

3-headed version:
1 massive nu: 85.6ms (note: Planck case, which is likely to become the default cosmology)
2 massive nu: 108ms
3 massive nu: 128ms

python loop:
1 massive nu: 144ms
2 massive nu: 160ms
3 massive nu: 177ms

So, that's a 70% hit in the most common case, which isn't negligible.

Now, the relatively clean loop version is still a good 3.5x faster than HEAD -- but the ugly-ass version is 6x faster.

@aconley

aconley commented Jun 4, 2015

Copy link
Copy Markdown
Contributor Author

@mhvk -- ok, you'll like this. If I store a copy of the variable we need as a python list instead of a numpy array, then the pure python loop version is a) faster than the 3-headed version b) only weakly dependent on the number of neutrinos.

So -- ditch numpy, get simpler (as in, only one version) and faster code. Hrm.

@mhvk

mhvk commented Jun 4, 2015

Copy link
Copy Markdown
Contributor

@aconley - wow! Are you doing the equivalent of the following?

a = np.array([1., 2., 3.])
%timeit sum(a)
# 100000 loops, best of 3: 1.96 µs per loop
%timeit sum(a.tolist())
# 1000000 loops, best of 3: 354 ns per loop

@aconley

aconley commented Jun 4, 2015

Copy link
Copy Markdown
Contributor Author

Not quite... we do have to do some manipulation on the variable (_nu_y; it gets divided by 1+z and raised to various funny powers). So the idea is to also store an instance variable with _nu_y as a list (leaving a ndarray version for when people are actually calling on ndarray zs), and then use a python loop. There's still some ugliness -- you want a special case that is not in the thing being integrated for there being no massive nus -- but it's down to 2 additional private functions instead of 5. And the private functions are easier to understand.

This is making me question everything I thought I knew about numpy...

@astrofrog

Copy link
Copy Markdown
Member

Btw one should be careful with caching - for the examples @mhvk showed, I get warnings like The slowest run took 17.64 times longer than the fastest. This could mean that an intermediate result is being cached

@aconley

aconley commented Jun 4, 2015

Copy link
Copy Markdown
Contributor Author

@astrofrog -- Yeah, I know. That can also reflect something being compiled to bytecode. So you do it multiple times, and the warning goes away.

The timing tests I'm talking about are based on modifying core.py and reinstalling, then checking the distances are still the same, then doing %timeit. If I do the middle step, I no longer get that warning during the timing test.

…ndarray.

* Store FLRW._nu_y as a python list in addition to ndarray.
* Use the list version and a python loop in the scalar nu_relative_density
  calculation.  This turns out to be quite a bit faster than using
  ndarrays, contrary to 'popular' wisdom.  It also leads to much simpler
  code -- fewer special case functions -- than the previous versions of
  this pull.
* Speedup is now fairly uniformly 6-7x over the old calculation, and
  this works for >3 massive neutrino species.  In fact, this is faster
  than the previous 1, 2, 3 neutrino scalar special functions.
* All unit tests still pass.
@aconley aconley changed the title Speed up distances with massive neutrinos by 5-7x for 1, 2, 3 flavors Speed up distances with massive neutrinos by 6-7x Jun 4, 2015
@embray embray added this to the v1.1.0 milestone Jun 8, 2015
Comment thread astropy/cosmology/core.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting...

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.

Yeah, no kidding... not something I would have ever expected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually, not surprising at all now that I look at how this is used. When we're talking just (typically) 3 neutrino species, looping over a simple Python list (in Python) is going to involve far less overhead than looping (again, in Python) over a Numpy array.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In other words (and I tested this) if you had say Neff=1000 all with different masses, using a Numpy ufunc in _massivenu_relative_density_scalar was about 2 orders of magnitude faster. But when you get down to tiny arrays of 3 or 4 elements the overhead of setting up the ufunc and returning arrays overtakes.

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.

Yeah, that's what I concluded. But that was entirely after the fact justification -- before I tried it I definitely expected numpy to be faster even for 3 or 4 elements. So, hey, learned something new!

@aconley

aconley commented Sep 6, 2015

Copy link
Copy Markdown
Contributor Author

Apologies for letting this languish for so long; new job, blah, blah. Among other things I was trying to make sure I could legally contribute to astropy...

Anyways, I have now started on a cython version, which will probably make this pull obsolete. It's... a lot faster, although I haven't tried the neutrinos yet.

@aconley

aconley commented Sep 6, 2015

Copy link
Copy Markdown
Contributor Author

Replacement cython version is #4127

@embray

embray commented Sep 9, 2015

Copy link
Copy Markdown
Member

No need for apologies--it's cool you gave the Cython version a try.

@eteq

eteq commented Sep 24, 2015

Copy link
Copy Markdown
Member

#4127 replaces this, so I'm closing this one.

@eteq eteq closed this Sep 24, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants