11.1 概述
- 了解如何分配和使用零拷贝内存(zero-copy memory)
- 了解如何在同一个应用程序中使用多个GPU
- 了解如何分配和使用可移动的固定内存(Portable pinned Memory)
11.2 零拷贝主机内存
固定内存(页锁定内存)能确保不会交换出物理内存。我们通过调用cudaHostAlloc()来分配这种内存,并且传递参数cudaHostAllocDefault()来获得默认的固定内存。
分配固定内存,还可以使用其他参数,除了cudaHostAllocDefault还有cudaHostAllocMapped。xxMapped参数也不能从物理内存中交换出去或者重新定位,但是这种内存除了用于主机和GPU之间的内存复制外, 还可以打破第三章中主机内存规则之一:
可以在CUDA C核函数中直接访问这种类型的主机内存。由于这种内存不需要复制到GPU, 因此也叫零拷贝内存。
11.2.1 通过零拷贝内存实现点积运算
11.2.2 零拷贝内存的性能
对于独立GPU和集成GPU是不同的。对于集成GPU,使用零拷贝内存通常都会带来性能提升,因为内存在物理上与主机是共享的。将缓冲区声明为零拷贝内存的唯一作用就是避免不必要的数据复制。和固定内存一样,占用系统的可用物理内存,最终会降低系统的性能。
当输入内存和输出内存都只使用一次时,那么在独立GPU上使用零拷贝内存将带来性能提升。如果多次读取内存,那么僵得不偿失,还不如一开始就将数据复制到GPU。
11.3 使用多个GPU
将多个GPU添加到独立的PCIE槽上,通过NVIDIA的SLI技术将他们桥接。
11.4 可移动的固定内存
通过调用cudaHostAlloc()来分配这种内存,并使用cudaHostAllocPortalbe标志表示该固定内存为可移动。
portable.cu
#include "../common/book.h"
#define imin(a,b) (a<b?a:b)
#define N (33*1024*1024)
const int threadsPerBlock = 256;
const int blocksPerGrid =
imin( 32, (N/2+threadsPerBlock-1) / threadsPerBlock );
__global__ void dot( int size, float *a, float *b, float *c ) {
__shared__ float cache[threadsPerBlock];
int tid = threadIdx.x + blockIdx.x * blockDim.x;
int cacheIndex = threadIdx.x;
float temp = 0;
while (tid < size) {
temp += a[tid] * b[tid];
tid += blockDim.x * gridDim.x;
}
// set the cache values
cache[cacheIndex] = temp;
// synchronize threads in this block
__syncthreads();
// for reductions, threadsPerBlock must be a power of 2
// because of the following code
int i = blockDim.x/2;
while (i != 0) {
if (cacheIndex < i)
cache[cacheIndex] += cache[cacheIndex + i];
__syncthreads();
i /= 2;
}
if (cacheIndex == 0)
c[blockIdx.x] = cache[0];
}
struct DataStruct {
int deviceID;
int size;
int offset;
float *a;
float *b;
float returnValue;
};
void* routine( void *pvoidData ) {
DataStruct *data = (DataStruct*)pvoidData;
if (data->deviceID != 0) {
HANDLE_ERROR( cudaSetDevice( data->deviceID ) );
HANDLE_ERROR( cudaSetDeviceFlags( cudaDeviceMapHost ) );
}
int size = data->size;
float *a, *b, c, *partial_c;
float *dev_a, *dev_b, *dev_partial_c;
// allocate memory on the CPU side
a = data->a;
b = data->b;
partial_c = (float*)malloc( blocksPerGrid*sizeof(float) );
// allocate the memory on the GPU
HANDLE_ERROR( cudaHostGetDevicePointer( &dev_a, a, 0 ) );
HANDLE_ERROR( cudaHostGetDevicePointer( &dev_b, b, 0 ) );
HANDLE_ERROR( cudaMalloc( (void**)&dev_partial_c,
blocksPerGrid*sizeof(float) ) );
// offset 'a' and 'b' to where this GPU is gets it data
dev_a += data->offset;
dev_b += data->offset;
dot<<<blocksPerGrid,threadsPerBlock>>>( size, dev_a, dev_b,
dev_partial_c );
// copy the array 'c' back from the GPU to the CPU
HANDLE_ERROR( cudaMemcpy( partial_c, dev_partial_c,
blocksPerGrid*sizeof(float),
cudaMemcpyDeviceToHost ) );
// finish up on the CPU side
c = 0;
for (int i=0; i<blocksPerGrid; i++) {
c += partial_c[i];
}
HANDLE_ERROR( cudaFree( dev_partial_c ) );
// free memory on the CPU side
free( partial_c );
data->returnValue = c;
return 0;
}
int main( void ) {
int deviceCount;
HANDLE_ERROR( cudaGetDeviceCount( &deviceCount ) );
if (deviceCount < 2) {
printf( "We need at least two compute 1.0 or greater "
"devices, but only found %d\n", deviceCount );
return 0;
}
cudaDeviceProp prop;
for (int i=0; i<2; i++) {
HANDLE_ERROR( cudaGetDeviceProperties( &prop, i ) );
if (prop.canMapHostMemory != 1) {
printf( "Device %d can not map memory.\n", i );
return 0;
}
}
float *a, *b;
HANDLE_ERROR( cudaSetDevice( 0 ) );
HANDLE_ERROR( cudaSetDeviceFlags( cudaDeviceMapHost ) );
HANDLE_ERROR( cudaHostAlloc( (void**)&a, N*sizeof(float),
cudaHostAllocWriteCombined |
cudaHostAllocPortable |
cudaHostAllocMapped ) );
HANDLE_ERROR( cudaHostAlloc( (void**)&b, N*sizeof(float),
cudaHostAllocWriteCombined |
cudaHostAllocPortable |
cudaHostAllocMapped ) );
// fill in the host memory with data
for (int i=0; i<N; i++) {
a[i] = i;
b[i] = i*2;
}
// prepare for multithread
DataStruct data[2];
data[0].deviceID = 0;
data[0].offset = 0;
data[0].size = N/2;
data[0].a = a;
data[0].b = b;
data[1].deviceID = 1;
data[1].offset = N/2;
data[1].size = N/2;
data[1].a = a;
data[1].b = b;
CUTThread thread = start_thread( routine, &(data[1]) );
routine( &(data[0]) );
end_thread( thread );
// free memory on the CPU side
HANDLE_ERROR( cudaFreeHost( a ) );
HANDLE_ERROR( cudaFreeHost( b ) );
printf( "Value calculated: %f\n",
data[0].returnValue + data[1].returnValue );
return 0;
}
11.5 小结
本章介绍了一些新的主机内存分配方式,所有这些内存都是通过cudaHostAlloc()来分配的。通过使用不同的标志,可以分配具备零拷贝、可移动或者合并时写入等属性的内存。
使用零拷贝内存,可以避免CPU和GPU之间的显式复制操作。
使用支持线程的库,可以在同一个应用程序中对对个GPU进行操作,使点积运算能够跨越多个设备执行。
最后,通过可移动的固定内存,使用多个GPU共享固定内存。