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

A shim mode for targets without threading support #861

Closed
RReverser opened this issue May 18, 2021 · 26 comments · Fixed by #1019
Closed

A shim mode for targets without threading support #861

RReverser opened this issue May 18, 2021 · 26 comments · Fixed by #1019

Comments

@RReverser
Copy link
Contributor

RReverser commented May 18, 2021

Maybe I haven't looked through issues closely enough, so apologies if this was brought up before.

One thing that comes up often when using Rayon is making sure that code works correctly on targets without threading support (e.g. wasm32-wasi or regular wasm32-unknown-unknown without threads support in general).

Probably the most common way to do this in a way that abstracts away platform differences is to build custom shim for the used rayon APIs that uses regular Iterator instead and expose it as mod rayon that will be included instead of rayon crate when corresponding feature is disabled (e.g. 1, 2, 3, 4).

However, Rayon itself feels like a lot more appropriate place for such shimming, since it has more control over APIs and would be easier to maintain than custom shims in each separate app that wants to build Rayon-dependant code in a cross-platform manner.

So, the question is: how realistic would it be to make Rayon use regular Iterators when a corresponding Cargo feature is enabled? Given that Rayon can already handle RAYON_NUM_THREADS=1 case, and the behaviour should be pretty similar, it seems doable but I'm not familiar with internals enough to estimate amount of work this would take.

@nikomatsakis
Copy link
Member

Hmm. Interesting question. How much does it matter if the code runs fast? I think the question is where is the right place to do this -- I think you could do it in a pretty narrow way at the right spot, but the resulting code might have some extra operations here and there.

@cuviper
Copy link
Member

cuviper commented May 18, 2021

spawn is a particular difficulty. I suppose we might force that to run synchronously, but code may expect independent progress.

Another way to look at that: even with RAYON_NUM_THREADS=1, there are usually at least two threads -- main and the single-threaded pool.

@cuviper
Copy link
Member

cuviper commented May 18, 2021

See also #591.

@nikomatsakis
Copy link
Member

Mm, yes, that's a good point.

@cuviper
Copy link
Member

cuviper commented May 18, 2021

AIUI, the standard library for wasm still builds as if it has threading support, but it panics at runtime. So one possibility for that case is to use my rayon-cond wrapper for iterators, and always choose the serial mode for wasm builds. As long as you're consistent with that, it won't ever try (and fail) to create the actual thread pool.

The other shims you linked are ignoring the fact that there are real API differences between Iterator and ParallelIterator. It must be close enough for their needs, but fold is a big example where they're not interchangeable.

@RReverser
Copy link
Contributor Author

RReverser commented May 18, 2021

The other shims you linked are ignoring the fact that there are real API differences between Iterator and ParallelIterator. It must be close enough for their needs, but fold is a big example where they're not interchangeable.

Indeed, that's where complexity starts creeping in in bigger apps, and why I think that rayon is a better place to solve this problem - since it has full coverage of the API, and can more easily verify that implementations are interchangeable for parallel and serial versions.

spawn is a particular difficulty. I suppose we might force that to run synchronously, but code may expect independent progress.

Yeah, spawn or, say, manual usage of Mutex and other threading APIs would be particularly tricky, but I think those are rare enough that it could be justifiable to just panic on them in the minimal shim, like std::thread does.

What I'm more interested in is just iterators themselves as that's probably most popular use-case and would allow to easily use same API for parallel and non-parallel builds.

Well, also probably rayon::join since a lot of other things build atop it, but that one also already supports serial execution and shouldn't be an issue.

@RReverser
Copy link
Contributor Author

See also #591.

Oh yeah, that sounds pretty similar - although the use-cases are quite different, the desired end solution is probably the same.

@arifd
Copy link

arifd commented Nov 7, 2021

I'm glad I found this issue, I was surprised I couldn't find out how to do this, so I ended up defining and implementing NoRayon traits with functions that mirror those that I needed and implementing it on the types I needed.

Leaving the code here for future searchers who perhaps don't have the rusty chops to build it for themselves without seeing an example first:

// put this in a file called `no_rayon.rs`, then you can conditionally pull it in as required, i.e.
//
// #[cfg(not(target_arch = "wasm32"))]
// use rayon::prelude::*;
// #[cfg(target_arch = "wasm32")]
// mod no_rayon;
// use no_rayon::prelude::*;

pub mod prelude {
    pub use super::{NoRayonSlice, NoRayonSliceMut};
}

pub trait NoRayonSlice<T> {
    fn par_iter(&self) -> core::slice::Iter<'_, T>;
    fn par_chunks_exact(&self, chunk_size: usize) -> core::slice::ChunksExact<'_, T>;
}
impl<T> NoRayonSlice<T> for [T] {
    fn par_iter(&self) -> core::slice::Iter<'_, T> {
        self.iter();
    }
    fn par_chunks_exact(&self, chunk_size: usize) -> core::slice::ChunksExact<'_, T> {
        self.chunks_exact(chunk_size);
    }
}

