Metal 冷启动并发下,Pipeline Cache 为什么必须合并在途编译

问题与范围

图像处理、视频帧处理和实时预览通常不会在启动时把所有 Metal pipeline 一次性建完。某个 kernel、函数常量或输出组合第一次被请求时,系统才按需创建对应的 MTLComputePipelineState。

最直观的实现是一个字典:命中就返回,未命中就创建后写回。单线程下它足够正确;但冷启动恰好容易有多个请求同时发现同一个缓存键不存在。它们会一起进入创建路径,令同一份工作重复发生,也让 reset 与旧创建的交错难以解释。

本文讨论计算 pipeline 的并发缓存合同,不讨论滤镜效果、相机采集、视频编码或跨设备性能数字。

先给结论

  • 一个 pipeline identity 至少要有"不存在、正在创建、已完成"三种状态;普通缓存字典只表达其中两种。
  • 同一个 identity 的后来者应等待首个创建任务,并接收同一个成功结果或失败结果;不同 identity 不应因此串行。
  • 创建要在缓存锁外进行;等待者也不能带着缓存锁等待,否则完成阶段可能形成互相等待。
  • reset 应隔离旧结果对新缓存的写入,而不是让已在等待的调用者失去明确结果。
  • identity 的定义必须覆盖会改变 pipeline 的编译输入;键过粗会错误共享,键过细会失去合并效果。

为什么"有缓存"仍会重复创建

下面的写法只处理了已完成结果:

swift 复制代码
if let pipeline = cache[identity] {
    return pipeline
}

let pipeline = try createPipeline(identity)
cache[identity] = pipeline
return pipeline

当线程 A 与线程 B 同时执行到第一行,二者都可能看到空缓存,然后都开始创建。问题并不是字典线程安全就能解决:即使读取与写入都加锁,只要"检查为空"和"登记创建者"不是同一个受保护的状态转换,重复创建仍可能发生。

正确的模型是为每个 identity 记录一项在途任务:

text 复制代码
线程 A:X 不存在 → 登记正在创建 X → 创建
线程 B:X 正在创建 → 等待 A 的结果
线程 C:X 正在创建 → 等待 A 的结果

这里的目标不是把全部请求排成一列,而是只合并相同 identity。不同 identity 仍可独立创建,避免一个慢 kernel 阻塞整个冷启动路径。

锁只保护状态转换,不包住 Metal 创建

一个可靠的最小流程可以写成:

  1. 短暂持有缓存锁,检查已完成缓存。
  2. 若同一 identity 已有在途任务,取出它;若没有,登记新的在途任务。
  3. 释放缓存锁。
  4. 创建者在锁外调用 Metal 创建;等待者只等待该任务自己的结果。
  5. 创建结束后唤醒全部等待者,并在当前 generation 仍有效时写入缓存。

这避免了两类问题:长时间持有全局缓存锁会让无关 identity 彼此阻塞;等待者持锁等待则可能阻止创建者回写结果并通知等待者。

在途任务必须传播 Result,而不仅是成功值。这样一批等待者会一起拿到同一个 pipeline,或一起收到同一个错误;失败不会把等待者留在未知状态。失败后的下一次请求可以重新尝试创建,但当前这批请求必须先被完整收口。

递归和 reset 是两个常被漏掉的边界

如果创建 X 的过程又在同一线程请求 X,等待自己完成会形成确定性的自死锁。因此在途任务应识别创建线程,并对这种递归返回明确的配置错误。

reset 也不只是清空字典。考虑以下交错:

text 复制代码
旧创建开始
reset 清空缓存
新请求创建同一 identity
旧创建结束并写回缓存

如果没有额外边界,旧结果会污染 reset 后的新状态。一个直接的做法是引入 generation:创建开始时记录 generation;结束时只有 generation 未变化才能写入当前缓存。旧任务仍应向已经等待它的调用者交付结果,但不再拥有修改新缓存的资格。

这区分了两件事:对已经接受的调用负责,以及对当前资源世代负责。二者不必互相牺牲。

源码案例与验证范围

Harbeth 的 Device.makePipelineState(for:create:) 先检查已完成的 identity 缓存,再查询 pipelineCreations 中同 identity 的在途创建。相同 identity 的后来者等待同一 ComputePipelineCreation;不同 identity 不持有共同缓存锁完成 Metal 创建。创建完成后,只有 generation 未变化,结果才会写入缓存。

ComputePipelineCreation 用 NSCondition 保存一次性的 Result 并唤醒等待者;同线程递归等待返回配置错误。相应的 PipelineConcurrencyTests 本轮在 macOS arm64e 运行 6 项、0 失败,覆盖:16 个并发冷请求只得到 1 个 unique pipeline、reset 不污染新 generation、失败可重试、递归不死锁、不同 identity 不等待慢创建,以及失败唤醒所有等待者。

测试输出曾记录一次约 270ms 的冷启动探针,但它只属于该测试环境和该次运行,不能外推为通用性能结论。这里被验证的是并发结果与状态边界,而不是设备级性能承诺。

Kakapos 在这个问题中只提供媒体层的边界案例:CameraEngine 创建 preview 与 recording controller 时将 controlsSourceLifecycle 设为 false,由 CameraEngine 管理共享 source 的启动。采集、录制和停止生命周期仍属于媒体编排层;逐帧 GPU 后端不应反过来接管它们。

小结

Pipeline cache 一旦面对并发首次请求,就不再只是一个容器,而是一份小型并发协议。把"正在创建"建模出来,并明确以下规则,才能让冷启动可解释、可测试:

  • 谁登记创建者;
  • 同键后来者如何加入;
  • 失败如何传递和重试;
  • reset 如何隔离旧结果;
  • 递归等待如何终止;
  • 不同 identity 如何保持并行。

你在 Metal pipeline 或其他 GPU 资源缓存中,遇到过重复创建、reset 竞态或递归死锁吗?欢迎讨论 identity 的边界、等待机制与失败处理策略。

源码与参考实现

相关推荐
美狐美颜SDK开放平台3 小时前
准备开发直播APP?视频美颜SDK接入前的十项检查清单
android·ios·音视频·直播美颜sdk·视频美颜sdk
承渊政道15 小时前
iOS远程控制电脑用什么好?iPhoneiPad远程控制Windows电脑最佳方案2026横评
windows·ios·远程控制·1024程序员节·iphone ipad
茶底世界之下15 小时前
彩色建筑适合转黑白吗?用 Awaking 检查颜色消失后的层次
ios
黑科技iOS上架17 小时前
App Store 26年10月新应用审核放缓大量应用不进审
ios·审核
OxYGC17 小时前
手机为什么能“自动跳过“开屏广告?李跳跳、GKD 与 iOS 代理软件原理盘点
android·ios·开屏广告·广告屏蔽
终端安全笔记1 天前
一份配置管十项 AI 功能:它关的是入口,不是数据
网络安全·ios·移动设备管理
2501_915106321 天前
跨平台抓包工具怎么选?TraceEagle抓包鹰 的四种跨平台能力
网络协议·计算机网络·网络安全·ios·adb·https·udp
黑科技iOS上架2 天前
iOS应用上架:提前规避审核被拒风险
ios·审核
茶底世界之下2 天前
方图一定要裁照片吗?用 Awaking 比较裁切与留白
ios
00后程序员张2 天前
无需 Mac 的 iOS App 上架工具,开心上架Appuploader 负责哪些环节
android·ios·小程序·https·uni-app·iphone·webview