Skip to content

Investigate SIMD support #377

Closed
Closed

Description

@pitdicker

Disclaimer: I don't really know what I am talking about here.

The first step, as far as Rand is concerned, is to generate a small array of values from an RNG. Some RNG's may lend itself well for this, like ChaCha, and possibly HC-128, although that one does not use SIMD. Other options are packing together multiple small RNGs, like Xorshift+ or Xoroshiro+. The result type of such an RNG should be some array, so maybe we can make use of the BlockRngCore trait?

Making use of the generated values can be a more interesting problem. This involves the distributions code.

Converting to floating point: the Standard uniform distribution should not cause any problems, and neither should the range code.

I suppose the other distributions are going to give trouble, at least anything that needs branches or a look-up table (i.e. the current ziggurat code) is not going to work well. And what should happen when an algorithm needs to do rejections sampling, and one of a couple of SIMD values get rejected? Return NaN? For most distributions there should exist an algorithm suitable for SIMD. The Box-Muller transform for example looks promising for the Normal distribution, although it would need a library to provide functions like sin and cos.

Integer ranges: I don't expect the widening multiply method to work for integer range reduction, and division or modulus also do not seem to be available. And I suppose we have no way to indicate a value is rejected because it falls outside the acceptable zone and can cause bias.

Now I don't know how far we should go, and if Rand should provide any implementations at all. But I think we should make sure the API does not prevent Rand from being used in this area.