pub trait NoRayonSliceMut<T> {
    fn par_chunks_exact_mut(&mut self, chunk_size: usize) -> core::slice::ChunksExactMut<'_, T>;
    fn par_iter_mut(&mut self) -> core::slice::IterMut<'_, T>;
}
impl<T> NoRayonSliceMut<T> for [T] {
    fn par_chunks_exact_mut(&mut self, chunk_size: usize) -> core::slice::ChunksExactMut<'_, T> {
        self.chunks_exact_mut(chunk_size);
    }
    fn par_iter_mut(&mut self) -> core::slice::IterMut<'_, T> {
        self.iter_mut();
    }
}

Also adding these keywords for searchability: fallback, fall back, polyfill, poly fill

@RReverser
Copy link
Contributor Author

Yeah I hope there will be some further discussion on this, the conversation above just stopped.

@tmpfs
Copy link

tmpfs commented Feb 2, 2022

This would be very useful for us. We have a scenario where we are compiling to webassembly and transient dependencies use rayon so we used wasm-bindgen-rayon (thanks @RReverser ) to get the code to compile.

However this means that LLVM will enable shared memory and thus SharedArrayBuffer is used. The big problem for us is that we want to use the webassembly module in a browser extension where we cannot use SharedArrayBuffer due to the Spectre mitigations.

Being able to have a shim mode that could completely disable threading would solve the problem for us.

@cuviper
Copy link
Member

cuviper commented Feb 8, 2022

Light sketch:

  • An environment variable can override Registry::new to create no threads at all.
    • Or maybe check that in ThreadPoolBuilder::get_num_threads, returning 0 if an explicit count is not set.
  • Registry::inject can notice when it has no threads and instead execute the job immediately, blocking.
  • Caveat emptor: some code may deadlock if there are no threads to make independent progress.

@kornelski
Copy link
Contributor

kornelski commented Feb 12, 2022

I second the request. I need to support single-threaded WASM in my libraries (the experimental WASM threading support is too complex and not supported well enough).

I've been using a shim like this:

https://github.com/ImageOptim/libimagequant/blob/main/src/rayoff.rs#L34
https://github.com/kornelski/dssim/blob/main/dssim-core/src/lieon.rs

but it would be best if it could be supported as a compile-time Cargo feature in rayon itself, so that I can apply this to the whole crate instead of faking APIs one by one.

@RReverser
Copy link
Contributor Author

(the experimental WASM threading support is too complex and not supported well enough)

@kornelski Offtop:

If by "not supported well enough" you mean browsers, then Wasm threads are actually supported in all major browsers nowadays.

