DeviceArray — Stream、Queue 和 Context
本文是 DeviceArray 参考 的进阶配套文档。只有当您需要针对该缓冲区运行自己的 GPU 代码时才需要阅读本文,例如 CUDA 或 OpenCL 内核,或 PyTorch、CuPy、OpenCV 等框架。本文介绍 SDK 如何将其 GPU 工作与您的 stream 或 queue 进行排序,以及何时必须共享 CUDA 或 OpenCL context。
Stream 和 Queue 排序
stream (CUDA)或 command queue (OpenCL)是一个有序的 GPU 工作列表:放入其中的任务会按顺序依次执行。SDK 在其自己的 stream(或 queue)上运行捕获和处理工作,与您创建的任何 stream 或 queue 相互独立。
当您调用返回 DeviceArray 的方法时,例如 imageDeviceArray() 或 devicePointsXYZ(),您会将自己的 stream 或 queue 包装在 StreamOrQueue 中传入。SDK 随后会让您的 stream 等待其自身的工作完成。因此,您在该调用之后放入自己 stream 中的任何任务都能保证看到已完成的数据,而 CPU 无需等待。
涉及两个 stream / queue
flowchart LR
subgraph sdk [SDK-owned]
SDKStream["SDK stream / queue"]
captureOp(["capture / processing"])
end
subgraph user [User-owned]
UserStream["User stream / queue"]
userKernelOp(["your kernel or copy"])
end
captureOp --> SDKStream
userKernelOp --> UserStream
acqOp(["imageDeviceArray(userStream)"])
SDKStream --> acqOp
acqOp -.-> UserStream
classDef zividClass fill:#4A8FA4,stroke:#34323D,color:#FFFFFF
classDef api fill:#91D2C8,stroke:#4A8FA4,color:#000000
class SDKStream,UserStream zividClass
class captureOp,userKernelOp,acqOp api
虚线箭头表示该调用建立的排序关系。用户 stream 会等待在 SDK stream 上记录的一个事件。此过程中不发生 CPU 侧同步;两个 stream 都会持续异步运行。
典型调用序列的时间线
sequenceDiagram
participant Host
participant SDK as SDK stream / queue
participant User as User stream / queue
Host->>SDK: capture / processing work
Note right of SDK: producer runs asynchronously
Host->>SDK: imageDeviceArray(userStream)
SDK-->>User: record event, User waits on it
Host-->>Host: DeviceArray returned (no host sync)
Host->>User: launch consumer kernel(devicePointer)
Note right of User: kernel is ordered after SDK work
Host->>User: copyToHost(dst, userStream)
Note right of User: D2H copy enqueued, call returns
Host->>User: synchronizeStream(userStream)
User-->>Host: blocks until copy completes
Host->>Host: read dst on CPU
关键特性
该调用在主机端是非阻塞的,一旦排序设置完成即返回。
devicePointer()是一个普通的访问器;它不执行任何同步操作。该缓冲区已经可以在您传入的同一 stream / queue 上使用。
将数据复制到 CPU 的主机端访问器(
copyToHost()、toArray2D()、toArray1D()以及独立的toImage(buffer, streamOrQueue)重载函数)也会立即返回。在读取主机端目标数据之前,请在同一 stream / queue 上调用
synchronizeStream。
传入 SDK 自身的 stream / queue
如果您没有自己的 stream 或 queue,可传入 computeDevice().sdkStreamOrQueue()。这样 SDK 的工作和您的工作会共享同一个 stream,等待步骤无需等待任何东西,也不会产生任何开销。当您只是为不提供自身 stream 句柄的框架获取原始设备指针时,推荐使用此方式。
在 StreamOrQueue 类型之间选择
StreamOrQueue 是一个简单的带标签的包装类型。只会读取与当前所用后端相匹配的成员;另一个成员必须为空。
在 CUDA 构建中,请传入
CUDAStreamPtr{ stream }。允许传入空 stream,此时表示使用 CUDA 的默认/传统(legacy)stream。在 OpenCL 构建中,请传入
OpenCLCommandQueuePtr{ queue }。queue 句柄不能为空。
这两种包装类型都可以隐式转换为 StreamOrQueue,因此这些方法在调用处只需传入一个参数。
GPU Context 共享
备注
只有当您创建自己的 CUDA 或 OpenCL context,或希望 SDK 与其他框架共享 context 时,才需要阅读本节。在默认设置下,SDK 会创建自己的 context,而您在自己的代码中使用 CUDA runtime,这种情况下一切已经共享同一个 context,无需做任何额外操作。
context 是 GPU runtime 用于存放分配的内存、stream 和 queue 的容器。在一个 context 中分配的缓冲区无法被属于另一个 context 的 stream 或 queue 读取。DeviceArray 背后的缓冲区存在于 SDK 所使用的 context 中,因此您用来消费它的任何 stream 或 queue 都必须属于同一个 context。CUDA 和 OpenCL 在为您自动完成这项设置的程度上有所不同。
CUDA
CUDA runtime 会在幕后为每个设备创建并管理一个共享 context(CUDA 称之为 primary context)。只要您进程中的所有代码都通过 CUDA runtime 与同一个 GPU 通信,所有的 stream 和设备指针就会自动共享该 context。这也是 SDK 默认使用的方式。因此最简单的设置方式是让 SDK 创建自己的 CUDA context,同时在您自己的代码中同样使用 CUDA runtime API。无需任何额外设置,在请求设备数组时传入的任何 CUstream 都是兼容的。
如果您使用的是自己的 CUDA driver-API context(即通过 cuCtxCreate 创建的 CUcontext,或由其他框架拥有的 context),则必须在构造 Application 时将其提供给 SDK。否则 SDK 会创建一个独立的 context,其返回的 DeviceArray 将无法在您的 stream 中使用。
CUcontext cuContext = ...;
Zivid::Application zivid{ Zivid::CUDAContextPtr{ cuContext } };
OpenCL
OpenCL 没有类似 runtime 托管的 primary context 的概念。每个 cl_mem 和 cl_command_queue 都绑定到特定的 cl_context,而一个 queue 只能对属于同一个 context 的缓冲区排入工作。如果您传入的 OpenCLCommandQueuePtr 所属的 queue 与 SDK 的 context 不同,排序调用将失败,且您的内核无法安全地使用该设备指针。
因此,您必须让 SDK 和您的代码共用同一个 context。实现方式有两种。
方式一:预先将您的 context 提供给 SDK。在构造 Application 时传入 OpenCLContextPtr{ clContext }。SDK 会在该 context 中创建其内部 queue,而您已在该 context 中拥有的任何 queue 都是兼容的。
cl_context clContext = ...;
Zivid::Application zivid{ Zivid::OpenCLContextPtr{ clContext } };
方式二:采用 SDK 的 context。如果您可以在 Application 构造完成之后创建 OpenCL 对象,可获取 SDK 的 context,并在其中创建您自己的 queue。
auto *sdkContext = static_cast<cl_context>(camera.computeDevice().nativeContext());
cl_command_queue userQueue = clCreateCommandQueueWithProperties(sdkContext, device, nullptr, &err);
无论采用哪种方式,您都可以将 OpenCLCommandQueuePtr{ userQueue } 传入任何返回 DeviceArray 的方法,返回的数组可安全地在该 queue 上使用。
备注
createDeviceArrayView (OpenCL 重载版本)也有同样的要求。您所包装的 cl_mem 必须在 SDK 的 context 中创建,否则 SDK 无法针对它排入工作。
另请参阅
DeviceArray 参考,了解各格式的数据布局。
GPU 访问教程,了解端到端的使用方法和框架集成示例。