5.1 计算管线与分派


文档摘要

5.1 计算管线与分派 本节摘要:计算管线只有单个着色阶段,却承载了 GPU 通用计算的全部入口。本节讲清 GPGPU 在 Vulkan 里的打开方式:管线装配、工作组网格与分派维度、共享内存的使用,并用一次直方图统计任务把「缓冲、管线、分派、屏障、读回」整条链走通。 「GPGPU」这个行话的意思是通用图形处理器计算——把 GPU 当成大规模并行数据处理器用,而不是只当像素流水线。Vulkan 的计算管线是这个行话的正式落地:不需要窗口、不需要交换链,一块支持计算的 GPU 加一个 Vulkan 账户就能开工。上一章的图形管线有十个固定功能工位,本节的主角把它们全部拆掉,只留下一个着色器阶段——这是 Vulkan 里最简单的管线,也是并行数据处理的完整起点。

5.1 计算管线与分派

本节摘要:计算管线只有单个着色阶段,却承载了 GPU 通用计算的全部入口。本节讲清 GPGPU 在 Vulkan 里的打开方式:管线装配、工作组网格与分派维度、共享内存的使用,并用一次直方图统计任务把「缓冲、管线、分派、屏障、读回」整条链走通。

「GPGPU」这个行话的意思是通用图形处理器计算——把 GPU 当成大规模并行数据处理器用,而不是只当像素流水线。Vulkan 的计算管线是这个行话的正式落地:不需要窗口、不需要交换链,一块支持计算的 GPU 加一个 Vulkan 账户就能开工。上一章的图形管线有十个固定功能工位,本节的主角把它们全部拆掉,只留下一个着色器阶段——这是 Vulkan 里最简单的管线,也是并行数据处理的完整起点。

一、最简单的管线与它的着色器

计算管线装配只涉及两样东西:管线布局(3.4 的集合布局加推常量)与一个计算着色器阶段。没有顶点输入、没有光栅化、没有混合:

VkComputePipelineCreateInfo cpi = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; cpi.stage = computeStage; // 与 4.1 相同的阶段结构,stage 填 COMPUTE_BIT cpi.layout = computeLayout; VkPipeline computePipeline; vkCreateComputePipelines(device, pipelineCache, 1, &cpi, NULL, &computePipeline);

对应的 GLSL 也不像图形着色器那样有固定的输入输出,只有全局工作组与自定义缓冲:

#version 450 layout(local_size_x = 64) in; // 工作组内 64 个线程 layout(set = 0, binding = 0) buffer InData { float values[]; }; layout(set = 0, binding = 1) buffer OutData { uint bins[]; }; void main() { uint gid = gl_GlobalInvocationID.x; // 全局线程编号 if (gid >= uDataCount) return; // 越界卫兵:网格可能超配 uint bin = uint(clamp(values[gid] * 0.5 + 0.5, 0.0, 0.999) * 256.0); atomicAdd(bins[bin], 1u); // 多线程写同格,必须原子加 }

这段三十行的着色器浓缩了计算编程的三条纪律。越界卫兵必须有:分派网格按上限取整,尾部线程必然越界。写入冲突要原子:多个线程对同一格的累加,非原子读写是数据损坏。缓冲按 storage 声明:对应 3.2 的 STORAGE_BUFFER 用法位,读写双向还要 transfer 源位备读回。

二、分派:三维网格的账要算清

vkCmdDispatch(groupX, groupY, groupZ) 派发的是「工作组网格」,每个工作组含 local_size 声明的线程数。任务量 N=200000、组内 64 线程时,网格取 ceil(200000/64)=3125 个工作组。三套坐标要分清:gl_LocalInvocationID 是组内编号、gl_WorkGroupID 是组编号、gl_GlobalInvocationID 是全局编号(两者换算而来)。组尺寸的选择是个权衡:64 是常见的「一个硬件调度单元」尺寸;用共享内存做协作时,尺寸越大占用越高;访存模式连续时,让相邻线程访问相邻数据是硬原则。

图 5-1:分派网格与两种内存空间

图 5-1:分派网格与两种内存空间

共享内存(GLSL 里 shared 限定)是组内线程的便签板:容量小、延迟低,做规约与分桶的第一落点。它的容量上限按设备查询(maxComputeSharedMemorySize,常见 32KB 起),申报超标会在管线创建时报错——又一个「查询先行」的日常。

三、直方图任务的完整命令序列

把第一节着色器跑起来的命令序列,注意分派前后的两道屏障(屏障语法的完整展开在第 6 章,这里先看节奏):

vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, computeLayout, 0, 1, &set, 0, NULL); // 屏障一:输入数据就绪(上一阶段写入对计算着色器可见) barrier(stage = COMPUTE_SHADER, access = SHADER_READ); vkCmdDispatch(cmd, (200000 + 63) / 64, 1, 1); // 屏障二:bins 的原子写对后续消费者可见 barrier(stage = COMPUTE_SHADER, access = SHADER_WRITE); // 之后:拷到主机可见内存读回,或直接供后续计算/渲染消费

计算与图形可以在同一条命令缓冲里交错:先用计算管线做蒙皮与剔除,再切图形管线渲染,再切回来做后处理——管线绑定点的切换开销很低,这种交错正是现代引擎把「剔除、粒子、光照」搬进计算的基础。若希望计算与图形真正并行,则分队列提交并用信号量连线(第 6 章)。

四、案例:粒子模拟的移植全程

背景:某演示程序在 CPU 上模拟二十万粒子(位置、速度积分),单帧模拟耗时逼近预算,主线程被打满。目标:整段搬到计算管线。

操作:分四步移植。数据侧,把粒子数组放进 storage 缓冲(设备本地,3.1 方案 A),双缓冲两份用于帧间乒乓;着色器侧,把 CPU 积分循环改写为计算着色器,组内用共享内存做邻居查找的分桶预排序;执行侧,每帧一次分派加两道屏障,与渲染命令同缓冲交错;交互侧,外力参数走推常量(3.5),每帧直推。

结果:模拟段耗时从逼近帧预算降到约二十分之一,CPU 帧时间同步大降——原本用于模拟的主线程循环整体消失。新增成本是一次性写着色器与调组尺寸的两天工时。

解读:移植收益的来源值得看清:不是「GPU 算得快」这么笼统,而是任务具备「数据独立、访存可预测」的并行形态,二十万份相同计算天然适合网格分派。反过来,分支密集、强串行依赖的逻辑搬上去只会更慢——判断任务形态比学 API 更重要。

变式:若邻居关系复杂(碰撞检测类),单次分派装不下,就拆多趟分派加中间缓冲:排序一趟、构建查找结构一趟、求解一趟——多趟之间以屏障连线,形成「计算管线谱系」。这个思路也是光追剔除、GPU 挑选等高级管线的骨架。

五、要点回顾

  • 极简管线:计算管线只有着色阶段与布局两样,装配最简但承担全部 GPGPU 入口。
  • 三套坐标:组内编号、组编号、全局编号,越界卫兵与原子操作是分派编程的两条保命纪律。
  • 两种内存:全局缓冲大而慢、共享内存小而快,组内协作靠 barrier,跨组协作靠原子或多趟分派。
  • 屏障定节奏:分派前保输入就绪、分派后保输出可见,交错图形计算靠绑定切换,真并行靠分队列。
  • 形态决定收益:数据独立、访存可预测的任务才值得上计算管线,复杂依赖拆多趟。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U