Breaking News

Why Rust fails hard at scientific computing

Why Rust fails hard at scientific computing

1.5 years ago I started a computer go bot in Rust based on Monte Carlo Tree Search (MCTS).

MCTS is at the heart of all strong go programs, and many AI for various games and real world competitions like RoboCup Soccer. Yes, even Google AlphaGo’s neural networks are just “suggesting” moves to the MCTS, it has the last words.

After weeks of fighting the borrow checker like many beginners I managed to program my way out, and produce this and brain dump material probably worth a PhD or two (check the README):

  • Compilation
  • Vectorization
  • Localization / Branch Prediction / Cache
  • Randomization / MCTS optimization
  • Data structure research
  • Machine Learning
  • Algorithms
  • Consumption (of trees and iterators/vectors)
  • Parallelism
  • Hashing
  • Heuristics
  • Memory
  • Unit tests

6 months ago, I found the time to dive into Data Science and Deep Learning, and 1 week ago I got the urge to write my own neural network library. Rust didn’t even enter my mind at the time, it had to be Nim.

4 Nim bugs later … After breaking a (Guiness ?) record of 5 bugs in 12 hours to a core language tracker.

… and a discussion with a fellow data scientist, I still think it’s the best language that fits my needs. Those bugs are only flesh wounds.

 

 

Let’s go back to Rust

Rust appealed to me due to speed, type safety and functional programming facilities. Why ? well, my first real programming language after bash, SQL and Excel VBA was Haskell, yep before even Javascript and Python.

So why did it fails for me, and why is it still failing for scientific computing:

 

1. Too much symbols & <> :: {} (Your mileage may vary, C++ programmers will feel right at home)

I’m not even talking about Rc, RefCell and Box which seems like security through obscurity. (Though it can’t reach Haskell monadic level)

 

2. Arrays in Rust are a second-class citizens, actually I think they don’t even have their visas. I hear them laughing at me when I try to use them. You can’t even clone them:

Actually I misrepresented, you can, only if the array size is 32 or less.

Consequences ? You can’t use Rust arrays to represent a matrix bigger than 4×8, how useful is that?

Actually you can’t even represent a 8×8 chessboard without coding every properties from scratch (copy, clone, print, indexing with [] …). I’m in luck, go has 9×9, 13×13 and 19×19 board sizes …

You can work around it by using a Vec (arbitrary sized sequence/list) but then your matrix is allocated on the heap not the stack, meaning slower operations. Plus that means you cannot use Rust wonderful type system to check that you multiply matrices with compatible dimensions, say a 2×2 matrix with a 2×1 matrix, without jumping through hoops.

That brings me to the third point.

 

3. Rust is still “discussing” integer as generic type parameter (since 2015), meaning a matrix type Matrix[M, N, float] will not exist before a long long time. The following github discussions are quite the read:

That’s it folks, hope you enjoyed the read.

PS: Would “3 reasons why Rust fails hard at scientific computing” be too much baitclick ?


Also published on Medium.


Tags assigned to this article:
nimprogrammingrust

Related Articles

High performance tensor library in Nim

