拓冰建站拓冰建站
首页 / 资讯中心 / 正文

oneTBB Flow Graph 的 opencl_node 模板类:在异构设备上执行 OpenCL 内核的完整指南

oneTBB Flow Graph 的 opencl_node 模板类在异构设备上执行 OpenCL 内核的完整指南【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读opencl_node是 oneTBB本仓库 third-party/tbb 目录下的 Threading Building BlocksFlow Graph 组件中用于异构计算的节点类它允许开发者在具有 OpenCL 支持的设备集成/独立 GPU 或 CPU上执行 OpenCL 内核并自动处理设备内存缓冲区的传输与管理。本文以 oneTBB 规范文档 opencl_node_cls.rst 为主体结合仓库内的 完整可运行示例 与 OpenCL 内核源码深入讲解其模板语法、设备选择机制、辅助类体系、参数绑定方法及成员构造函数读完即可将 GPU 计算无缝接入 Flow Graph 流水线。1. 概述把 OpenCL 内核请进 Flow Graphopencl_node的核心目标是让 OpenCL 内核能够像普通 Flow Graph 节点一样参与图调度它接收来自输入端口input port的数据在 OpenCL 设备上排队执行内核kernel再通过输出端口把结果送回图的下游。它屏蔽了以下所有底层细节内核的排队与执行主机与设备之间的内存同步与结果回取内核参数与 ND-rangeND 范围的绑定。从实现角度讲opencl_node是一个使用了opencl_factory的 streaming_node 特化。opencl_factory实现了 streaming_node 的 Factory 概念把内核执行、内存同步和结果检索的全部复杂度封装起来并严格遵循 streaming_node 的执行工作流详见第 6 节。需要特别注意的是本仓库的 mold 项目在构建时会编译并链接 third-party/tbb 库但opencl_node属于预览preview特性默认头文件并不包含它必须显式定义宏并单独包含头文件见第 3 节。2. 模板语法与三种缺省形态opencl_node的完整模板签名如下JP为 Join PolicyFactory为设备工厂类型Ports为端口数据类型template typename JP, typename Factory, typename... Ports class opencl_node tuple Ports... , JP, Factory 为降低使用门槛它提供了两级缺省参数// 仅省略 Factory使用默认工厂 default_opencl_factory template typename JP, typename... Ports class opencl_node tuple Ports... , JP // 同时省略 Factory 与 JPFactory 用 default_opencl_factoryJP 用 queueing 策略 template typename... Ports class opencl_node tuple Ports... 也就是说最简单的用法只需给出端口数据类型元组例如opencl_node std::tuple opencl_vector 其余全部采用默认行为排队式queueingJoin Policy 默认 OpenCL 工厂。JPJoin Policy的语义与join_node一致可参考 Flow Graph 规范中join_node的描述。3. 头文件与预处理宏预览特性开关opencl_node依赖两个预览特性宏且其头文件不被oneapi/tbb/tbb.h和oneapi/tbb/flow_graph.h间接包含因此必须显式包含#define TBB_PREVIEW_FLOW_GRAPH_NODES 1 #define TBB_PREVIEW_FLOW_GRAPH_FEATURES 1 #include oneapi/tbb/flow_graph_opencl_node.h这一点在仓库的官方示例源码第 14 行得到印证——示例正是按此顺序定义宏并包含头文件。也就是说如果忘记定义TBB_PREVIEW_FLOW_GRAPH_NODES或TBB_PREVIEW_FLOW_GRAPH_FEATURESopencl_node相关 API 将不可见编译会直接失败。4. 设备管理双实体Device Filter 与 Device Selectoropencl_factory将设备初始化和执行时选设备两个职责拆分成两个独立实体职责清晰、互不耦合实体用途模型Device Filter设备过滤器用于初始化opencl_factory确定一组可供内核执行的设备集合。所有被过滤出的设备必须属于同一个 OpenCL Platform。默认过滤器取第一个被发现的 OpenCL Platform 上的全部设备。Device Selector设备选择器用于为某个具体的opencl_node实例选择执行内核的设备。默认选择器取设备过滤器构造出的设备列表中的第一个设备并且每次内核执行都会重新执行选择逻辑。这种过滤一次、逐次选择的模型带来的直接收益是同一个opencl_factory可以被多个opencl_node共享每个节点通过各自的 Device Selector 把内核派发到不同设备例如一个节点固定用 GPU另一个固定用 CPU而设备集合的发现只发生一次。从仓库示例可以直观看到自定义 Device Selector 的写法opencl_node_example.cpp 中的gpu_device_selector是一个函数对象其operator()接收工厂引用遍历f.devices()返回的opencl_device_list通过d.info(CL_DEVICE_TYPE, type)查询每个设备的类型优先返回CL_DEVICE_TYPE_GPU设备若不存在 GPU则回退到第一个可用设备并打印提示信息。这一实现恰好演示了 Device Selector 的全部关键 APIopencl_factory::devices()、opencl_device_list的迭代器、以及opencl_device::info()。5. 辅助类体系面向 OpenCL 实体的轻量封装opencl_node自带一组辅助类把 OpenCL 的底层实体设备、内存对象、内核、ND-range包装成 C 友好的形态辅助类说明opencl_buffer/opencl_subbuffer强类型线性数组的抽象模板类。OpenCL 内核操作的是主机侧分配的专用内存对象这两个类封装了主机与目标设备之间的内存事务逻辑传输、同步。opencl_bufferT拥有类似std::vector的接口——示例中opencl_vector即opencl_buffercl_int可直接vec[i]下标访问、std::fill(vec.begin(), vec.end(), 2)填充。opencl_deviceOpenCL 设备的抽象类主要在 Device Selector 与 Device Filter 内部使用同时提供查询设备信息的公开 API如示例中的d.info(CL_DEVICE_TYPE, type)。opencl_device_list存放opencl_device实例的容器示例中以std::find_if配合迭代器遍历。opencl_program用于指定内核类型、从文件读取内核源码或构建内核。示例中opencl_program program(vector_operations.cl)即从工作目录读取内核源文件program.get_kernel(cuber)取出具名内核。opencl_program_type枚举声明opencl_node支持的内核类型见下表。opencl_range使opencl_node支持 ND-range对应 OpenCL 的clEnqueueNDRangeKernel()方法的范围参数。示例中vector_cuber.set_range({{ vector_size }})即设置一维、长度为 10 的全局工作范围。opencl_program_type枚举的三个取值及其语义SOURCEOpenCL C 源码。以该类型初始化的opencl_program会在运行时读取源文件并构建内核是opencl_program的默认值PRECOMPILED目标设备可直接执行的机器码格式内核无需运行时编译SPIR标准可移植中间表示Standard Portable Intermediate RepresentationSPIR。6. 参数绑定set_args 与 port_refopencl_node默认按顺序绑定第一个输入端口绑定到第一个内核参数第二个输入端口绑定到第二个内核参数依此类推。当某些内核参数在每次运行时都保持不变时可用set_args方法把固定值直接绑定到内核参数如果某参数的值应来自输入端口则使用port_ref辅助函数引用对应端口。二者的组合使内核参数的绑定非常灵活固定值绑定一次即可动态值则随消息流不断更新。以仓库示例为例cuber内核只有一个__global int* buf参数因此vector_cuber.set_args(oneapi::tbb::flow::port_ref0); vector_cuber.set_range({{ vector_size }});含义是第一个内核参数从第 0 号输入端口取值即从广播节点收到的opencl_vector缓冲区并把 ND-range 固定为vector_size一维、10 个全局工作项。同理vector_squarer也做了完全相同的绑定。port_ref的完整用法与更多示例可参考 streaming_node 章节的描述——opencl_node继承自streaming_node其set_args、set_range等接口语义完全一致。7. 完整实战示例立方与平方流水线原文档附带的示例将经典的 cuber and squarer立方与平方消息流示例改造为把部分计算卸载到 GPU两个opencl_node并行计算立方与平方再在 CPU 上求和。以下两个文件都在仓库的third-party/tbb/doc/main/specification/source/uncategorized/examples/目录下可直接阅读与复现。7.1 OpenCL 内核文件 vector_operations.cl__kernel void cuber( __global int* buf ){ int idx get_global_id(0); buf[idx] buf[idx] * buf[idx] * buf[idx]; } __kernel void squarer( __global int* buf ){ int idx get_global_id(0); buf[idx] buf[idx] * buf[idx]; }两个内核都是典型的一维 SIMD 风格get_global_id(0)取全局工作项索引对缓冲区中对应元素做立方/平方运算。opencl_program会在运行时读取该文件并构建这两个内核SOURCE类型默认行为。7.2 主程序 opencl_node_example.cpp完整源码见 opencl_node_example.cpp其关键结构如下typedef oneapi::tbb::flow::opencl_buffercl_int opencl_vector; class gpu_device_selector { /* 自定义 Device Selector优先选择 GPU见第 4 节 */ }; int main() { const int vector_size 10; int sum 0; oneapi::tbb::flow::graph g; oneapi::tbb::flow::broadcast_node opencl_vector broadcast(g); // GPU 计算部分 gpu_device_selector gpu_selector; oneapi::tbb::flow::opencl_program program(vector_operations.cl); oneapi::tbb::flow::opencl_node std::tuple opencl_vector vector_cuber(g, program.get_kernel(cuber), gpu_selector); oneapi::tbb::flow::opencl_node std::tuple opencl_vector vector_squarer(g, program.get_kernel(squarer), gpu_selector); // 绑定内核参数与问题规模 vector_cuber.set_args(oneapi::tbb::flow::port_ref0); vector_cuber.set_range({{ vector_size }}); vector_squarer.set_args(oneapi::tbb::flow::port_ref0); vector_squarer.set_range({{ vector_size }}); // CPU 计算部分 oneapi::tbb::flow::join_node std::tuple opencl_vector, opencl_vector join_data(g); oneapi::tbb::flow::function_node std::tuple opencl_vector, opencl_vector summer( g, 1, { // 从结果元组中取出立方/平方结果在 CPU 上累加 for (int i 0; i vector_size; i) sum std::get0(res)[i] std::get1(res)[i]; }); // 图拓扑broadcast - {cuber, squarer} - join - summer make_edge( broadcast, vector_cuber ); make_edge( broadcast, vector_squarer ); make_edge( vector_cuber, oneapi::tbb::flow::input_port0(join_data) ); make_edge( vector_squarer, oneapi::tbb::flow::input_port1(join_data) ); make_edge( join_data, summer ); // 数据初始化与运行 opencl_vector vec(vector_size); std::fill( vec.begin(), vec.end(), 2 ); broadcast.try_put(vec); g.wait_for_all(); std::cout Sum is sum \n; // 输出 880 }该示例同时展示了opencl_node的多个核心用法两节点共享一个内核文件cuber与squarer两个节点都从同一个opencl_program取出各自的内核对象异构协同GPU 负责立方/平方opencl_nodeCPU 负责求和function_node二者通过join_node汇合make_edge完成连接同步语义透明summer中直接对opencl_vector元素做下标访问异步执行与数据就绪的等待由底层async_msg机制透明处理见第 9 节开发者无需手写clEnqueueReadBuffer之类的同步代码数值可验证10 个元素全部初始化为 2立方和8×1080平方和4×1040总和应为 120——示例将结果累加进sum并打印便于自测运行正确性仓库内该示例输出 Sum is ... 的具体数值以实际运行为准。7.3 构建与运行仓库在示例同目录提供了 opencl_node_example.makefile 与 opencl_node_example.makefile.windows 两个构建脚本覆盖 Unix/Linux 与 Windows 平台。构建时需保证编译环境具备 OpenCL 头文件与库链接 OpenCL ICD 库通常为-lOpenCL宏TBB_PREVIEW_FLOW_GRAPH_NODES与TBB_PREVIEW_FLOW_GRAPH_FEATURES已定义运行时vector_operations.cl与可执行文件位于同一工作目录opencl_program在运行时按文件名读取源码目标机器存在可用的 OpenCL 平台/设备GPU 或 CPU 皆可无 GPU 时示例的 Device Selector 会自动回退到首个可用设备。8. 成员与构造函数opencl_node位于命名空间oneapi::tbb::flow完整继承链为opencl_nodetuplePorts..., JP, Factory→streaming_nodetuplePorts..., JP, Factory。其公开成员除下表列出的外其余全部继承自基类streaming_node。模板参数/成员说明typename... Ports节点的输入/输出数据类型以std::tuple...形式给出。typename JPJoin Policy连接策略细节参见join_node类说明省略时默认queueing。typename Factory设备相关的工厂类型本节点默认使用opencl_factory省略时使用opencl_info::default_opencl_factory。typedef implementation-dependent kernel_type内核句柄类型由实现定义一般通过opencl_program::get_kernel(name)获取。四个主要构造函数细节与基类streaming_node一致opencl_node( graph g, const kernel_type kernel ); // 默认工厂 opencl_node( graph g, const kernel_type kernel, Factory f ); // 指定工厂 template typename DeviceSelector opencl_node( graph g, const kernel_type kernel, DeviceSelector d ); // 默认工厂 自定义选择器 template typename DeviceSelector opencl_node( graph g, const kernel_type kernel, DeviceSelector d, Factory f ); // 全参数从规范源码片段中的类声明可以看出三种模板特化逐级提供默认参数带JP的特化继承自带Factory的特化而最简形式同时缺省JPqueueing与Factorydefault_opencl_factory。示例中的构造方式opencl_node std::tuple opencl_vector (g, kernel, gpu_selector)走的正是第三种重载。9. 底层机制streaming_node 执行工作流与 async_msg 语义由于opencl_node本质是 Factory 特化后的 streaming_node其执行行为遵循 streaming_node 的简化算法共六步在输入端口接收输入数据若尚未包装为 Factory 特定的async_msg类型则先包装为本次内核执行选择设备调用 Device Selector把内核参数与可选的内核范围发送到设备将内核排队到设备上执行按需更新async_msg中的依赖关系把更新后的async_msg从输出端口发出作为节点输出。理解这一工作流有助于把握opencl_node的两个重要语义约束输出总是异步消息streaming_node及opencl_node的输出端口总是发出async_msg派生类型的消息节点不会等待内核执行结束——等待被推迟到真正消费结果数据的时刻类似 C 的 future-promise 模型。因此图下游节点无法看见未就绪的数据async_msg的依赖机制会保证数据可用后才被消费。这正是示例中summer能安全直接访问opencl_vector元素的原因。默认假设参数可被修改节点默认假定所有内核参数都可能被内核执行修改即使某参数实际是只读的这一保守假设也可能延迟输出async_msg中数据的可用性。此外streaming_node文档还强调其输入数据的三种去向作为内核参数、作为内核范围、或原样透传到对应输出端口当输入不参与内核参数或范围绑定时。opencl_node的set_args/set_range/port_ref机制正是这一模型的直接体现。10. 小结什么时候用 opencl_node综合原文档与仓库源码opencl_node最适合以下场景应用已经基于 TBB Flow Graph 组织流水线希望把计算密集的内核卸载到 GPU/其他 OpenCL 设备而不重写图结构需要以图的方式表达CPU 节点 GPU 内核节点的异构协作且希望内存缓冲区在主机与设备间的传输由框架自动管理需要在内核执行前按设备类型动态选择执行设备Device Selector 每次内核执行都会被调用天然支持运行时负载/可用性感知。使用前请确认定义两个预览宏并直接包含oneapi/tbb/flow_graph_opencl_node.h设备过滤器选出的设备必须属于同一 OpenCL PlatformSOURCE类型内核在运行时读文件构建部署时需保证内核文件可访问。完整的 API 细节可继续查阅本仓库的 opencl_node 规范文档、基类 streaming_node 规范文档 以及可运行示例。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门