Activity

  1. TheIronBorn commented on Apr 5, 2018

    @TheIronBorn
    Contributor
  2. TheIronBorn commented on Apr 5, 2018

    @TheIronBorn
    Contributor
  3. TheIronBorn commented on Apr 6, 2018

    @TheIronBorn
    Contributor

    Also of note: for multi-rngs we could use several sets of constants at once. More or less alternating between Xorshift64(10, 13, 10), Xorshift64(8, 9, 22), Xorshift64(2, 7, 3), and Xorshift64(23, 3, 24) for example. Might help raise statistical quality.

  4. pitdicker commented on Apr 6, 2018

    @pitdicker
    ContributorAuthor

    Great if you would want to help out a bit here!

    I believe @vks has also been exploring this, for example in https://github.com/vks/xoroshiro/blob/master/src/rng/xoroshiro128simd.rs

    I think this might give us some direction lemire/SIMDxorshift

    👍 I can't read those function calls yet, but he is doing two widening multplies, even and odd, and keeping the high parts? And without checking against low they will be biased, but this is not always a problem, especially if the range is small.

    Also of note: for multi-rngs we could use several sets of constants at once.

    We could, and that seems like a good idea. But just to make sure, that doesn't mean all four (/eight?) can use the same seed.

    @TheIronBorn Would you be interested in exploring what it would take to have a Distribution take the output of such an RNG, and transforming it wile staying in the SIMD area?

    Converting it to a float, or a floating point range are the simplest methods.

  5. TheIronBorn commented on Apr 6, 2018

    @TheIronBorn
    Contributor

    We could, and that seems like a good idea. But just to make sure, that doesn't mean all four (/eight?) can use the same seed.

    Are you asking if such an implementation would require each lane to have the same value upon seeding?

  6. TheIronBorn commented on Apr 6, 2018

    @TheIronBorn
    Contributor

    Float gen is pretty simple. Here's a quick gist https://gist.github.com/TheIronBorn/1ba4da07d6e8e05ab20cb780c383dbec

  7. TheIronBorn commented on Apr 6, 2018

    @TheIronBorn
    Contributor

    Added range sampling

  8. dhardy commented on Apr 6, 2018

    @dhardy
    Member

    Speeding up block RNGs with SIMD sounds reasonable; anything else sounds application-specific IMO.

    Distribution::sample(..) -> T so in theory the trait can support SIMD distributions, given native consumers of the SIMD types. I guess it should be possible to make adapter implementations, i.e. automatically implement Distribution<(f64, f64)> for any type supporting Distribution<f64x2>.

    For anything else, e.g. attempting to produce Normal samples in blocks for individual consumption, I guess something like the MutDistribution or CachedDistribution (i.e. equivalent to the old Sample trait) is needed to allow caching — but this significantly increases complexity and I'm not sure it would even be faster in many cases.

    One specific case might be worth a specific solution: StandardBlockRng<T>. It may be possible to make a wrapper around any BlockRngCore which immediately applies the Standard distribution to convert its results buffer to an array of type T, then draws values from this buffer. In theory this might allow some optimisation between the RNG (generation) and the distribution (conversion) steps, although probably whatever loop the RNG step requires to fill the buffer would prevent this. This has the added advantages of a common design with & without SIMD and that it can simply implement Iterator<Item=T>.

  9. pitdicker commented on Apr 6, 2018

    @pitdicker
    ContributorAuthor

    Thank you for code @TheIronBorn, something to look into. Can I also ask you to create a branch in your fork of rand with the changes? Or did you have another way set up to test things?

    Speeding up block RNGs with SIMD sounds reasonable; anything else sounds application-specific IMO.

    When I was exploring papers around RNGs and related stuff GPU processing was the current trend, and exploring the possibilities of SIMD also seems to come up regularly. As it is a topic we did not give any attention until know, at least good to explore a bit...

    But I have no idea yet how useful it will really be. Especially for some distributions it may be hard for SIMD to provide any benefit over the traditional methods.

    One thing that seems like an obvious problem is that Distribution takes an Rng, but Rng exposes no methods to give something like next_u32x4. I think a lot can be solved by adding another method and associated type to RngCore. This type would indicate the native value produced by this RNG. It is something I thought that would be useful for months, but never spoke about...

    So our current Xorshift implementation natively generates u32s, Xoroshiro128+ u64s, ChaCha [u32; 16], etc. With an addition like this is is possible to implement RNGs for specific types not covered by the trait. For example one specialized for u16 (maybe for embedded?), for SIMD with a type like next_u32x4, or one designed for floats with f64. I created an incomplete branch with changes. @dhardy Please don't shoot it down instantly...

  10. pitdicker commented on Apr 6, 2018

    @pitdicker
    ContributorAuthor

    For anything else, e.g. attempting to produce Normal samples in blocks for individual consumption, I guess something like the MutDistribution or CachedDistribution (i.e. equivalent to the old Sample trait) is needed to allow caching

    Why would we need caching? To return one value at a time instead of a SIMD type? Or did you write this because the smaller number of sampling algorithms may need some mutable state?

  11. dhardy commented on Apr 6, 2018

    @dhardy
    Member

    Caching would just be to return one value at a time, yes; the trouble with that approach is that there is significant overhead storing/retrieving the cached values.

    @pitdicker your branch looks interesting, though you could probably just do RngCore: Iterator instead. Also what would the OsRng implementation look like?

    The BlockRngCore::Results return values are probably going to be too big for many uses and there is no provided way of breaking the block up into smaller segments; this can be worked around however by choosing an RNG with Item type of the correct size.

    There is a significant reason I don't want to add an associated type to RngCore: it is no longer object-safe without specifying the type (the same problem we hit in #307). It might however make sense to use a completely separate trait, possibly just Iterator, and then just implement SIMD distributions as simple functions usable via Iterator::map — this is not well integrated however.

    But I hold to my previous point: other than a few cases where SIMD can fit into the existing API (e.g. ChaCha core) I see little point trying to develop the infrastructure without at least one (preferably several) application able to consume SIMD values.

  12. TheIronBorn commented on Apr 6, 2018

    @TheIronBorn
    Contributor
  13. TheIronBorn commented on Apr 7, 2018

    @TheIronBorn
    Contributor

    Blending sets of constants for Xorshift seems to only optimize well for machines later than somewhere around Haswell: https://godbolt.org/g/G2x1E2. (Adapted from O'Neill's Xorshift library) I don't have such a machine so I can't confirm actual speed.

    Removing the multiply helps.

  14. pitdicker commented on Apr 7, 2018

    @pitdicker
    ContributorAuthor

    There is a significant reason I don't want to add an associated type to RngCore: it is no longer object-safe without specifying the type

    I realized just after posting. So it was a bad idea from me, feel free to shoot it down 😄.

    Here's my branch TheIronBorn/rand:src/simd@simd

    Nice, I have been playing with it. And SFC seems a pretty good choice here.

    I think I have found a way to support SIMD without extra traits like SimdRng and SimdDistribution. The idea is that RngCore::fill_bytes does a simple copy of its result (like u32x4) to the byte slice if they have exactly the same length. And the integer distributions for Standard do the opposite: they initialize an empty u32x4, and pass it to fill_bytes as a byte slice. The optimizer is able to work things out.

    I don't think the BlockRng wrapper is going to work well in combination with a simple RNG adapted for SIMD. I expect the bookkeeping to take as much time as just generating a new batch of values. So we may just as well always generate a new batch, and use as much as needed.

    I have a proof of concept in https://github.com/pitdicker/rand/commits/simd_support. The code in sfc.rs is very bad and ugly. I am not sure using BlockRngCore in there is useful. But the trick with using fill_bytes seems to work.

    But I hold to my previous point: other than a few cases where SIMD can fit into the existing API (e.g. ChaCha core) I see little point trying to develop the infrastructure without at least one (preferably several) application able to consume SIMD values.

    I agree we shouldn't turn everything upside down only to support SIMD, especially when we are not sure how useful it will be.

    Does implementing Standard and Range for SIMD types fall for you in the category 'develop infrastructure' or 'extension of existing functionality'? Without tests it takes about 125 extra loc: pitdicker/rand@c03deb6...pitdicker:6918e11. The first commit is a necessary change in rand_core to get the inlining right for fill_bytes, and I think that is something that should be in rand_core 0.1. The rest is definitely not interesting until after rand 0.5.

  15. dhardy commented on Apr 8, 2018

    @dhardy
    Member

    Nice, I didn't expect fill_bytes to work so well!

    Implementing existing traits/distributions is fine — well, we have to look at how much code gets added, but probably fine.

  16. 43 remaining items

  17. dhardy commented on Jul 1, 2018

    @dhardy
    Member

    You improved test performance just by shuffling the output? Interesting. I wonder if this just makes correlations harder to spot though? If PractRand is still seeing all the same output then literally the only change is the byte order. I think many of the tests are engineered to look for certain types of pattern, and some of those may not show up with the extra shuffle.

    On the other hand I think shuffling the state could significantly improve the quality (cycle length could in theory be increased by the power of the number of streams), though in effect this produces a totally different generator, hence there could be problems and it might be preferable to use different constants. I'm not an expert on this; it would be much better to have input from someone who actually designs PRNGs.

  18. dhardy commented on Jul 1, 2018

    @dhardy
    Member

    Nice work @TheIronBorn assembling that collection of SIMD generators.

    What's XSM? A pseudo-LCG?

    It would be interesting to include 96-bit / 128-bit LCGs. Basically half of PCG, with a bit better performance:

    test lcg32x2                 ... bench:       3,152 ns/iter (+/- 60) = 2598 MB/s
    test lcg32x4                 ... bench:       3,161 ns/iter (+/- 64) = 5183 MB/s
    test lcg32x8                 ... bench:       3,841 ns/iter (+/- 150) = 8531 MB/s
    

    I'm not quite sure where this is going, but publishing some of these at least as part of Rand would be nice. I guess we should get back to #431.

  19. TheIronBorn commented on Jul 1, 2018

    @TheIronBorn
    Contributor

    Could you make that LCG code available? Perhaps just a pull request on https://github.com/TheIronBorn/simd_prngs.

    And here’s @pitdicker’s evaluation of XSM dhardy#60 (comment)

  20. pitdicker commented on Jul 1, 2018

    @pitdicker
    ContributorAuthor

    What's XSM? A pseudo-LCG?

    dhardy#60 (comment)

  21. TheIronBorn commented on Jul 1, 2018

    @TheIronBorn
    Contributor

    I managed to get xorshift128+_x4 to 4GB by swizzling the full state before the regular xorshift, but it's got 512 bits of state so that isn't too impressive

  22. dhardy commented on Jul 2, 2018

    @dhardy
    Member

    No, that's not impressive. It seems to me that multiplication or even addition by a constant would cause better bit-avalanche than bit-shift-and-XOR operations, but then Xorshift stays away from these for performance.

    I'm not sure whether it's worth continuing down this path. It's essentially building a new PRNG using only automated tests for evaluation. But I'll have a go at building a mixing LCG/MCG if you like.

  23. TheIronBorn commented on Jul 2, 2018

    @TheIronBorn
    Contributor

    Intel has a mixing LCG here https://software.intel.com/en-us/articles/fast-random-number-generator-on-the-intel-pentiumr-4-processor/. (Perhaps I'm reading it incorrectly and it's not actually mixing state)

    My quick testing doesn't indicate it's very fast and they don't give many details on its quality.

  24. TheIronBorn commented on Jul 2, 2018

    @TheIronBorn
    Contributor

    I was only looking into the state mixing to try to find some way of achieving

    ideally there would be some mixing between the different parts of state

    Probably not the best approach to test mixing with a poorer quality generator like xorshift128+. Also we still won't have the math guarantees of the period/etc of the mixed generator.

  25. dhardy commented on Jul 2, 2018

    @dhardy
    Member

    I was just experimenting with a generator based on this. Unfortunately performance is abysmal using simd_shuffle16 etc (2-3GB/s) which means it's probably not worth further investigation.

  26. dhardy commented on Jul 2, 2018

    @dhardy
    Member

    In all honesty, I think it might be time to close this issue. Lets recap.

    #523 landed support for SIMD types on the Standard and Uniform distributions (thanks especially to @pitdicker and @TheIronBorn!).

    There has been some discussion of supporting SIMD types for other distributions, notably Normal and the Ziggurat algorithm, and possible alternatives (Central Limit Theorem).

    There has been some discussion on adapting the RngCore type, though for now it looks like the current API might be sufficient.

    There has been a lot of discussion on parallel/SIMD RNG implementations and @TheIronBorn has assembled a collection of SIMD generators. I remain sceptical about simply bolting together multiple small RNGs since there are several existing PRNGs with 128-bit output at least, and larger generators tend to have better quality than smaller ones, although for some applications multiple small generators may be faster and have sufficient quality.


    At this point I think it makes sense to create smaller, more topical discussions on issues of continued interest.

    I also suggest that when considering SIMD PRNGs we should try benchmarking with something more complex than micro-benchmarks where possible. @gnzlbg's Ambient Occlusion ray casting benchmark may be useful for this (though multiple applications would be preferred).

    In fact, it may be worth opening an issue just to talk about benchmarking (SIMD as well as scalar PRNGs).

  27. gnzlbg commented on Jul 2, 2018

    @gnzlbg

    FYI the purpose of aobench is to compare Rust's code generation with that of ISPC, so I will keep the benchmark algorithmically/data-structure-wise as close as possible to the benchmark implementation in ISPC (see the volta subdirectory).

    However, ISPC only provides one PRNG, and if we can beat it conveniently in Rust, that benchmark might be a good place to try that out and even report it. Just keep in mind what the original intent of the benchmark is, and if you submit a PR with a better PRNGs, put them behind a feature flag so that we can still use the benchmark in the lolbench suite to track Rust run-time performance regressions.

  28. added a commit that references this issue on Jul 5, 2018
  29. added this to the 0.6 release milestone on Aug 23, 2018
  30. martinothamar commented on Jul 26, 2023

    @martinothamar

    Hi! This topic is interesting to me, I have this pet project where I'm using the monte carlo method to simulate football matches. I've used this project as an excuse to learn more about SIMD and different topics. I also wanted to learn Rust so naturally I ended up here. I noticed that the current rand SIMD feature doesn't really vectorize random number generation, so I've ended up implementing some vectorized PRNGs here: https://github.com/martinothamar/simd-rand

    I have some portable implementations which are using nightly portable SIMD, and some that are using SIMD intrinsics directly from std::arch. The fastest one is Xoshiro256+ for AVX512 which has reached ~50 GiB/s on my laptop when generating u64x8 vectors.

    I thought I'd mention it here in case there is any interest in this still

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions