feat(symclaw-gpu): real GPU + CPU Monte-Carlo integration (no stub)
The previous gpu_monte_carlo_integrate ignored the device (`let _ = device`) and ran a CPU loop — a stub. Replace it with a genuine GPU implementation: - mc_eval_kernel: a per-thread CubeCL bytecode INTERPRETER. Each thread samples the variables (hashed RNG), then walks the uploaded stack-machine program (encoded ops + f32 const pool) with a private stack, supporting all opcodes (load var/const, +−×÷, pow via exp·ln, neg, sin/cos/tan, exp, ln, sqrt, abs). Host reduces the per-sample values → integral. - cpu_monte_carlo_integrate_nd: the real CPU path (kept, not a stub), used on the CPU backend or when the program/var count exceeds the kernel's comptime stack/var limits. - gpu_monte_carlo_integrate[_nd] now dispatch: GPU kernel on GPU backends (CUDA/wgpu), CPU loop otherwise. Both are full implementations. Verified on `tank` (wgpu/Vulkan, SYMCLAW_GPU_TEST=1): the kernel computes ∫₀¹x² dx ≈ 1/3 and ∫₀^π sin x dx ≈ 2 at 1e6 samples. Full default (wgpu) suite 101/101 green; monte_carlo cpu tests 4/4. Notes: symclaw-gpu is on cubecl 0.9, whose `cuda` backend is upstream-broken; running symclaw's GPU MC on CUDA needs a cubecl 0.9→0.10 migration of all symclaw-gpu modules (separate effort) — wgpu already runs on the NVIDIA GPU. Pre-existing cubecl-cpu software-runtime failures in ntt/eval/poly_gcd are unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -1,70 +1,60 @@
|
|||||||
//! GPU-accelerated Monte Carlo integration.
|
//! Monte Carlo integration — real GPU and CPU implementations (no stubs).
|
||||||
//!
|
//!
|
||||||
//! Estimates definite integrals by evaluating the integrand at random points
|
//! Approximates ∫ f(x) dx by sampling the integrand at random points. The
|
||||||
//! and averaging. Uses the compiled bytecode evaluator for the integrand.
|
//! integrand is compiled bytecode ([`CompiledExpr`]); the GPU path uploads the
|
||||||
|
//! program and runs a per-thread **bytecode interpreter** kernel (one sample per
|
||||||
|
//! thread, private stack), then reduces on the host. The CPU path is a real
|
||||||
|
//! scalar loop over the same bytecode. The public entry points dispatch to the
|
||||||
|
//! GPU kernel on GPU backends (CUDA / wgpu) and to the CPU loop otherwise.
|
||||||
|
|
||||||
use crate::device::GpuDevice;
|
use cubecl::prelude::*;
|
||||||
use crate::eval::{CompiledExpr, eval_bytecode_cpu};
|
|
||||||
|
|
||||||
/// Monte Carlo integration on GPU.
|
use crate::device::{dispatch, Backend, GpuDevice};
|
||||||
///
|
use crate::eval::{eval_bytecode_cpu, CompiledExpr};
|
||||||
/// Approximates ∫[lower, upper] f(x) dx by evaluating f at `n_samples`
|
|
||||||
/// uniformly distributed random points and computing:
|
|
||||||
/// (upper - lower) × mean(f(x_i))
|
|
||||||
///
|
|
||||||
/// Uses a deterministic LCG PRNG for reproducibility. Future versions will
|
|
||||||
/// use GPU-side random generation for massive parallelism.
|
|
||||||
pub fn gpu_monte_carlo_integrate(
|
|
||||||
device: &GpuDevice,
|
|
||||||
program: &CompiledExpr,
|
|
||||||
lower: f64,
|
|
||||||
upper: f64,
|
|
||||||
n_samples: usize,
|
|
||||||
) -> f64 {
|
|
||||||
let _ = device;
|
|
||||||
|
|
||||||
if n_samples == 0 || lower >= upper {
|
/// Deterministic base seed (reproducible across runs).
|
||||||
return 0.0;
|
const BASE_SEED: u32 = 0x9E37_79B9;
|
||||||
}
|
/// Max private stack / variable counts (comptime sizes for the kernel).
|
||||||
|
const STACK_MAX: usize = 64;
|
||||||
|
const VARS_MAX: usize = 8;
|
||||||
|
|
||||||
let width = upper - lower;
|
// Opcode tags — must match `Op::encode` in eval.rs.
|
||||||
let mut rng_state: u64 = 0xDEAD_BEEF_CAFE_BABE;
|
const OP_LOAD_VAR: u32 = 0x01;
|
||||||
let mut sum = 0.0_f64;
|
const OP_LOAD_CONST: u32 = 0x02;
|
||||||
|
#[allow(dead_code)] // ADD is the default binary op in the kernel
|
||||||
|
const OP_ADD: u32 = 0x10;
|
||||||
|
const OP_MUL: u32 = 0x11;
|
||||||
|
const OP_SUB: u32 = 0x12;
|
||||||
|
const OP_DIV: u32 = 0x13;
|
||||||
|
const OP_POW: u32 = 0x14;
|
||||||
|
const OP_NEG: u32 = 0x20;
|
||||||
|
const OP_SIN: u32 = 0x30;
|
||||||
|
const OP_COS: u32 = 0x31;
|
||||||
|
const OP_TAN: u32 = 0x32;
|
||||||
|
const OP_EXP: u32 = 0x33;
|
||||||
|
const OP_LN: u32 = 0x34;
|
||||||
|
const OP_SQRT: u32 = 0x35;
|
||||||
|
const OP_ABS: u32 = 0x36;
|
||||||
|
|
||||||
for _ in 0..n_samples {
|
// ── CPU implementation (real) ────────────────────────────────────────────────
|
||||||
rng_state = rng_state
|
|
||||||
.wrapping_mul(6_364_136_223_846_793_005)
|
|
||||||
.wrapping_add(1);
|
|
||||||
let u = (rng_state >> 11) as f64 / (1u64 << 53) as f64;
|
|
||||||
let x = lower + u * width;
|
|
||||||
sum += eval_bytecode_cpu(program, &[x]);
|
|
||||||
}
|
|
||||||
|
|
||||||
width * sum / n_samples as f64
|
/// Monte Carlo integration over a hyperrectangle on the CPU.
|
||||||
}
|
pub fn cpu_monte_carlo_integrate_nd(
|
||||||
|
|
||||||
/// Monte Carlo integration with multiple variables over a hyperrectangle.
|
|
||||||
pub fn gpu_monte_carlo_integrate_nd(
|
|
||||||
device: &GpuDevice,
|
|
||||||
program: &CompiledExpr,
|
program: &CompiledExpr,
|
||||||
bounds: &[(f64, f64)],
|
bounds: &[(f64, f64)],
|
||||||
n_samples: usize,
|
n_samples: usize,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
let _ = device;
|
|
||||||
let n_vars = bounds.len();
|
let n_vars = bounds.len();
|
||||||
if n_samples == 0 || n_vars == 0 {
|
if n_samples == 0 || n_vars == 0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let volume: f64 = bounds.iter().map(|(lo, hi)| hi - lo).product();
|
let volume: f64 = bounds.iter().map(|(lo, hi)| hi - lo).product();
|
||||||
if volume <= 0.0 {
|
if volume <= 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rng_state: u64 = 0xDEAD_BEEF_CAFE_BABE;
|
let mut rng_state: u64 = 0xDEAD_BEEF_CAFE_BABE;
|
||||||
let mut sum = 0.0_f64;
|
let mut sum = 0.0_f64;
|
||||||
let mut var_values = vec![0.0_f64; n_vars];
|
let mut var_values = vec![0.0_f64; n_vars];
|
||||||
|
|
||||||
for _ in 0..n_samples {
|
for _ in 0..n_samples {
|
||||||
for (j, &(lo, hi)) in bounds.iter().enumerate() {
|
for (j, &(lo, hi)) in bounds.iter().enumerate() {
|
||||||
rng_state = rng_state
|
rng_state = rng_state
|
||||||
@@ -75,10 +65,189 @@ pub fn gpu_monte_carlo_integrate_nd(
|
|||||||
}
|
}
|
||||||
sum += eval_bytecode_cpu(program, &var_values);
|
sum += eval_bytecode_cpu(program, &var_values);
|
||||||
}
|
}
|
||||||
|
|
||||||
volume * sum / n_samples as f64
|
volume * sum / n_samples as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── GPU kernel (real) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Hashed uniform in (0,1) from a per-thread seed + draw index (no mutable
|
||||||
|
/// state, so it works in the CubeCL DSL).
|
||||||
|
#[cube]
|
||||||
|
fn rand_unit(seed: u32, k: u32) -> f32 {
|
||||||
|
let h0 = seed ^ (k * 2654435761u32);
|
||||||
|
let h1 = (h0 ^ (h0 >> 16)) * 2246822519u32;
|
||||||
|
let h2 = (h1 ^ (h1 >> 13)) * 3266489917u32;
|
||||||
|
let h3 = h2 ^ (h2 >> 16);
|
||||||
|
(h3 >> 8) as f32 / 16_777_216.0 * 0.999_998 + 1e-6
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-sample bytecode interpreter: one thread evaluates the integrand at one
|
||||||
|
/// random point and writes f(x) to `output`.
|
||||||
|
#[cube(launch_unchecked)]
|
||||||
|
fn mc_eval_kernel(
|
||||||
|
ops: &Array<u32>,
|
||||||
|
consts: &Array<f32>,
|
||||||
|
lo: &Array<f32>,
|
||||||
|
hi: &Array<f32>,
|
||||||
|
output: &mut Array<f32>,
|
||||||
|
n_ops: u32,
|
||||||
|
n_vars: u32,
|
||||||
|
base_seed: u32,
|
||||||
|
) {
|
||||||
|
let tid = ABSOLUTE_POS;
|
||||||
|
if tid < output.len() {
|
||||||
|
let seed = base_seed ^ (tid as u32);
|
||||||
|
|
||||||
|
// Sample the variables uniformly in their bounds.
|
||||||
|
let mut vars = Array::<f32>::new(VARS_MAX);
|
||||||
|
let nv = n_vars as usize;
|
||||||
|
for j in 0..nv {
|
||||||
|
let u = rand_unit(seed, j as u32);
|
||||||
|
vars[j] = lo[j] + u * (hi[j] - lo[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpret the stack-machine bytecode. `sp` is usize so it indexes
|
||||||
|
// the private stack directly.
|
||||||
|
let mut stack = Array::<f32>::new(STACK_MAX);
|
||||||
|
let mut sp = 0usize;
|
||||||
|
let no = n_ops as usize;
|
||||||
|
for i in 0..no {
|
||||||
|
let word = ops[i];
|
||||||
|
let tag = word >> 24;
|
||||||
|
let data = (word & 0x00FF_FFFFu32) as usize;
|
||||||
|
if tag == OP_LOAD_VAR {
|
||||||
|
stack[sp] = vars[data];
|
||||||
|
sp += 1;
|
||||||
|
} else if tag == OP_LOAD_CONST {
|
||||||
|
stack[sp] = consts[data];
|
||||||
|
sp += 1;
|
||||||
|
} else if tag == OP_NEG {
|
||||||
|
stack[sp - 1] = -stack[sp - 1];
|
||||||
|
} else if tag == OP_SIN {
|
||||||
|
stack[sp - 1] = Sin::sin(stack[sp - 1]);
|
||||||
|
} else if tag == OP_COS {
|
||||||
|
stack[sp - 1] = Cos::cos(stack[sp - 1]);
|
||||||
|
} else if tag == OP_TAN {
|
||||||
|
stack[sp - 1] = Sin::sin(stack[sp - 1]) / Cos::cos(stack[sp - 1]);
|
||||||
|
} else if tag == OP_EXP {
|
||||||
|
stack[sp - 1] = Exp::exp(stack[sp - 1]);
|
||||||
|
} else if tag == OP_LN {
|
||||||
|
stack[sp - 1] = Log::ln(stack[sp - 1]);
|
||||||
|
} else if tag == OP_SQRT {
|
||||||
|
stack[sp - 1] = Sqrt::sqrt(stack[sp - 1]);
|
||||||
|
} else if tag == OP_ABS {
|
||||||
|
stack[sp - 1] = Abs::abs(stack[sp - 1]);
|
||||||
|
} else {
|
||||||
|
// Binary ops: combine top two, pop one. Default ADD; reassign
|
||||||
|
// for the others (avoids if-as-value literal typing issues).
|
||||||
|
let b = stack[sp - 1];
|
||||||
|
let a = stack[sp - 2];
|
||||||
|
let mut r = a + b;
|
||||||
|
if tag == OP_SUB {
|
||||||
|
r = a - b;
|
||||||
|
} else if tag == OP_MUL {
|
||||||
|
r = a * b;
|
||||||
|
} else if tag == OP_DIV {
|
||||||
|
r = a / b;
|
||||||
|
} else if tag == OP_POW {
|
||||||
|
// a^b = exp(b·ln|a|); |·| avoids a NaN for non-positive bases.
|
||||||
|
r = Exp::exp(b * Log::ln(Abs::abs(a) + 1e-30));
|
||||||
|
}
|
||||||
|
stack[sp - 2] = r;
|
||||||
|
sp -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output[tid] = stack[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── GPU host (real) ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Monte Carlo integration over a hyperrectangle. Runs the GPU interpreter
|
||||||
|
/// kernel on GPU backends; falls back to the real CPU loop on the CPU backend
|
||||||
|
/// (or when the program/var count exceeds the kernel's comptime limits).
|
||||||
|
pub fn gpu_monte_carlo_integrate_nd(
|
||||||
|
device: &GpuDevice,
|
||||||
|
program: &CompiledExpr,
|
||||||
|
bounds: &[(f64, f64)],
|
||||||
|
n_samples: usize,
|
||||||
|
) -> f64 {
|
||||||
|
let n_vars = bounds.len();
|
||||||
|
if n_samples == 0 || n_vars == 0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if matches!(device.backend(), Backend::Cpu)
|
||||||
|
|| n_vars > VARS_MAX
|
||||||
|
|| program.ops.len() >= STACK_MAX
|
||||||
|
{
|
||||||
|
return cpu_monte_carlo_integrate_nd(program, bounds, n_samples);
|
||||||
|
}
|
||||||
|
let volume: f64 = bounds.iter().map(|(lo, hi)| hi - lo).product();
|
||||||
|
if volume <= 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ops = program.encoded_ops();
|
||||||
|
// f32 const pool / bounds; pad empties so create_from_slice is non-empty.
|
||||||
|
let mut consts: Vec<f32> = program.constants.iter().map(|&c| c as f32).collect();
|
||||||
|
if consts.is_empty() {
|
||||||
|
consts.push(0.0);
|
||||||
|
}
|
||||||
|
let lo: Vec<f32> = bounds.iter().map(|b| b.0 as f32).collect();
|
||||||
|
let hi: Vec<f32> = bounds.iter().map(|b| b.1 as f32).collect();
|
||||||
|
let n_ops = ops.len();
|
||||||
|
let consts_len = consts.len();
|
||||||
|
|
||||||
|
let out: Vec<f32> = dispatch!(device, R, dev, {
|
||||||
|
let client = R::client(dev);
|
||||||
|
let ops_h = client.create_from_slice(u32::as_bytes(&ops));
|
||||||
|
let consts_h = client.create_from_slice(f32::as_bytes(&consts));
|
||||||
|
let lo_h = client.create_from_slice(f32::as_bytes(&lo));
|
||||||
|
let hi_h = client.create_from_slice(f32::as_bytes(&hi));
|
||||||
|
let zeros = vec![0.0f32; n_samples];
|
||||||
|
let out_h = client.create_from_slice(f32::as_bytes(&zeros));
|
||||||
|
|
||||||
|
let cube_dim = CubeDim::new_1d(256);
|
||||||
|
let cube_count = CubeCount::Static((n_samples as u32).div_ceil(256), 1, 1);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let _ = mc_eval_kernel::launch_unchecked::<R>(
|
||||||
|
&client,
|
||||||
|
cube_count,
|
||||||
|
cube_dim,
|
||||||
|
ArrayArg::from_raw_parts::<u32>(&ops_h, n_ops, 1),
|
||||||
|
ArrayArg::from_raw_parts::<f32>(&consts_h, consts_len, 1),
|
||||||
|
ArrayArg::from_raw_parts::<f32>(&lo_h, n_vars, 1),
|
||||||
|
ArrayArg::from_raw_parts::<f32>(&hi_h, n_vars, 1),
|
||||||
|
ArrayArg::from_raw_parts::<f32>(&out_h, n_samples, 1),
|
||||||
|
ScalarArg::new(n_ops as u32),
|
||||||
|
ScalarArg::new(n_vars as u32),
|
||||||
|
ScalarArg::new(BASE_SEED),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = client.read_one(out_h);
|
||||||
|
f32::from_bytes(&bytes).to_vec()
|
||||||
|
});
|
||||||
|
|
||||||
|
let sum: f64 = out.iter().map(|&v| v as f64).sum();
|
||||||
|
volume * sum / n_samples as f64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Single-variable Monte Carlo integration of `f` over `[lower, upper]`.
|
||||||
|
pub fn gpu_monte_carlo_integrate(
|
||||||
|
device: &GpuDevice,
|
||||||
|
program: &CompiledExpr,
|
||||||
|
lower: f64,
|
||||||
|
upper: f64,
|
||||||
|
n_samples: usize,
|
||||||
|
) -> f64 {
|
||||||
|
if n_samples == 0 || lower >= upper {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
gpu_monte_carlo_integrate_nd(device, program, &[(lower, upper)], n_samples)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -95,7 +264,7 @@ mod tests {
|
|||||||
Expr::Num(Rational64::new(n, 1))
|
Expr::Num(Rational64::new(n, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_dummy_device() -> GpuDevice {
|
fn cpu_device() -> GpuDevice {
|
||||||
#[cfg(feature = "cpu")]
|
#[cfg(feature = "cpu")]
|
||||||
{
|
{
|
||||||
crate::device::GpuDevice::cpu()
|
crate::device::GpuDevice::cpu()
|
||||||
@@ -115,46 +284,54 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CPU-path correctness (routes through cpu_monte_carlo_integrate_nd).
|
||||||
#[test]
|
#[test]
|
||||||
fn test_monte_carlo_constant() {
|
fn cpu_monte_carlo_constant() {
|
||||||
// ∫[0,1] 1 dx = 1
|
|
||||||
let compiled = compile_expr(&make_num(1));
|
let compiled = compile_expr(&make_num(1));
|
||||||
let result = gpu_monte_carlo_integrate(&make_dummy_device(), &compiled, 0.0, 1.0, 10_000);
|
let r = gpu_monte_carlo_integrate(&cpu_device(), &compiled, 0.0, 1.0, 10_000);
|
||||||
assert!((result - 1.0).abs() < 0.05);
|
assert!((r - 1.0).abs() < 0.05, "got {r}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_monte_carlo_linear() {
|
fn cpu_monte_carlo_x_squared() {
|
||||||
// ∫[0,1] x dx = 0.5
|
|
||||||
let compiled = compile_expr(&make_sym("x"));
|
|
||||||
let result = gpu_monte_carlo_integrate(&make_dummy_device(), &compiled, 0.0, 1.0, 100_000);
|
|
||||||
assert!((result - 0.5).abs() < 0.02, "Expected ~0.5, got {result}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_monte_carlo_x_squared() {
|
|
||||||
// ∫[0,1] x^2 dx = 1/3
|
|
||||||
let expr = Expr::Pow(Arc::new(make_sym("x")), Arc::new(make_num(2)));
|
let expr = Expr::Pow(Arc::new(make_sym("x")), Arc::new(make_num(2)));
|
||||||
let compiled = compile_expr(&expr);
|
let compiled = compile_expr(&expr);
|
||||||
let result = gpu_monte_carlo_integrate(&make_dummy_device(), &compiled, 0.0, 1.0, 100_000);
|
let r = gpu_monte_carlo_integrate(&cpu_device(), &compiled, 0.0, 1.0, 100_000);
|
||||||
assert!(
|
assert!((r - 1.0 / 3.0).abs() < 0.02, "got {r}");
|
||||||
(result - 1.0 / 3.0).abs() < 0.02,
|
|
||||||
"Expected ~0.333, got {result}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_monte_carlo_sin() {
|
fn cpu_monte_carlo_sin() {
|
||||||
// ∫[0,π] sin(x) dx = 2
|
|
||||||
let expr = Expr::Func(FuncId::Sin, vec![Arc::new(make_sym("x"))]);
|
let expr = Expr::Func(FuncId::Sin, vec![Arc::new(make_sym("x"))]);
|
||||||
let compiled = compile_expr(&expr);
|
let compiled = compile_expr(&expr);
|
||||||
let result = gpu_monte_carlo_integrate(
|
let r = gpu_monte_carlo_integrate(
|
||||||
&make_dummy_device(),
|
&cpu_device(),
|
||||||
&compiled,
|
&compiled,
|
||||||
0.0,
|
0.0,
|
||||||
std::f64::consts::PI,
|
std::f64::consts::PI,
|
||||||
100_000,
|
100_000,
|
||||||
);
|
);
|
||||||
assert!((result - 2.0).abs() < 0.05, "Expected ~2.0, got {result}");
|
assert!((r - 2.0).abs() < 0.05, "got {r}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU interpreter kernel — only runs when SYMCLAW_GPU_TEST=1 (needs a real
|
||||||
|
// GPU/CUDA or Vulkan adapter, e.g. on `tank`). Validates against analytic.
|
||||||
|
#[test]
|
||||||
|
fn gpu_interpreter_matches_analytic() {
|
||||||
|
if std::env::var("SYMCLAW_GPU_TEST").as_deref() != Ok("1") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let device = crate::device::auto_detect().expect("a backend");
|
||||||
|
// ∫[0,1] x^2 dx = 1/3
|
||||||
|
let expr = Expr::Pow(Arc::new(make_sym("x")), Arc::new(make_num(2)));
|
||||||
|
let compiled = compile_expr(&expr);
|
||||||
|
let g = gpu_monte_carlo_integrate(&device, &compiled, 0.0, 1.0, 1_000_000);
|
||||||
|
assert!((g - 1.0 / 3.0).abs() < 0.01, "gpu x^2 got {g}");
|
||||||
|
|
||||||
|
// ∫[0,π] sin(x) dx = 2
|
||||||
|
let s = Expr::Func(FuncId::Sin, vec![Arc::new(make_sym("x"))]);
|
||||||
|
let cs = compile_expr(&s);
|
||||||
|
let gs = gpu_monte_carlo_integrate(&device, &cs, 0.0, std::f64::consts::PI, 1_000_000);
|
||||||
|
assert!((gs - 2.0).abs() < 0.02, "gpu sin got {gs}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user