OpenCL 'non-blocking' 读取的成本高于预期
OpenCL 'non-blocking' reads have higher cost than expected
考虑以下代码,它在 1 到 100000 之间排队 'non-blocking' 随机访问缓冲区读取并测量时间:
#define __CL_ENABLE_EXCEPTIONS
#include <CL/cl.hpp>
#include <vector>
#include <iostream>
#include <chrono>
#include <stdio.h>
static const int size = 100000;
int host_buf[size];
int main() {
cl::Context ctx(CL_DEVICE_TYPE_DEFAULT, nullptr, nullptr, nullptr);
std::vector<cl::Device> devices;
ctx.getInfo(CL_CONTEXT_DEVICES, &devices);
printf("Using OpenCL devices: \n");
for (auto &dev : devices) {
std::string dev_name = dev.getInfo<CL_DEVICE_NAME>();
printf(" %s\n", dev_name.c_str());
}
cl::CommandQueue queue(ctx);
cl::Buffer gpu_buf(ctx, CL_MEM_READ_WRITE, sizeof(int) * size, nullptr, nullptr);
std::vector<int> values(size);
// Warmup
queue.enqueueReadBuffer(gpu_buf, false, 0, sizeof(int), &(host_buf[0]));
queue.finish();
// Run from 1 to 100000 sized chunks
for (int k = 1; k <= size; k *= 10) {
auto cstart = std::chrono::high_resolution_clock::now();
for (int j = 0; j < k; j++)
queue.enqueueReadBuffer(gpu_buf, false, sizeof(int) * (j * (size / k)), sizeof(int), &(host_buf[j]));
queue.finish();
auto cend = std::chrono::high_resolution_clock::now();
double time = std::chrono::duration<double>(cend - cstart).count() * 1000000.0;
printf("%8d: %8.02f us\n", k, time);
}
return 0;
}
一如既往,有一些随机变化,但对我来说典型的输出是这样的:
1: 10.03 us
10: 107.93 us
100: 794.54 us
1000: 8301.35 us
10000: 83741.06 us
100000: 981607.26 us
虽然我确实预计单次读取会有相对较高的延迟,但考虑到需要 PCIe 往返,我对将后续读取添加到队列的高成本感到惊讶 - 好像真的没有'queue',但每次读取都会增加完整的延迟惩罚。这是在 Linux 和驱动程序版本 455.45.01 的 GTX 960 上。
- 这是预期的行为吗?
- 其他 GPU 的行为是否相同?
- 除了始终从内核内部进行随机访问读取之外,是否有任何解决方法?
您正在使用单个顺序命令队列。因此,所有排队的读取都由硬件/驱动程序按顺序执行。
'non-blocking' 方面仅表示调用本身是异步的,并且在 GPU 工作时不会阻塞您的主机代码。
在您的代码中,您使用 clFinish
阻塞直到所有读取完成。
所以是的,这是预期的行为。您将为每次 DMA 传输支付全额罚金。
只要您创建有序命令队列(默认),其他 GPU 的行为相同。
如果您的硬件/驱动程序支持无序队列,您可以使用它们来潜在地重叠 DMA 传输。或者,您可以使用多个有序队列。但性能当然取决于硬件和驱动程序。
使用多队列/无序队列更高级一些。您应该确保正确利用事件以避免竞争条件或导致未定义的行为。
为了减少与 GPU 主机 DMA 传输相关的延迟,建议您使用固定主机缓冲区而不是 std::vector
。固定主机缓冲区通常通过 clCreateBuffer
和 CL_MEM_ALLOC_HOST_PTR
标志创建。
考虑以下代码,它在 1 到 100000 之间排队 'non-blocking' 随机访问缓冲区读取并测量时间:
#define __CL_ENABLE_EXCEPTIONS
#include <CL/cl.hpp>
#include <vector>
#include <iostream>
#include <chrono>
#include <stdio.h>
static const int size = 100000;
int host_buf[size];
int main() {
cl::Context ctx(CL_DEVICE_TYPE_DEFAULT, nullptr, nullptr, nullptr);
std::vector<cl::Device> devices;
ctx.getInfo(CL_CONTEXT_DEVICES, &devices);
printf("Using OpenCL devices: \n");
for (auto &dev : devices) {
std::string dev_name = dev.getInfo<CL_DEVICE_NAME>();
printf(" %s\n", dev_name.c_str());
}
cl::CommandQueue queue(ctx);
cl::Buffer gpu_buf(ctx, CL_MEM_READ_WRITE, sizeof(int) * size, nullptr, nullptr);
std::vector<int> values(size);
// Warmup
queue.enqueueReadBuffer(gpu_buf, false, 0, sizeof(int), &(host_buf[0]));
queue.finish();
// Run from 1 to 100000 sized chunks
for (int k = 1; k <= size; k *= 10) {
auto cstart = std::chrono::high_resolution_clock::now();
for (int j = 0; j < k; j++)
queue.enqueueReadBuffer(gpu_buf, false, sizeof(int) * (j * (size / k)), sizeof(int), &(host_buf[j]));
queue.finish();
auto cend = std::chrono::high_resolution_clock::now();
double time = std::chrono::duration<double>(cend - cstart).count() * 1000000.0;
printf("%8d: %8.02f us\n", k, time);
}
return 0;
}
一如既往,有一些随机变化,但对我来说典型的输出是这样的:
1: 10.03 us
10: 107.93 us
100: 794.54 us
1000: 8301.35 us
10000: 83741.06 us
100000: 981607.26 us
虽然我确实预计单次读取会有相对较高的延迟,但考虑到需要 PCIe 往返,我对将后续读取添加到队列的高成本感到惊讶 - 好像真的没有'queue',但每次读取都会增加完整的延迟惩罚。这是在 Linux 和驱动程序版本 455.45.01 的 GTX 960 上。
- 这是预期的行为吗?
- 其他 GPU 的行为是否相同?
- 除了始终从内核内部进行随机访问读取之外,是否有任何解决方法?
您正在使用单个顺序命令队列。因此,所有排队的读取都由硬件/驱动程序按顺序执行。
'non-blocking' 方面仅表示调用本身是异步的,并且在 GPU 工作时不会阻塞您的主机代码。
在您的代码中,您使用 clFinish
阻塞直到所有读取完成。
所以是的,这是预期的行为。您将为每次 DMA 传输支付全额罚金。
只要您创建有序命令队列(默认),其他 GPU 的行为相同。
如果您的硬件/驱动程序支持无序队列,您可以使用它们来潜在地重叠 DMA 传输。或者,您可以使用多个有序队列。但性能当然取决于硬件和驱动程序。
使用多队列/无序队列更高级一些。您应该确保正确利用事件以避免竞争条件或导致未定义的行为。
为了减少与 GPU 主机 DMA 传输相关的延迟,建议您使用固定主机缓冲区而不是 std::vector
。固定主机缓冲区通常通过 clCreateBuffer
和 CL_MEM_ALLOC_HOST_PTR
标志创建。