Simple Monte Carlo With Thrust

We have already shown before how to set up CUDA in both Windows and Linux environment and how to create a simple project. Today we will demonstrate how easy it is to perform a simple Monte Carlo simulation using Thrust library. We will do so by estimating $\pi$ with random numbers.

From the project's homepage we see that

Thrust is a parallel algorithms library which resembles the C++ Standard Template Library (STL). Thrust's high-level interface greatly enhances programmer productivity while enabling performance portability between GPUs and multicore CPUs.

And this is very true. When you use Thrust it is almost identical to using the STL. While it might not be the fastest GPU implementation for your particular problem, it is easy to use and provides nice tools for quick development. It takes care of many things behind the scene (cudaMalloc, cudaFree, etc.) and makes the code cleaner and pleasant to read.

We will illustrate the $\pi$ estimation with three different approaches. Firstly, we will implement the algorithm outlined in this presentation. Secondly, we will implement a modified code from Thrust examples (with CuRand in kernel). Finally, we will implement the standard CuRand example utilizing Thrust.

Approach 1

The first version of $\pi$ estimation is using random number generator implemented in Thrust library. The code is very strightforward and has three main components. The functor randomPoint responsible for generating pair of random numbers is

struct randomPoint{  
    __device__ float2 operator() (const unsigned int n) {
        thrust::default_random_engine rng;
        rng.discard(2 * n);
        return make_float2(
            (float)rng() / thrust::default_random_engine::max,
            (float)rng() / thrust::default_random_engine::max);
    }
};

Note, that the original presentation is missing and important line of code rng.discard(2 * n). We would geerate the same random numbers again and again without it.

Following functor is responsible for deciding whether the point is inside a unit circle or not

struct insideCircle {  
    __device__ unsigned int operator() (float2 p) const {
        return (sqrtf(p.x*p.x + p.y*p.y) < 1.0f) ? 1 : 0;
    }
};

And the final piece of code for gluing these parts together is

// DEVICE: Generate random points within a unit square
thrust::device_vector<float2> d_random(N);  
thrust::counting_iterator<unsigned int> d_indexSequence(0);  
thrust::transform(d_indexSequence, d_indexSequence + N,  
                  d_random.begin(), randomPoint());

// DEVICE: Flags to mark points lying inside or outside the circle
thrust::device_vector<unsigned int> d_inside(N);

// DEVICE: Function evaluation. Mark points as inside or outside and aggregate
unsigned int total = thrust::transform_reduce(  
    d_random.begin(),
    d_random.end(),
    insideCircle(),
    0,
    thrust::plus<unsigned int>());

return 4.0f * (float)total / (float)N;  

The original presentation uses transform and reduce functions separately. For a small performance improvement we combined these two functions into one - transform_reduce.

Approach 2

The second version follows an example from Thrust library but instead of using Thrust random number generator we use CuRand random number generator in kernel function.

The heavy-weighting is done by following functor

struct estimatePiHelper :  
    public thrust::unary_function<unsigned int, float>
{
    __device__
        float operator()(unsigned int thread_id)
    {
        float sum(0);
        unsigned int N(100000); // samples per thread

        unsigned int seed = thread_id;

        curandState s;

        // seed a random number generator
        curand_init(seed, 0, 0, &s);

        // take N samples in a quarter circle
        for (unsigned int i = 0; i < N; ++i)
        {
            // draw a sample from the unit square
            float x = curand_uniform(&s);
            float y = curand_uniform(&s);

            // measure distance from the origin
            float dist = sqrtf(x*x + y*y);

            // add 1.0f if (u0,u1) is inside the quarter circle
            if (dist <= 1.0f)
                sum += 1.0f;
        }

        // multiply by 4 to get the area of the whole circle
        sum *= 4.0f;

        // divide by N
        return sum / N;
    }
};

The main difference between first and second approach is that here we generate 100,000 random numbers within each thread, i.e. we perform more computations within each kernel function.

Following is code which calls the code above and estimates $\pi$

float estimate = thrust::transform_reduce(  
        thrust::counting_iterator<unsigned int>(0),
        thrust::counting_iterator<unsigned int>(N),
        estimatePiHelper(),
        0.0f,
        thrust::plus<float>());

return estimate / N;  

The only downside is that we found it rather cumbersome to specify a different random number generator, e.g. Sobol or Mersenne Twister.

Approach 3

The last version utilizes CuRand function called from host rather than device. The helper functor responsible for deciding whether point is within unit circle is

struct insideCircle {  
    __device__ unsigned int operator() (thrust::tuple<float, float> p) const {
        float x = thrust::get<0>(p);
        float y = thrust::get<1>(p);
        return (sqrtf(x*x + y*y) < 1.0f) ? 1 : 0;
    }
};

The core part of code of this version is

curandGenerator_t qrng;  
thrust::device_vector<float> d_points(2 * N);

// Set type of random number generator
curandCreateGenerator(&qrng, CURAND_RNG_QUASI_SOBOL32);  
//curandCreateGenerator(&qrng, CURAND_RNG_PSEUDO_DEFAULT);

// Set dimension of random number generator
curandSetQuasiRandomGeneratorDimensions(qrng, 2);

// Set default ordering
curandSetGeneratorOrdering(qrng, CURAND_ORDERING_QUASI_DEFAULT);

// Generate N numbers in 2 dimensions
curandGenerateUniform(qrng, thrust::raw_pointer_cast(&d_points[0]), 2 * N);

unsigned int total = thrust::transform_reduce(  
    thrust::make_zip_iterator(
        thrust::make_tuple(
            d_points.begin(),
            d_points.begin() + N + 1)),
    thrust::make_zip_iterator(
        thrust::make_tuple(
            d_points.begin() + N + 1,
            d_points.end())),
    insideCircle(),
    0,
    thrust::plus<unsigned int>()
);

// Destroy random number generator
curandDestroyGenerator(qrng);

return 4.0f * (float)total / (float)N;  

Note, that we use Sobol random number generator in this case. Also note, that we use structure of arrays (SoA) rather than array of structures (AoS) as described in this presentation.

We hope you enjoyed this post and do not forget to leave us a comment. Thank you.