我目前正在CUDA中编写蒙特卡罗模拟。因此,我需要使用cuRAND
库动态生成 lot 随机数。
每个线程处理一个巨大的float
数组中的一个元素(在示例中省略),并为每个内核调用生成1或2个随机数。
通常的方法(参见下面的示例)似乎是为每个线程分配一个状态。状态在后续内核调用中重用。 但是,当线程数增加时(在我的应用程序中最多10⁸),这不能很好地扩展,因为它成为程序中主要的内存(和带宽)使用。
我知道一种可能的方法是以grid stride loop方式处理每个线程的多个数组元素。在这里,我想研究一种不同的方法。
我知道对于我所针对的计算能力(3.5),每个SM的最大常驻线程数为2048,即下面的示例中的2个块。 /> 是否可以在每个多处理器中仅使用2048个状态,而不管线程总数? 生成的所有随机数仍应在统计上独立。
我认为如果每个驻留线程都与一个唯一的索引模2048相关联,则可以这样做,然后可以使用该索引从数组中获取状态。 这样的索引是否存在?
更一般地说,还有其他方法可以减少RNG状态的内存占用吗?
#include <cuda.h>
#include <curand.h>
#include <curand_kernel.h>
#include <assert.h>
#define gridSize 100
#define blockSize 1024
#define iter 100
__global__ void rng_init(unsigned long long seed, curandState * states) {
const size_t Idx = blockIdx.x * blockDim.x + threadIdx.x;
curand_init(seed, Idx, 0, &states[Idx]);
}
__global__ void kernel(curandState * states) {
const size_t Idx = blockIdx.x * blockDim.x + threadIdx.x;
const float x = curand_uniform(&states[Idx]);
// Do stuff with x ...
}
int main() {
curandState * states;
assert(cudaMalloc(&states, gridSize*blockSize*sizeof(curandState)) == cudaSuccess);
rng_init<<<gridSize,blockSize>>>(clock(), states);
assert(cudaDeviceSynchronize() == cudaSuccess);
for (size_t i = 0 ; i < iter ; ++i) {
kernel<<<gridSize,blockSize>>>(states);
assert(cudaDeviceSynchronize() == cudaSuccess);
}
return 0;
}
答案 0 :(得分:0)
根据RNG类型,您可以使用多种方法。在你的情况下,如果你对rng类型有一点自由,如果你有一个具有良好双精度的设备,你可能想要完全删除存储的rng状态的概念并调用init和跳过技术。然后,唯一需要的元素是种子和可以根据迭代和模拟ID计算的索引。
请参阅cuRand文档的Skip Ahead部分,并了解大多数curand_init
method接受偏移参数。在某些情况下,鉴于RNG状态结构的性质以及init的小成本,最好使用可能位于寄存器空间中的状态数据结构上的适当偏移调用cuda_init
而不是加载/在每个随机值提取中存储来自全局内存的状态数据结构。
答案 1 :(得分:0)
简而言之,在您的问题中,您提到使用网格跨越循环作为折叠所需状态的方法。我认为这种方法,或类似的方法(由@talonmies建议)是最明智的方法。选择一种方法,将线程数减少到保持机器忙/充分利用所需的最小值。然后让每个线程计算多个操作,重新使用提供的随机生成器状态。
我从你的代码shell开始,把它变成了一个经典的“hello world”MC问题:根据方形区域与内切圆区域的比率计算pi,随机生成点来估计区域。 / p>
然后我们将考虑3种方法:
创建一个大的1D网格,以及每个线程的状态,以便每个线程计算一个随机点并对其进行测试。
创建一个小得多的1D网格,以及每个线程的状态,并允许每个线程计算多个随机点/测试,以便生成与情况1中相同数量的点/测试。 / p>
创建一个与方法2大小相同的网格,但也创建一个“唯一的驻留线程索引”,然后提供足够的状态来覆盖唯一的常驻线程。每个线程将有效地使用使用“驻留线程索引”提供的状态,而不是普通的全局唯一线程索引。计算与案例1中相同数量的点/测试。
“唯一驻留线程索引”的计算不是一件小事。在主机代码中:
我们必须确定理论上可能驻留的最大块数。我已经使用了一个简单的启发式方法,但可以说有更好的方法。我简单地将每个多处理器的最大驻留线程除以每个块所选线程的数量。我们必须使用整数除法。
然后初始化足够的状态以覆盖最大块数乘以GPU上SM的数量。
在设备代码中:
threadIdx.x
变量以创建“唯一驻留线程索引”。这将成为我们典型的线程索引,而不是通常的计算。从结果的角度来看,可以进行以下观察:
与其他答案中所述相反,初始时间不无关紧要。不应该忽视它。对于这个简单的问题,init内核运行时超过了计算内核的运行时间。因此,我们最重要的要点是我们应该寻求最小化随机发生器状态的创建的方法。我们当然不希望不必要地重新运行初始化。因此,对于这个特定的代码/测试,我们应该根据这些结果丢弃方法1.
从计算内核运行时的角度来看,没有什么可以推荐一个内核而不是另一个内核。
从代码复杂性的角度来看,方法2显然不如方法3复杂,同时提供相同的性能。
对于这个特定的测试案例,方法2似乎是胜利者,正如@talonmies预测的那样。
以下是3种方法的实例。我没有声称这对每个案例,代码或场景都是无缺陷的或有启发性的。这里有很多活动部分,但我相信上面的3个结论对于这种情况是有效的。
$ cat t1201.cu
#include <cuda.h>
#include <curand.h>
#include <curand_kernel.h>
#include <assert.h>
#include <iostream>
#include <stdlib.h>
#define blockSize 1024
#include <time.h>
#include <sys/time.h>
#define USECPSEC 1000000ULL
#define MAX_SM 64
unsigned long long dtime_usec(unsigned long long start){
timeval tv;
gettimeofday(&tv, 0);
return ((tv.tv_sec*USECPSEC)+tv.tv_usec)-start;
}
__device__ unsigned long long count = 0;
__device__ unsigned int blk_ids[MAX_SM] = {0};
__global__ void rng_init(unsigned long long seed, curandState * states) {
const size_t Idx = blockIdx.x * blockDim.x + threadIdx.x;
curand_init(seed, Idx, 0, &states[Idx]);
}
__global__ void kernel(curandState * states, int length) {
const size_t Idx = blockIdx.x * blockDim.x + threadIdx.x;
for (int i = 0; i < length; i++){
const float x = curand_uniform(&states[Idx]);
const float y = curand_uniform(&states[Idx]);
if (sqrtf(x*x+y*y)<1.0)
atomicAdd(&count, 1ULL);}
}
static __device__ __inline__ int __mysmid(){
int smid;
asm volatile("mov.u32 %0, %%smid;" : "=r"(smid));
return smid;}
__device__ int get_my_resident_thread_id(int sm_blk_id){
return __mysmid()*sm_blk_id + threadIdx.x;
}
__device__ int get_block_id(){
int my_sm = __mysmid();
int my_block_id = -1;
bool done = false;
int i = 0;
while ((!done)&&(i<32)){
unsigned int block_flag = 1<<i;
if ((atomicOr(blk_ids+my_sm, block_flag)&block_flag) == 0){my_block_id = i; done = true;}
i++;}
return my_block_id;
}
__device__ void release_block_id(int block_id){
unsigned int block_mask = ~(1<<block_id);
int my_sm = __mysmid();
atomicAnd(blk_ids+my_sm, block_mask);
}
__global__ void kernel2(curandState * states, int length) {
__shared__ volatile int my_block_id;
if (!threadIdx.x) my_block_id = get_block_id();
__syncthreads();
const size_t Idx = get_my_resident_thread_id(my_block_id);
for (int i = 0; i < length; i++){
const float x = curand_uniform(&states[Idx]);
const float y = curand_uniform(&states[Idx]);
if (sqrtf(x*x+y*y)<1.0)
atomicAdd(&count, 1ULL);}
__syncthreads();
if (!threadIdx.x) release_block_id(my_block_id);
__syncthreads();
}
int main(int argc, char *argv[]) {
int gridSize = 10;
if (argc > 1) gridSize = atoi(argv[1]);
curandState * states;
assert(cudaMalloc(&states, gridSize*gridSize*blockSize*sizeof(curandState)) == cudaSuccess);
unsigned long long hcount;
//warm - up
rng_init<<<gridSize*gridSize,blockSize>>>(1234ULL, states);
assert(cudaDeviceSynchronize() == cudaSuccess);
//method 1: 1 curand state per point
std::cout << "Method 1 init blocks: " << gridSize*gridSize << std::endl;
unsigned long long dtime = dtime_usec(0);
rng_init<<<gridSize*gridSize,blockSize>>>(1234ULL, states);
assert(cudaDeviceSynchronize() == cudaSuccess);
unsigned long long initt = dtime_usec(dtime);
kernel<<<gridSize*gridSize,blockSize>>>(states, 1);
assert(cudaDeviceSynchronize() == cudaSuccess);
dtime = dtime_usec(dtime) - initt;
cudaMemcpyFromSymbol(&hcount, count, sizeof(unsigned long long));
std::cout << "method 1 elapsed time: " << dtime/(float)USECPSEC << " init time: " << initt/(float)USECPSEC << " pi: " << 4.0f*hcount/(float)(gridSize*gridSize*blockSize) << std::endl;
hcount = 0;
cudaMemcpyToSymbol(count, &hcount, sizeof(unsigned long long));
//method 2: 1 curand state per gridSize points
std::cout << "Method 2 init blocks: " << gridSize << std::endl;
dtime = dtime_usec(0);
rng_init<<<gridSize,blockSize>>>(1234ULL, states);
assert(cudaDeviceSynchronize() == cudaSuccess);
initt = dtime_usec(dtime);
kernel<<<gridSize,blockSize>>>(states, gridSize);
assert(cudaDeviceSynchronize() == cudaSuccess);
dtime = dtime_usec(dtime) - initt;
cudaMemcpyFromSymbol(&hcount, count, sizeof(unsigned long long));
std::cout << "method 2 elapsed time: " << dtime/(float)USECPSEC << " init time: " << initt/(float)USECPSEC << " pi: " << 4.0f*hcount/(float)(gridSize*gridSize*blockSize) << std::endl;
hcount = 0;
cudaMemcpyToSymbol(count, &hcount, sizeof(unsigned long long));
//method 3: 1 curand state per resident thread
// compute the maximum number of state entries needed
int num_sms;
cudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0);
int max_sm_threads;
cudaDeviceGetAttribute(&max_sm_threads, cudaDevAttrMaxThreadsPerMultiProcessor, 0);
int max_blocks = max_sm_threads/blockSize;
int total_state = max_blocks*num_sms*blockSize;
int rgridSize = (total_state + blockSize-1)/blockSize;
std::cout << "Method 3 sms: " << num_sms << " init blocks: " << rgridSize << std::endl;
// run test
dtime = dtime_usec(0);
rng_init<<<rgridSize,blockSize>>>(1234ULL, states);
assert(cudaDeviceSynchronize() == cudaSuccess);
initt = dtime_usec(dtime);
kernel2<<<gridSize,blockSize>>>(states, gridSize);
assert(cudaDeviceSynchronize() == cudaSuccess);
dtime = dtime_usec(dtime) - initt;
cudaMemcpyFromSymbol(&hcount, count, sizeof(unsigned long long));
std::cout << "method 3 elapsed time: " << dtime/(float)USECPSEC << " init time: " << initt/(float)USECPSEC << " pi: " << 4.0f*hcount/(float)(gridSize*gridSize*blockSize) << std::endl;
hcount = 0;
cudaMemcpyToSymbol(count, &hcount, sizeof(unsigned long long));
return 0;
}
$ nvcc -arch=sm_35 -O3 -o t1201 t1201.cu
$ ./t1201 28
Method 1 init blocks: 784
method 1 elapsed time: 0.001218 init time: 3.91075 pi: 3.14019
Method 2 init blocks: 28
method 2 elapsed time: 0.00117 init time: 0.003629 pi: 3.14013
Method 3 sms: 14 init blocks: 28
method 3 elapsed time: 0.001193 init time: 0.003622 pi: 3.1407
$