If you mean on the Rust side, yeah, there is no built-in support unfortunately. To cover at least Rayon use-cases and work around browser bugs, I created the wasm-bindgen-rayon, but, yeah, it still has the usual Wasm threads caveats in terms of usage (e.g. you can't use it from the main browser thread).

@RReverser
Copy link
Contributor Author

RReverser commented Feb 12, 2022

An environment variable can override Registry::new to create no threads at all.

@cuviper I'm not aware of cases where we'd need to disable multithreading at runtime tbh. Moreover, for cases like Wasm compile-time flag would be preferable because it would allow to completely remove Rayon threading code & stdlib thread panics code from the resulting Wasm module, leading to smaller network sizes.

@cuviper
Copy link
Member

cuviper commented Feb 13, 2022

The runtime shim would be for cases like #591, where you probably don't want to change the actual codegen, but otherwise need to make sense of your code apart from rayon. Although I'm not sure it would be any better than what you get with RAYON_NUM_THREADS=1 already, if the higher-level iterator code is still going through the motions of split/join.

I can see the use for compile-time trimming too. That would be even better served by cutting rayon out entirely for those threadless targets, but that's more work on the user side.

@kornelski
Copy link
Contributor

Like I've linked above, its doable to disable rayon per application, but it's a significant hassle. It affects many iterators and requires shims for join, scope, etc. It's not as easy as merely making the dependency optional.

It's commonly needed for WASM, and it's a hassle to reinvent shim traits for every project.

I've tried creating reusable dummy no-op implementations of rayon's interfaces, but the api surface is large. You probably know best where to cut the code out to keep most of the interface working without any thread use.

I'm concerned about WASM, not profiling use-case. If you think they're best served with different solutions, I can open a separate issue for single-threaded WASM builds.

@jakkos-net
Copy link

This would also be very useful to me as well!

I have a project that ideally would work on WASM, but it uses a lot of the rayon API. While wasm-bindgen-rayon and rayon-cond are both really cool, unfortunately both have unworkable caveats for me.

Were there any additional updates or progress for this?

@nullchinchilla
Copy link

This is also extremely important for my usecase, which involves porting an entire codebase full of data-parallel uses of rayon, none of which would be deal-breaking to serialize, to WASM.

@emilk
Copy link

emilk commented Sep 28, 2022

I'm adding another +1 here.

Trying to use rayon in wasm32-unknown-unknown on a browser results in

panicked at 'The global thread pool has not been initialized.: ThreadPoolBuildError { kind: IOError(Error { kind: Unsupported, message: "operation not supported on this platform" }) }', ~/.cargo/registry/src/github.com-1ecc6299db9ec823/rayon-core-1.9.3/src/registry.rs:170:10

@Pratyush
Copy link

Pratyush commented Oct 4, 2022

For another polyfill, we've defined our own macros that, depending on a feature flag, either invokes the Iterator or ParallelIterator method. See here: https://docs.rs/ark-std/0.3.0/ark_std/macro.cfg_into_iter.html

@sergree
Copy link

sergree commented Feb 14, 2023

+1
We really need this feature.
Otherwise, such boilerplate polyfills must be integrated into every wasm/wasi application that uses rayon. It's not cool. 🤨

@sergree
Copy link

sergree commented Feb 14, 2023

It seems native threads will be supported in the upcoming wasm32-wasi-threads target. But i can't find an ETA for it.

Refs:
rust-lang/compiler-team#574
rust-lang/rust#77839
t-compiler/major changes @ Zulip

@cuviper
Copy link
Member

cuviper commented Feb 15, 2023

See #1019 -- feedback would be appreciated!

@cuviper
Copy link
Member

cuviper commented Feb 15, 2023

To be clear, #1019 does not try to revert to regular Iterator APIs, as that would be hugely invasive. My change is basically just catching io::ErrorKind::Unsupported to fall back to using the current thread, without running rayon's main_loop. That will probably be good enough for a lot of use cases, but anything that needs independent execution progress (like rayon::spawn) generally won't work.

@hyf0
Copy link

hyf0 commented Apr 13, 2024

Try to make our program running on wasi threads target, but it just keeps throwing problems on rayon. We use this shims.

#[cfg(target_family = "wasm")]
mod wasm_shims {
  pub use std::iter::Iterator as ParallelIterator;

  pub trait ParallelBridge: Sized {
    fn par_bridge(self) -> Self;
  }

  impl<T: Iterator + Send> ParallelBridge for T {
    fn par_bridge(self) -> Self {
      self
    }
  }

  pub trait IntoParallelIterator: Sized {
    type Item;
    type Iter: Iterator<Item = Self::Item>;

    fn into_par_iter(self) -> Self::Iter;
  }

  impl<I> IntoParallelIterator for I
  where
    I: IntoIterator,
  {
    type Item = I::Item;
    type Iter = I::IntoIter;

    fn into_par_iter(self) -> Self::Iter {
      self.into_iter()
    }
  }

  pub trait IntoParallelRefIterator<'data> {
    type Item: 'data;
    type Iter: ParallelIterator<Item = Self::Item>;

    fn par_iter(&'data self) -> Self::Iter;
  }

  impl<'data, I: 'data + ?Sized> IntoParallelRefIterator<'data> for I
  where
    &'data I: IntoParallelIterator,
  {
    type Iter = <&'data I as IntoParallelIterator>::Iter;
    type Item = <&'data I as IntoParallelIterator>::Item;

    fn par_iter(&'data self) -> Self::Iter {
      self.into_par_iter()
    }
  }
}

use rayon::iter::IntoParallelRefMutIterator;
#[cfg(target_family = "wasm")]
pub use wasm_shims::{
  IntoParallelIterator, IntoParallelRefIterator, ParallelBridge, ParallelIterator,
};

#[cfg(not(target_family = "wasm"))]
pub use rayon::iter::{
  IntoParallelIterator, IntoParallelRefIterator, ParallelBridge, ParallelIterator,
};

fn _usages() {
  let mut demo = vec![1, 2, 3, 4, 5];
  demo.iter().par_bridge().for_each(|_| {});
  demo.iter_mut().par_bridge().for_each(|_| {});
  demo.clone().into_iter().par_bridge().for_each(|_| {});
  demo.par_iter().for_each(|_| {});
  demo.par_iter_mut().for_each(|_| {});
  demo.clone().into_par_iter().for_each(|_| {});
}

@cuviper
Copy link
Member

cuviper commented Apr 15, 2024

@hyf0 Please open a new issue if you're having trouble with rayon. However, the way your wasm_shims are configured looks like you will be explicitly avoiding rayon on that target.

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 a pull request may close this issue.