【问题标题】:How to have seedable RNG in parallel in rust如何使可播种的 RNG 在生锈中并行
【发布时间】:2022-11-12 15:01:51
【问题描述】:

我通过实现光线追踪器来学习 rust。我有一个单线程的工作原型,我正在尝试使其成为多线程。

在我的代码中,我有一个采样器,它基本上是StdRng::seed_from_u64(123) 的包装器(当我添加不同类型的采样器时,它会改变),因为StdRNG 是可变的。我需要有一个可重复的行为,这就是我播种随机数生成器的原因。

在我的渲染循环中,我以下列方式使用采样器

        let mut sampler = create_sampler(&self.sampler_value);
        let sample_count = sampler.sample_count();

        println!("Rendering ...");
        let progress_bar = get_progress_bar(image.size());

        // Generate multiple rays for each pixel in the image
        for y in 0..image.size_y {
            for x in 0..image.size_x {
                image[(x, y)] = (0..sample_count)
                    .into_iter()
                    .map(|_| {
                        let pixel = Vec2::new(x as f32, y as f32) + sampler.next2f();
                        let ray = self.camera.generate_ray(&pixel);
                        self.integrator.li(self, &mut sampler, &ray)
                    })
                    .sum::<Vec3>()
                    / (sample_count as f32);

                progress_bar.inc(1);
            }
        }

当我用par_into_iter 替换into_iter 时,编译器会告诉我不能将sampler 借用为可变的,因为它是Fn 闭包中的捕获变量

在这种情况下我该怎么办?

谢谢!

附:如果它有任何用处,这是回购:https://github.com/jgsimard/rustrt

【问题讨论】:

  • 如果你保持一个单身的对于所有线程的 RNG,无论幕后使用何种锁定/原子机制,您都将终止并行性,因为您将在每个线程的每次迭代中都有缓存失效。您可能需要使用手工解决方案进行并行化:明确选择线程数,将图像沿 y 除以该数字(类似于chunks_mut()),为每个线程提供其自己的RNG(如您所愿),并让这些线程工作独立地在他们的部分图像切片上。

标签: rust raytracing rayon


【解决方案1】:

即使 Rust 没有阻止你,您不能只使用具有并行性的种子 PRNG 并获得可重现的结果。

这样想:具有特定种子/状态的 PRNG 会产生特定的数字序列.再现性(确定性)不仅要求数字相同,而且要求它们从序列中提取的方式是相同的。但是,如果您有多个线程计算不同的像素(不同的用途),它们相互竞争以从单个 PRNG 中获取数字,那么像素将在不同的运行中获取不同的数字。

为了获得您想要的确定性,您必须确定性地选择哪个随机数用于哪个目的。

一种方法是组成一个随机数的“图像”,按顺序计算,然后将其传递给并行循环。然后每条射线都有自己的随机数,它可以用作它的种子其他只有那条射线使用的 PRNG。

另一种可以更高效和可用的方法(因为它根本不需要任何顺序性)是使用散列函数而不是 PRNG。每当你想要一个随机数时,使用散列函数(比如那些在 Rust 中实现 std::hash::Hasher 特征的函数,但不一定是 std 提供的特定函数,因为它不是最快的)来组合一堆信息,比如

  • 种子值
  • 像素 x 和 y 位置
  • 您正在计算该像素的哪个反弹或辅助光线

成一个值,您可以将其用作伪随机数。这样,相同情况下的“随机”结果是相同的(因为您明确指定应该从它们计算)即使程序执行的其他部分发生变化(无论是代码更改还是操作系统的线程调度决定)。

【讨论】:

【解决方案2】:

你的采样器不是线程安全的,如果只是因为它是 &amp;mut Sampler 并且可变引用不能在线程之间共享,显然。

最简单的方法是将它包装到 Arc&lt;Mutex&lt;Sampler&gt;&gt; 中并将其克隆到每个闭包中。像(未经测试):

let sampler = Arc::new(Mutex::new(create_sampler(&self.sampler_value)));
//...
for y in 0..image.size_y {
            for x in 0..image.size_x {
                image[(x, y)] = (0..sample_count)
                    .par_into_iter()
                    .map({
                       let sampler = Arc::clone(sampler);
                       move |_| {
                           let mut sampler = sampler.lock().unwrap();
                           // use the sampler
                       }
                    })
                    .sum::<Vec3>() //...

但这可能不是很有效的方法,因为互斥体大部分时间都会被锁定,你会杀死并行性。您可以尝试在光线追踪期间锁定/解锁互斥锁,看看它是否有所改善。

理想的解决方案是使 Sampler 线程安全且内部可变,这样 next2f 和朋友就不需要 &amp;mut self 部分 (Sampler::next2f(&amp;self))。同样,最简单的方法是使用内部互斥体。

或者您可以尝试无锁!我的意思是,您当前对该功能的实现是:

    fn next2f(&mut self) -> Vec2 {
        self.current_dimension += 2;
        Vec2::new(self.rng.gen(), self.rng.gen())
    }

您可以将current_dimension 替换为AtomicI32,将rng 替换为rand::thread_rng(也未经测试):

    fn next2f(&self) -> Vec2 {
        self.current_dimension.fetch_add(2, Ordering::SeqCst);
        let mut rng = rand::thread_rng();
        Vec2::new(rng.gen(), rng.gen())
    }

【讨论】:

  • 使用原子版本,我看不到如何仍然可以播种随机数生成器
  • (如果你想测试它,cargo run --release -- -o test.png -s 3
  • 我的想法是使用 atomic 作为计数器,使用 rand::thread_rng() 作为随机数生成器,这是一个静态的、线程局部的 RNG,所以你不需要播种它,它已经存在了。如果你仍然想播种它,你可以创建你自己的本地线程,但是每个线程都有自己的线程,并且并发性无论如何都会使它变得不可预测(也就是说,你的输出将不可重现)。
  • 如果你真的想要可重现的输出,那么你必须非常非常小心地混合 RNG 和线程。
【解决方案3】:

我就是这样做的。我使用 ChaCha8Rngset_stream 函数来并行获取可播种的 PRNG。我不得不将 image[(x, y)] 放在迭代器之外,因为 into_par_iter 不允许在闭包内进行可变借用。如果您在我的解决方案中看到一些愚蠢的东西,请告诉我!

let size_x = image.size_x;
let img: Vec<Vec<Vec3>> = (0..image.size_y)
    .into_par_iter()
    .map(|y| {
         (0..image.size_x)
             .into_par_iter()
             .map(|x| {
                 let mut rng = ChaCha8Rng::seed_from_u64(sampler.seed());
                 rng.set_stream((y * size_x + x) as u64);
                 let v = (0..sample_count)
                     .into_iter()
                     .map(|_| {
                         let pixel = Vec2::new(x as f32, y as f32) + sampler.next2f(&mut rng);
                         let ray = self.camera.generate_ray(&pixel);
                         self.integrator.li(self, &sampler, &mut rng, &ray)
                     })
                     .sum::<Vec3>()
                     / (sample_count as f32);
                  progress_bar.inc(1);
                  v
              }).collect()
      }).collect();

for (y, row) in img.into_iter().enumerate() {
    for (x, p) in row.into_iter().enumerate() {
         image[(x, y)] = p;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-13
    • 1970-01-01
    • 2012-09-28
    • 2014-02-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多