Toward a (smoking !) high performance tensor library in Nim Forewords In April I started Arraymancer, yet another tensor library™,

  • 1) Well, we are talking about a modern systems language, and not a scripting language, or a garbage-collected language. The symbols are a requirement, as how else would you explicitly convey that information to the compiler? I’d honestly like to see even more symbols, because there aren’t that many currently in use.

    2) Seems like you misunderstand what an array is. If you’re coming from a higher-level language, you don’t have access to arrays. You have access to vectors, and that’s what the Vec type is. Arrays are fixed-sized, and must be known at compile time. It’s the same story with all other systems languages, because that’s how arrays work.

    And actually, you can express any 2/3/4D set with arrays:

    let array = [[0u8; 1024]; 1024];

    There’s a 1024 by 1024 array. If you don’t know what size you need, you can just do:

    let map = Vec::new();
    map.push(Vec::new());

    Then you have a Vec<Vec>, which is 2-dimensional.

    3) You must not be aware of the following:

    https://docs.rs/nalgebra/
    https://docs.rs/num/

    • Mamy Ratsimbazafy

      Hi Michael, thanks for taking the time for posting

      1) I think we have to agree to disagree, the whole scientific community already as a lot of symbols to learn and having to learn even more to program is detrimental. It is possible to convey information to the compiler in a way that is far less noisy than C++ or Rust in a statically typed language like what is done in Nim or Haskell.

      2) I understand perfectly what an array is, I actually used Rust to program a go playing bot 2 years ago. Go board are fixed size, 19×19, link here: and then due to this huge issue in Rust, I have to implement Clone (link), Index (link), IndexMut (link) because you can’t derive clone/index/Indexmut. You might say, 30 lines, easy no? Well except that Go can be played on 9×9, 11×11, 13×13, 15×15 and 17×17 too, any oddxodd size, no way I’m implementing those 30 lines x 6 times, this is not fun

      3) Even though I say that Rust fails at Scientific computing I follow very closely what each ndarray and machine learning library writer is doing, in go, rust, closure, ocaml, java … For example in my area of interest, Deep Learning, the Rust ecosystem already has end-to-end Deep Learning frameworks with OpenCL, CPU and Cuda backend: Leaf and its forks Parenchyma and Juice.

      Now Rust in scientific computing has 3 problems.
      1) Today, the core of scientific computing library is in C, C++ or Fortran, this legacy code is huge, the cost of rewriting it is big, writing new code in Rust is possible though provided it has good support for GPU computing.

      2) Scientists need to do fast experimentations on top of that, today that means a scripting language like Python or Matlab or Julia

      3) Scientists (not software devs) identify a bottleneck in their experimentations and rewrite it to C/Cython.

      Rust only address point 1. Scientists won’t learn a steep programming language like Rust unless it cut down meteorological or physics simulation from 2 weeks to 1 week compared to C.

      So the Rust scientific stack is good for production to make sure it is robust but researchers won’t use it. And one of the biggest challenge in scientific computing is reducing “time-to-market”, how to go fast from experimentations to production.

      • Dabo Ross

        I understand Debug/Clone is an issue for higher dimensional arrays – even though Index/IndexMut isn’t.

        For the ‘Go’ example though – all of those sizes are less than 32, no? 9×9 is [[SomeType; 9]; 9] – which has everything you mentioned implemented for it. Same with 11×11, 13×13, 15×15, etc.

        Above 32×32 matrices do have that problem, but I would argue those are less common, and at that point you can probably make do with Vec<Vec>>.

        I’m not at all objecting to the other points you make, I just think it’s straight out _wrong_ to say rust can’t represent a 17×17 matrix, because it literally can.

        • Mamy Ratsimbazafy

          It’s true, I could have, to be honest I don’t remember if it didn’t occurred to me or if there was another reason.

      • 1) There really isn’t that much to the symbols that Rust uses. Each of them has a precedence for their usage, as they are common symbols used in many other languages. A rudimentary course on systems programming is all you’d need to be effective with and understand what all the symbols are and do.

        The only additional symbol that Rust uses that aren’t common in other systems languages is the lifetime syntax from the Cyclone language experiment, but that’s not much to learn. It’s a very effective tool for those of us whom have learned how to use it.

        It’s also not possible at all to convey the information that Rust is conveying to the compiler without having the syntax that Rust is using. The two languages you mentioned, Nim and Haskell, are using runtime garbage collectors, and thus creating a heavy burden on paying runtime fees for not having that information available. That’s not what you want to have in a system’s language, or any kind of scientific library.

        2) I don’t understand why you’d need to implement/derive anything at all. It sounds like what you are asking for is simply to use vectors. Go doesn’t support arrays, as all of it’s arrays are vectors. The equivalent is therefore:

        struct GoBoard(Vec<Vec>);

        Vec already comes with Clone, Index, and IndexMut. No need to implement anything else…

        • Mamy Ratsimbazafy

          1) In Nim you have 3 types:

          Only the second one is managed by the garbage collector, I can do C-like memory management in Nim and even use compiler intrinsics which Rust cannot:

          Since April I created Arraymancer a tensor library based on Nim, on CPU it is significantly faster than everything I’ve benchmarked again: Torch (C/lua), Numpy (Python + core routine in C), Julia (Fortran).

          The snippet are taken from my implementation of a generic matrix-matrix multiplication as C/C++/Fortran libraries only have float/double implementations.

          2) Actually from the reddit thread I could have implemented that as

          and being able to derive instead of

          .
          Vec are not good for go however, you absolutely need stack arrays for performance as when predicting a move you need to generate 2000~10000 games per second, each game being about 400 moves (go boards).
          You don’t want to allocate/deallocate heap memory 4M times per second.

          • There’s no reason why you’d have to allocate more than once. Simply clear the board and reuse it — don’t ditch the allocation and re-allocate to get a clean slate. In addition there are stack-based vectors in Rust. See the smallvec crate.

          • Mamy Ratsimbazafy

            Yes, but shared memory data-structures are typically what is hard to do in Rust (well for good reasons)

          • I’ve written quite a bit of Rust software with shared data structures. This is actually really easy to achieve in Rust. There’s a number of ways that you can go about it, but the basic idea is just initialize a mutable variable and then re-use that variable for each iteration. This would be one possible demonstration:

            let mut board = Board::new(19);
            loop {
            let game: &Board = board.generate();
            // do something with the result
            }

            You can easily return either immutable or mutable references to the internal state. Doesn’t really matter what you pick. The signature would look like so:

            fn generate(&’a self) -> &’a Board;

            Or this:

            fn generate(&’a mut self) -> &’a mut Board;

            For added efficiency/convenience, you could even make your own type which clears the board upon dropping that type:

            fn generate(&’a mut self) -> GameResult {
            GameResult(self)
            }

            struct GameResult(&’a mut Board);

            impl Drop for GameResult {
            fn drop(self) { self.0.clear(); }
            }

    • foljs

      > 1) Well, we are talking about a modern systems language, and not a scripting language, or a garbage-collected language.

      That’s irrelevant.

      > Seems like you misunderstand what an array is. If you’re coming from a higher-level language, you don’t have access to arrays.

      You’d be surprised.

      • > That’s irrelevant.

        It is very relevant. Systems languages don’t have the luxury of a runtime garbage collector and/or virtual machine/interpreter taking care of all the unknown details that rise up with less-explicit language syntax. In order for Rust to be as safe and efficient as it is, the syntax is precisely as it is. There’s no other way to do this.

        > You’d be surprised.

        You can’t have access to arrays in a higher level language, else it wouldn’t be a high level language. They abstract arrays from the programmer and only allow access to vectors. Even if you use an array-like syntax, it is interpreted and stored within a vector.

        • Noah Yetter

          “You can’t have access to arrays in a higher level language…” This is false of course. Java has always had native arrays, and since they’re implemented in the JVM they’re available to all JVM languages no matter how high- or low-level you consider them to be. Native arrays can be used in Python through NumPy, or you can write your own wrapped C code.

          Unless you’re trying to say that any language with arrays is *by definition* “not higher level”, in which case you’re just falling victim to the No True Scotsman fallacy.

          • Mamy Ratsimbazafy

            Java is not low-level because you can’t control the memory layout of your data structure. In Java, like in Python, you can’t have stack allocated arrays, everything is a reference.

      • Mamy Ratsimbazafy

        Your comment makes opinionated claims (that’s fine) without any argument (that is not fine) with a sarcastic tone.

        Please keep it to a constructive discussion.

  • jedb

    Have you considered Ada? It seems to address all your issues here.

    1. Arbitrary symbols are minimised to promote ease of maintenance.
    2. It has proper array support. More so than a lot of other languages – arrays can be indexed with any integer/enumeration type and do not have start at zero/one.
    3. Integers, fixed point types, modulo types, enums, etc are all allowed as parameters for a generic.

    In addition:

    4. Since the language has been around for decades with a culture of reliability and correctness, bugs are extremely rare.
    5. Much greater control over basic types, with the ability to declare, for example, an Integer subtype with allowed values only in the range of 5 to 15.
    6. Ease of putting things on the stack combined with Controlled types makes “borrow checking” irrelevant.
    7. Interfacing with C and Fortran, both importing and exporting, is part of the language specification.
    8. Remember that control over basic types? Well, combined with the programming-by-contract features introduced in Ada2012, you can even get the compiler to check your units. See: http://www.adacore.com/adaanswers/gems/gem-136-how-tall-is-a-kilogram/

    List of learning materials if you’re interested: http://www.adaic.org/learn/materials/
    Don’t overlook good tools just because they aren’t the latest fashion.