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

共享内存(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 挑选等高级管线的骨架。