C++ 如何在OpenGL中测量峰值内存带宽?
为了了解我应该期望的速度,我一直在尝试对全局内存和着色器之间的传输进行基准测试,而不是依赖GPU规格表。但是我不能接近理论上的最大值。事实上,我被淘汰了50倍 我用的是GTX泰坦X,它是。Linux x64驱动程序352.21 我找到了一个CUDA基准测试,它提供了约240–250GB/s的速度(这比我预期的要高) 我正试图精确匹配它们对着色器的操作。我尝试过顶点着色器、计算着色器、通过访问缓冲区对象,以及使用C++ 如何在OpenGL中测量峰值内存带宽?,c++,opengl,glsl,nvidia,C++,Opengl,Glsl,Nvidia,为了了解我应该期望的速度,我一直在尝试对全局内存和着色器之间的传输进行基准测试,而不是依赖GPU规格表。但是我不能接近理论上的最大值。事实上,我被淘汰了50倍 我用的是GTX泰坦X,它是。Linux x64驱动程序352.21 我找到了一个CUDA基准测试,它提供了约240–250GB/s的速度(这比我预期的要高) 我正试图精确匹配它们对着色器的操作。我尝试过顶点着色器、计算着色器、通过访问缓冲区对象,以及使用floats、vec4s、着色器内的循环(使用工作组内的合并寻址)和各种计时方法。我被
float
s、vec4
s、着色器内的循环(使用工作组内的合并寻址)和各种计时方法。我被限制在~7GB/s(请参阅下面的更新)
为什么GL的速度要慢得多?我做错了什么吗?如果是,应该怎么做?
这是我的MWE,有三种方法(1.使用图像加载存储的顶点着色器,2.使用无绑定图形的顶点着色器,3.使用无绑定图形计算着色器):
一些注意事项:
glMapBuffer
检索数据,即使速度与其他方法匹配。在计算着色器中使用image_load_store可以工作,并提供与顶点着色器相同的速度(尽管我认为这可能是一个太多的排列)glDispatchCompute
之前调用glMemoryBarrier(GL\u ALL\u BARRIER\u BITS)
会导致驱动程序崩溃glBufferData(GL\u TEXTURE\u BUFFER,bufferSize,dat,GL\u STATIC\u DRAW)用于检查输出的代码>,将前两个测试的速度提高到17GB/s,计算着色器的速度猛增到292GB/s,这非常接近我想要的速度,但由于第1点的原因,这是不可信的
//http://www.ks.uiuc.edu/Research/vmd/doxygen/CUDABench_8cu-source.html
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cuda.h>
#define CUERR { cudaError_t err; \
if ((err = cudaGetLastError()) != cudaSuccess) { \
printf("CUDA error: %s, %s line %d\n", cudaGetErrorString(err), __FILE__, __LINE__); \
return -1; }}
//
// GPU device global memory bandwidth benchmark
//
template <class T>
__global__ void gpuglobmemcpybw(T *dest, const T *src) {
const unsigned int idx = threadIdx.x + blockIdx.x * blockDim.x;
dest[idx] = src[idx];
}
template <class T>
__global__ void gpuglobmemsetbw(T *dest, const T val) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
dest[idx] = val;
}
typedef float4 datatype;
static int cudaglobmembw(int cudadev, double *gpumemsetgbsec, double *gpumemcpygbsec) {
int i;
int len = 1 << 22; // one thread per data element
int loops = 500;
datatype *src, *dest;
datatype val=make_float4(1.0f, 1.0f, 1.0f, 1.0f);
// initialize to zero for starters
float memsettime = 0.0f;
float memcpytime = 0.0f;
*gpumemsetgbsec = 0.0;
*gpumemcpygbsec = 0.0;
// attach to the selected device
cudaError_t rc;
rc = cudaSetDevice(cudadev);
if (rc != cudaSuccess) {
#if CUDART_VERSION >= 2010
rc = cudaGetLastError(); // query last error and reset error state
if (rc != cudaErrorSetOnActiveProcess)
return -1; // abort and return an error
#else
cudaGetLastError(); // just ignore and reset error state, since older CUDA
// revs don't have a cudaErrorSetOnActiveProcess enum
#endif
}
cudaMalloc((void **) &src, sizeof(datatype)*len);
CUERR
cudaMalloc((void **) &dest, sizeof(datatype)*len);
CUERR
dim3 BSz(256, 1, 1);
dim3 GSz(len / (BSz.x * BSz.y * BSz.z), 1, 1);
// do a warm-up pass
gpuglobmemsetbw<datatype><<< GSz, BSz >>>(src, val);
CUERR
gpuglobmemsetbw<datatype><<< GSz, BSz >>>(dest, val);
CUERR
gpuglobmemcpybw<datatype><<< GSz, BSz >>>(dest, src);
CUERR
cudaEvent_t start, end;
cudaEventCreate(&start);
cudaEventCreate(&end);
// execute the memset kernel
cudaEventRecord(start, 0);
for (i=0; i<loops; i++) {
gpuglobmemsetbw<datatype><<< GSz, BSz >>>(dest, val);
}
CUERR
cudaEventRecord(end, 0);
CUERR
cudaEventSynchronize(start);
CUERR
cudaEventSynchronize(end);
CUERR
cudaEventElapsedTime(&memsettime, start, end);
CUERR
// execute the memcpy kernel
cudaEventRecord(start, 0);
for (i=0; i<loops; i++) {
gpuglobmemcpybw<datatype><<< GSz, BSz >>>(dest, src);
}
cudaEventRecord(end, 0);
CUERR
cudaEventSynchronize(start);
CUERR
cudaEventSynchronize(end);
CUERR
cudaEventElapsedTime(&memcpytime, start, end);
CUERR
cudaEventDestroy(start);
CUERR
cudaEventDestroy(end);
CUERR
*gpumemsetgbsec = (len * sizeof(datatype) / (1024.0 * 1024.0)) / (memsettime / loops);
*gpumemcpygbsec = (2 * len * sizeof(datatype) / (1024.0 * 1024.0)) / (memcpytime / loops);
cudaFree(dest);
cudaFree(src);
CUERR
return 0;
}
int main()
{
double a, b;
cudaglobmembw(0, &a, &b);
printf("%f %f\n", (float)a, (float)b);
return 0;
}
我现在通过使用图像加载存储或无绑定图形的计算着色器获得270–290GB/s现在我的问题包括:
- 假设每次测试都有缓冲区,并且计算着色器又好又快,为什么顶点着色器版本仍然如此缓慢
- 如果没有无绑定图形扩展,普通OpenGL用户应该如何将数据放入视频内存中(实际上是放入,而不是空泛地暗示驱动程序可能会喜欢) 我敢肯定,在现实世界中,我会注意到这个问题,而这个人为的基准测试遇到了一个缓慢的路径,那么我如何才能欺骗驱动程序使缓冲区对象驻留?首先运行计算着色器不会改变任何东西
dat
。这会导致大量缓存一致性通信。当GPU读取该内存时,它无法确定它是否是最新的,它可能在CPU缓存中,经过修改,尚未写回RAM。这导致GPU实际上必须从CPU缓存中读取数据,这比绕过CPU读取RAM要昂贵得多。RAM在正常运行期间通常处于空闲状态,因为现代CPU的命中率通常为95%到99%。缓存被连续使用
为了获得最大的性能,您需要让驱动程序分配内存。程序使用的常规内存(如全局变量和堆)在写回内存中分配。驱动程序分配的内存通常被分配为写合并或不缓存,这消除了一致性通信
只有在没有缓存一致性开销的情况下才能实现峰值公布带宽数
要让驱动程序分配数据,请对数据使用glBufferData
和nullptr
不过,如果您设法强迫驱动程序使用系统内存写入组合缓冲区,这并不完全是乐观的。CPU读取这些地址的速度将非常慢。顺序写入由CPU进行优化,但随机写入会导致写入组合缓冲区频繁刷新,从而影响性能。CUDA基准测试似乎在测量GPU本地内存带宽,而OpenGL基准测试确实测量PCI-E链路带宽,就像OpenGL驱动程序将对计算结果进行阴影复制一样。我会用计算着色器再试一次。@datenwolf谢谢你的兴趣。我想你的意思是CUDA正在测量GPU全局内存(内核中没有声明)。除非GPU虚拟化系统内存中的缓冲区对象,否则唯一的PCI-E数据传输来自实际基准之前的初始
glBufferData
s(并且glMapBuffer
未注释)。对于计算着色器的断言失败,我应该做些什么不同的事情吗?对于GPU local
,我的意思是“GPU板上本地安装的RAM”,而不是GPU连接到的系统内存(就GPU而言,它是非本地内存,B因为它不通过外围总线就无法访问它)。我不是指CUDA语义。您所引用的数字非常接近GPU本地内存访问与PCI-E链路带宽,这将是我要调查的第一件事。此外,OpenGL实现通常必须为规范实际实现所需的某些内容制作GPU到系统内存的卷影副本。@datenwolf真的!assert(glIsBufferResidentNV)
在测试失败之前。请参阅更新。顶点着色器仍然非常慢。可能gl\u VertexID
在工作组/warp中的着色器之间不是连续的,并且我达到了最差的缓存性能。感谢您的关注!我不相信glBufferData
会保持不变
image_load_store: 6.66GB/s
bindless: 6.68GB/s
bindless compute: 6.65GB/s
//http://www.ks.uiuc.edu/Research/vmd/doxygen/CUDABench_8cu-source.html
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cuda.h>
#define CUERR { cudaError_t err; \
if ((err = cudaGetLastError()) != cudaSuccess) { \
printf("CUDA error: %s, %s line %d\n", cudaGetErrorString(err), __FILE__, __LINE__); \
return -1; }}
//
// GPU device global memory bandwidth benchmark
//
template <class T>
__global__ void gpuglobmemcpybw(T *dest, const T *src) {
const unsigned int idx = threadIdx.x + blockIdx.x * blockDim.x;
dest[idx] = src[idx];
}
template <class T>
__global__ void gpuglobmemsetbw(T *dest, const T val) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
dest[idx] = val;
}
typedef float4 datatype;
static int cudaglobmembw(int cudadev, double *gpumemsetgbsec, double *gpumemcpygbsec) {
int i;
int len = 1 << 22; // one thread per data element
int loops = 500;
datatype *src, *dest;
datatype val=make_float4(1.0f, 1.0f, 1.0f, 1.0f);
// initialize to zero for starters
float memsettime = 0.0f;
float memcpytime = 0.0f;
*gpumemsetgbsec = 0.0;
*gpumemcpygbsec = 0.0;
// attach to the selected device
cudaError_t rc;
rc = cudaSetDevice(cudadev);
if (rc != cudaSuccess) {
#if CUDART_VERSION >= 2010
rc = cudaGetLastError(); // query last error and reset error state
if (rc != cudaErrorSetOnActiveProcess)
return -1; // abort and return an error
#else
cudaGetLastError(); // just ignore and reset error state, since older CUDA
// revs don't have a cudaErrorSetOnActiveProcess enum
#endif
}
cudaMalloc((void **) &src, sizeof(datatype)*len);
CUERR
cudaMalloc((void **) &dest, sizeof(datatype)*len);
CUERR
dim3 BSz(256, 1, 1);
dim3 GSz(len / (BSz.x * BSz.y * BSz.z), 1, 1);
// do a warm-up pass
gpuglobmemsetbw<datatype><<< GSz, BSz >>>(src, val);
CUERR
gpuglobmemsetbw<datatype><<< GSz, BSz >>>(dest, val);
CUERR
gpuglobmemcpybw<datatype><<< GSz, BSz >>>(dest, src);
CUERR
cudaEvent_t start, end;
cudaEventCreate(&start);
cudaEventCreate(&end);
// execute the memset kernel
cudaEventRecord(start, 0);
for (i=0; i<loops; i++) {
gpuglobmemsetbw<datatype><<< GSz, BSz >>>(dest, val);
}
CUERR
cudaEventRecord(end, 0);
CUERR
cudaEventSynchronize(start);
CUERR
cudaEventSynchronize(end);
CUERR
cudaEventElapsedTime(&memsettime, start, end);
CUERR
// execute the memcpy kernel
cudaEventRecord(start, 0);
for (i=0; i<loops; i++) {
gpuglobmemcpybw<datatype><<< GSz, BSz >>>(dest, src);
}
cudaEventRecord(end, 0);
CUERR
cudaEventSynchronize(start);
CUERR
cudaEventSynchronize(end);
CUERR
cudaEventElapsedTime(&memcpytime, start, end);
CUERR
cudaEventDestroy(start);
CUERR
cudaEventDestroy(end);
CUERR
*gpumemsetgbsec = (len * sizeof(datatype) / (1024.0 * 1024.0)) / (memsettime / loops);
*gpumemcpygbsec = (2 * len * sizeof(datatype) / (1024.0 * 1024.0)) / (memcpytime / loops);
cudaFree(dest);
cudaFree(src);
CUERR
return 0;
}
int main()
{
double a, b;
cudaglobmembw(0, &a, &b);
printf("%f %f\n", (float)a, (float)b);
return 0;
}
glBufferData(GL_TEXTURE_BUFFER, bufferSize, dat, GL_STATIC_DRAW);
glMakeBufferResidentNV(GL_TEXTURE_BUFFER, GL_READ_WRITE);
glGetBufferParameterui64vNV(GL_TEXTURE_BUFFER, GL_BUFFER_GPU_ADDRESS_NV, &address);
assert(glIsBufferResidentNV(GL_TEXTURE_BUFFER)); //sanity check