【发布时间】:2021-08-09 01:39:10
【问题描述】:
我在全局内存中有一个很长的 float2 数组,我的内核的每个块从中读取每个块中的 512 个连续值(但是对于任何给定块访问的特定块没有可识别的模式)。所以我在想我应该能够从全局内存中合并那些 512 个 float2 负载。为了做到这一点,我将指向 512 中第一个元素的指针加载到共享内存中,然后让每个线程将指针增加其 threadID 以从该地址读取 float2。但是,当我检查 Nsight Compute 时,这似乎并没有合并读取。我已经标记了下面的行,它说我有未合并的全局访问。
除了顺序访问之外,最佳实践指南还提到了对齐要求,但我使用的是矢量类型,指南说它是自动对齐的(除了它们对应于每个值 8 个字节之外)。所以我不确定为什么我的阅读没有合并。顶部读取的实际全局 L2 扇区与理想全局 L2 扇区的比率约为 1.12,底部读取为 1.09,我认为这还不错,但我仍然希望尽可能优化这些读取。
我上面没有提到的代码中的一个小问题是,这 512 次读取中的每一次都在循环中完成 nsegs 次,但我不确定这是否会影响读取模式。另一个奇怪的事情是,在我真正的完整代码中,Nsight 说我在第二次读取中已经取消合并读取(我通过 threadID 增加指针并读取该地址),但没有将从全局到共享内存的读取标记为未合并。与此不同的是,在下面的示例代码中,Nsight 将它们都标记为未合并读取。此外,我的完整代码使用 __shfl_down_sync warpReduce 方法(由 blockReduceSum 调用),但我在下面进行原子求和以缩短示例代码。
#include <stdio.h>
#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, const char *file, int line, bool abort = true) {
if (code != cudaSuccess) {
fprintf(stderr, "GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
if (abort) exit(code);
}
}
#define nbins 512
#define nsegs 340
#define ntemplates 100
__device__ float2 * d;
__global__ void kernel(float * d_all_sums, int * d_template_indices) {
float final = 0.0;
// blockIdx.x gives template index, threadIdx.x normally gets bin index but use it to mean segment index for loading global memory to shared.
__shared__ float2 * power_at_0_pointers[nsegs];
if (threadIdx.x < nsegs)
power_at_0_pointers[threadIdx.x] = &d[ __ldg(&d_template_indices[blockIdx.x * nsegs + threadIdx.x]) ]; // Uncoalesced here. Real to ideal ratio ~ 1.12.
__syncthreads();
__shared__ float power_sum;
for (int i = 0; i < nsegs; i++) {
__shared__ float powers[nbins];
float2 * pow_first_bin = power_at_0_pointers[i];
float2 input_power_c = *(pow_first_bin + threadIdx.x); // Uncoalesced here. Real to ideal ratio ~ 1.09.
float power = input_power_c.x * input_power_c.x + input_power_c.y * input_power_c.y;
power = (2 * power - 1.0) / 2.0;
powers[threadIdx.x] = power;
atomicAdd(&power_sum, powers[threadIdx.x]);
final += power / power_sum;
}
if (threadIdx.x == 0)
d_all_sums[blockIdx.x] = final;
}
int random(int min, int max){
return min + rand() / (RAND_MAX / (max - min + 1) + 1);
}
int main(){
float2 * h;
float * h_all_sums;
float2 * d_ptr;
float * d_all_sums;
int * h_template_indices, * d_template_indices;
h_template_indices = (int *) malloc(ntemplates * nsegs * sizeof(int));
h = (float2 *) malloc(nbins * nsegs * ntemplates * sizeof(float2));
h_all_sums = (float *) malloc(ntemplates * sizeof(float));
memset(h_all_sums, 0.0, ntemplates);
for (int k = 0; k < ntemplates; k++) {
for (int i = 0; i < nsegs; i++) {
h_template_indices[k * nsegs + i] = random(0, ntemplates * nsegs - nbins);
for (int j = 0; j < nbins; j++)
h[k * nbins * nsegs + i * nbins + j] = make_float2(100 * (float) rand() / (float)(RAND_MAX), 100 * (float) rand() / (float)(RAND_MAX));
}
}
gpuErrchk( cudaMalloc((void**) &d_ptr, nbins * nsegs * ntemplates * sizeof(float2)) );
gpuErrchk( cudaMemcpy(d_ptr, h, nbins * nsegs * ntemplates * sizeof(float2), cudaMemcpyHostToDevice) );
gpuErrchk( cudaMemcpyToSymbol(d, &d_ptr, sizeof(float2*)) );
gpuErrchk( cudaMalloc( (void**) &d_all_sums, ntemplates * sizeof(float) ) );
gpuErrchk( cudaMalloc((void**) &d_template_indices, nsegs * ntemplates * sizeof(int)) );
gpuErrchk( cudaMemcpy(d_template_indices, h_template_indices, nsegs * ntemplates * sizeof(int), cudaMemcpyHostToDevice) );
kernel<<<ntemplates, nbins>>>(d_all_sums, d_template_indices);
gpuErrchk( cudaMemcpy(h_all_sums, d_all_sums, ntemplates * sizeof(float), cudaMemcpyDeviceToHost) );
FILE *f = fopen("test_output.txt", "w");
if (f != NULL) {
for (int k = 0; k < ntemplates; k++)
fprintf(f, "k = %d; power = %f.\n", k, h_all_sums[k]);
}
fclose( f );
gpuErrchk( cudaFree(d_ptr) );
gpuErrchk( cudaFree(d_all_sums) );
gpuErrchk( cudaFree(d_template_indices) );
free( h );
free( h_all_sums );
free( h_template_indices );
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );
printf("All done.\n");
}
编辑:我在下面包含了 Nsight Compute 关于合并的结果。我还包含了与我的完整代码更接近的代码,就如何完成块减少(随机播放而不是原子)而言。随着这一变化,现在 Nsight 说我只能在一个站点进行未合并的访问(图书馆中的另一个站点,但这显然超出了我的控制范围)。由于某种原因,求和/归约方法的差异似乎使从全局到共享内存的读取合并?
#include <stdio.h>
#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, const char *file, int line, bool abort = true) {
if (code != cudaSuccess) {
fprintf(stderr, "GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
if (abort) exit(code);
}
}
__inline__ __device__ float warpReduceSum(float val) {
for (int offset = warpSize/2; offset > 0; offset /= 2)
val += __shfl_down_sync(0xffffffff, val, offset);
return val;
}
__inline__ __device__ float blockReduceSum(float val) {
static __shared__ float shared[32];
int lane = threadIdx.x % warpSize;
int wid = threadIdx.x / warpSize;
val = warpReduceSum(val);
if (lane == 0) shared[wid] = val;
__syncthreads();
val = (threadIdx.x < blockDim.x / warpSize) ? shared[lane] : float(0.0);
if (wid == 0) val = warpReduceSum(val);
return val;
}
#define nbins 512
#define nsegs 340
#define ntemplates 100
__device__ float2 * d;
__global__ void kernel(float * d_all_sums, int * d_template_indices) {
float final = 0.0;
// blockIdx.x gives template index, threadIdx.x normally gets bin index but use it to mean segment index for loading global memory to shared.
__shared__ float2 * power_at_0_pointers[nsegs];
if (threadIdx.x < nsegs)
power_at_0_pointers[threadIdx.x] = &d[ __ldg(&d_template_indices[blockIdx.x * nsegs + threadIdx.x]) ];
__syncthreads();
for (int i = 0; i < nsegs; i++) {
__shared__ float powers[nbins];
float2 * pow_first_bin = power_at_0_pointers[i];
float2 input_power_c = *(pow_first_bin + threadIdx.x); // Uncoalesced here.
float power = input_power_c.x * input_power_c.x + input_power_c.y * input_power_c.y;
power = (2 * power - 1.0) / 2.0;
powers[threadIdx.x] = 1.0;
float power_sum = blockReduceSum(powers[threadIdx.x]);
final += power / power_sum;
}
if (threadIdx.x == 0)
d_all_sums[blockIdx.x] = final;
}
int random(int min, int max){
return min + rand() / (RAND_MAX / (max - min + 1) + 1);
}
int main(){
float2 * h;
float * h_all_sums;
float2 * d_ptr;
float * d_all_sums;
int * h_template_indices, * d_template_indices;
h_template_indices = (int *) malloc(ntemplates * nsegs * sizeof(int));
h = (float2 *) malloc(nbins * nsegs * ntemplates * sizeof(float2));
h_all_sums = (float *) malloc(ntemplates * sizeof(float));
memset(h_all_sums, 0.0, ntemplates);
for (int k = 0; k < ntemplates; k++) {
for (int i = 0; i < nsegs; i++) {
h_template_indices[k * nsegs + i] = random(0, ntemplates * nsegs - nbins);
for (int j = 0; j < nbins; j++)
h[k * nbins * nsegs + i * nbins + j] = make_float2(100 * (float) rand() / (float)(RAND_MAX), 100 * (float) rand() / (float)(RAND_MAX));
}
}
gpuErrchk( cudaMalloc((void**) &d_ptr, nbins * nsegs * ntemplates * sizeof(float2)) );
gpuErrchk( cudaMemcpy(d_ptr, h, nbins * nsegs * ntemplates * sizeof(float2), cudaMemcpyHostToDevice) );
gpuErrchk( cudaMemcpyToSymbol(d, &d_ptr, sizeof(float2*)) );
gpuErrchk( cudaMalloc( (void**) &d_all_sums, ntemplates * sizeof(float) ) );
gpuErrchk( cudaMalloc((void**) &d_template_indices, nsegs * ntemplates * sizeof(int)) );
gpuErrchk( cudaMemcpy(d_template_indices, h_template_indices, nsegs * ntemplates * sizeof(int), cudaMemcpyHostToDevice) );
kernel<<<ntemplates, nbins>>>(d_all_sums, d_template_indices);
gpuErrchk( cudaMemcpy(h_all_sums, d_all_sums, ntemplates * sizeof(float), cudaMemcpyDeviceToHost) );
FILE *f = fopen("test_output.txt", "w");
if (f != NULL) {
for (int k = 0; k < ntemplates; k++)
fprintf(f, "k = %d; power = %f.\n", k, h_all_sums[k]);
}
fclose( f );
gpuErrchk( cudaFree(d_ptr) );
gpuErrchk( cudaFree(d_all_sums) );
gpuErrchk( cudaFree(d_template_indices) );
free( h );
free( h_all_sums );
free( h_template_indices );
gpuErrchk( cudaPeekAtLastError() );
gpuErrchk( cudaDeviceSynchronize() );
printf("All done.\n");
}
EDIT2:按要求添加内存图表和表格。
【问题讨论】:
-
能否附上 nsight 内存图表(可能还有表格)的屏幕截图,好吗?那么我们可以更好地说,如果有真正的优化潜力。 1.09 和 1.12 已经相当理想了。
-
确定索引后(在代码中随机),您可以将 512 个 float2 段复制到与 DRAM 段对齐的单独位置。那么您的效率将为 1,但对于最初的重叠,现在单独的 segs 您的缓存效率会更差。
-
谢谢!我在上面添加了内存图表和表格。
-
总的来说,您的内存性能非常好(瓶颈在其他地方)。几乎所有访问都由 L1 和 L2 缓存提供服务。由于未对齐而导致的 546200 次全局内存访问(循环中的指令中的 544000 次)被“吹”成 650501 个波前,并且它们大部分(> 70% 的 32 字节宽扇区是 L1 缓存未命中)被转发到 L2 缓存. L2 缓存线宽 128 字节,访问 (32 * float2) 256 字节。您最终会收到 1173730 个 L2 请求,几乎所有请求都得到了服务。加上几家商店,L2 缓存占用率为 12.84%。
-
内存性能不是问题。您可以优化其他瓶颈以将性能提高近 8 倍。之后,L2 缓存将受到限制。为了进一步优化,您应该尝试为 L1 缓存获得更好的命中率或使用共享内存进行数据重用。这将限于 85 倍的性能提升(您的 SM 多处理器的加载/存储管道占用 1.16%)。只有这样,您才能通过尝试优化单个内存指令(对齐、使用 float4 等)获得一些东西。但在此之前,您会遇到 GPU 的算术峰值性能。
标签: cuda