1.5. Generators and prefixes


The API is, in large part, the same for every generator, with only the prefix of the type and function names that changes across generators. For example, to use the MRG31k3p generator, one needs to include the corresponding header file (which is normally the lowercase name of the generator with a .h extension on the host or the device) and use type and function names that start with hcrngMrg31k3p:

#include <hcRNG/mrg31k3p.h>
double foo(hcrngMrg31k3pStream* stream) {
return hcrngMrg31k3pRandomU01(stream);
}

The above function just returns a number uniformly distributed in (0,1) generated using the stream passed as its argument. To use the LFSR113 generator instead of MRG31k3p, one must change the include directive and use type and function names that start with hcrngLfsr113:

#include <hcRNG/lfsr113.h>
double foo(hcrngLfsr113Stream* stream) {
return hcrngLfsr113RandomU01(stream);
}

In the generator API reference, the generator-specific part of the prefix is not shown. The hcRNG.h header file declares common function across different generators and also utility library functions.

1.5.1. Small examples

In the examples given below, we use the MRG31k3p from [4]. In general, a stream object contains three states: the initial state of the stream (or seed), the initial state of the current substream (by default it is equal to the seed), and the current state. With MRG31k3p, each state is comprised of six 31-bit integers. Each time a random number is generated, the current state advances by one position. There are also functions to reset the state to the initial one, or to the beginning of the current substream, or to the start of the next substream. Streams can be created and manipulated in arrays of arbitrary sizes. For a single stream, one uses an array of size 1. One can separately declare and allocate memory for an array of streams, create (initialize) the streams, clone them, copy them to preallocated space, etc.

1.5.2. Using streams on the host

We start with a small example in which we just create a few streams, then use them to generate numbers on the host computer and compute some quantity. This could be done as well by using only a single stream, but we use more just for the purpose of illustration.

The code includes the header for the MRG31k3p RNG.

#include <hcRNG/mrg31k3p.h>

We create an array of two streams named streams and a single stream named single.

hcrngMrg31k3pStream* streams = hcrngMrg31k3pCreateStreams(NULL, 2, NULL, NULL);
hcrngMrg31k3pStream* single = hcrngMrg31k3pCreateStreams(NULL, 1, NULL, NULL);

Then we repeat the following 100 times: we generate a uniform random number in (0,1) and an integer in {1,…,6}, and compute the indicator that the product is less than 2.

int count = 0;
for (int i = 0; i < 100; i++) {
  double u = hcrngMrg31k3pRandomU01(&streams[i % 2]);
  int x = hcrngMrg31k3pRandomInteger(single, 1, 6);
  if (x * u < 2) count++;
}

The uniform random numbers over (0,1) are generated by alternating the two streams from the array. We then print the average of those indicators.

printf("Average of indicators = %f\n", (double)count / 100.0);

1.5.3. Using streams in work items

In our second example, we create an array of streams and use them in work items that execute in parallel on a GPU device, one distinct stream per work item. Note that it is also possible (and sometimes useful) to use more than one stream per work item. We show only fragments of the code, to illustrate what we do with the streams. This code is only for illustration; the program does no useful computation.

In the code, we first include the hcRNG header for the MRG31k3p RNG:

#include <hcRNG/mrg31k3p.h>

Now suppose we have an integer variable numWorkItems that indicates the number of work items we want to use. We create an array of numWorkItems streams (and allocate memory for both the array and the stream objects). The creator returns in the variable streamBufferSize the size of the buffer that this array occupies (it depends on how much space is required to store the stream states), and an error code.

size_t streamBufferSize;
hcrngMrg31k3pStream* streams = hcrngMrg31k3pCreateStreams(NULL, numWorkItems, &streamBufferSize, (hcrngStatus *)&err);

Then we create an HCC buffer of size streamBufferSize and fill it with a copy of the array of streams, to pass to the device. We also create and pass a buffer that will be used by the device to return an array of numWorkItems values of type float.

Create buffer to transfer streams to the device. acc[1] denotes that a pointer is created in device (1st GPU). To create a pointer in device, it is mandatory to enumerate a list of accelerators and select the desired accelerator.

std::vector<hc::accelerator>acc = hc::accelerator::get_all();
accelerator_view accl_view = (acc[1].create_view());
hcrngMrg31k3pStream* buf_in = hc::am_alloc(sizeof(hcrngMrg31k3pStream) * numWorkItems, acc[1], 0);
hc::am_copy(streams_buffer, streams, numWorkItems * sizeof(hcrngMrg31k3pStream));

Create buffer to transfer output back from the device.

float* buf_out = hc::am_alloc(sizeof(float) * numberCount, acc[1], 0);

Call the kernel function. Kernels of type float and double has suffixes “_single” and “_double” respectively.

hcrngMrg31k3pDeviceRandomU01Array_single(accl_view, numWorkItems, buf_in, numberCount, buf_out);

The host can then recover the array of size numWorkItems that contains these outputs(numberCount). RNG-Specific API’s

hc::am_copy(RandomOutput, buf_out, numberCount * sizeof(float));

hcRNG_template describes the random streams API as it is intended to be implemented using different types of RNG’s or even using quasi-Monte Carlo (QMC) point sets.

In the description of this API, every data type and function name is assigned the prefix hcrng. It is understood that, in the implementation for each RNG type, the prefix hcrng is to be expanded with another prefix that indicates the type of RNG (or other method) used.

As this API is not polymorphic, replacing an RNG type with another one in client code requires changing the code to match hcRNG function names and data types to match those of the replacement RNG. We also intend to propose a generic (in the polymorphic sense) interface to the hcRNG library.

1.5.4. Stream Objects and Stream States

The library defines, among others, two closely related types of structures: stream objects (hcrngStream) and stream states (hcrngStreamState). The definitions of both structures depend on the specific type of RNG that they pertain to. Stream states correspond to the seeds of conventional RNG’s, to counter values in counter-based RNG’s, or to point and coordinate indices in QMC methods. Normally, the client should not deal with stream states directly, but use instead the higher-level stream objects. Stream objects are intended to store several stream states: the current and initial stream states, but also current substream state when support for substreams is available. Stream objects may also store other properties of the RNG, such as encryption keys for cryptography-based RNG’s.

1.5.5. Arrays of Stream Objects

Many functions are defined only for arrays of stream objects and not for single stream objects. It is always possible to use these functions for single stream objects by specifying a unit array size.

1.5.6. Defining Preprocessors

When a kernel is called, the stream states it needs are normally passed by the host and stored in global memory. If default settings are not suitable for the user’s needs, optional library behavior can be selected by defining specific preprocessor macros before including the hcRNG header. For example, to enable single precision on the device while using the MRG31k3p generator, use:

#define HCRNG_SINGLE_PRECISION