那次我想把一个图像处理工具做成“秒级调用”。目标听起来很直接:输入一张图,去掉不需要的内容,最好马上拿到结果。刚开始排查时,我很自然地把注意力放在设备上,想确认加速设备有没有打开。状态检查里出现了 MPS,直觉很容易顺着这条线走下去:既然已经检测到 MPS,应该就是 GPU 在工作。
但真正测起来,时间并没有因为一个配置状态出现就消失。单次调用仍然要等很久,尤其是第一次调用。后来我把一次处理拆开,才发现“能不能用加速设备”和“为什么这次调用很慢”其实是两件事。前者是设备能力问题,后者可能是模型加载、进程启动、磁盘读取或通信等待。
配置里出现了加速设备,不等于这次任务已经获得了真实的加速收益。
这件事让我重新意识到,性能判断不能只看一个开关,也不能只看工具给出的诊断结论。真正需要回答的是:时间究竟花在哪里?每个优化动作消除了哪一段等待?最后的验收,是否如实记录了模型实际运行在哪个设备上?
表面上是 GPU 问题,实际上先是冷启动问题
当时最明显的现象是,第一次处理要花约 10 秒以上。这个数字很容易让人怀疑算法本身,或者直接归因于 CPU 太慢。但继续看调用过程,会发现第一次运行还承担了额外工作:启动进程、导入依赖、读取模型、准备运行环境,然后才进入真正的图像处理。
这几段时间叠在一起,用户看到的只是“处理一张图用了很久”。如果只测从命令发出到结果返回的总时长,就会把模型冷启动和单次处理混成一个问题。这样一来,后面不管是更换设备、调整参数,还是继续复制模型文件,都很难判断到底有没有改善。
这里最容易误判的是,把首次耗时当成算法的固定成本。可如果同一个模型已经加载,后续请求的时间明显下降,就说明这部分等待并不属于每张图都必须支付的成本。它更像是一次性的入场费,只是原来的调用方式让每次请求都重新缴纳。
如果不处理这个问题,会导致两个后果。第一,批量任务会反复承担相同的初始化开销,输入越多,浪费越明显;第二,优化方向会被带偏,大家不断追着设备开关调,却没有先消除重复启动。最后可能得到一份看上去更“专业”的配置,实际体验却没有相应变化。
批量处理先解决重复启动
我先做的决定不是立刻改设备,而是把多个输入放到一次目录处理里。原因很简单:如果冷启动无法马上消除,至少不要让它对每个文件重复发生。测试结果也验证了这个判断,批量处理一组文件时,可以只冷启动 1 次。
这不是一个花哨的优化,却是很重要的分界。原来每张图都像一次新的任务,进程需要重新准备;批量方式则把“准备环境”和“处理多张图”放进同一个生命周期。模型加载的时间被摊薄之后,单张图的平均成本自然下降。
我选择先做批量复用,是因为它改变的是调用边界,不依赖某个特定硬件一定可用。即使后面发现模型没有跑在 GPU 上,批量仍然能减少重复初始化;如果设备加速后来确认有效,这个收益也不会浪费。
但批量不是所有场景的答案。交互式工具往往一次只来一张图,用户不愿意为了凑一批输入而等待。这里不能把批量测试的结果直接当成单次调用体验,还要继续处理常驻服务,或者为单次请求保留一个可接受的冷路径。
常驻服务解决的是模型生命周期
批量处理之后,我把思路再推进一步:让模型留在一个可复用的常驻服务里。请求到达时,服务不用重新导入模型,只需要接收输入、执行处理,再返回结果。这样优化的对象就变得清楚了——它主要减少的是进程启动、模型重复加载和进程间准备工作,而不是凭空让模型的每一步计算变快。
实测时,常驻服务热路径首次约 1.13 秒,后续约 0.22 秒。首次热请求仍然比后续请求慢,说明服务内部可能还有一次准备成本;但它和原来约 10 秒以上的冷启动已经不是同一个数量级。这里的关键不是记住某个漂亮数字,而是分清“服务第一次接请求”和“模型已经稳定驻留后的请求”。
常驻服务的价值,是把一次性的准备成本从每次调用中拿出来,而不是自动证明底层计算已经使用 GPU。
常驻也带来新的边界。服务可能停止、崩溃、被系统回收,或者因为模型异常而无法继续工作。为了不让性能优化变成可用性风险,我保留了冷路径兜底:服务停止后,仍有约 3.16 秒的回退处理。它不是热路径的竞争者,而是热路径失效时的安全网。
如果没有这个兜底,服务一旦不健康,用户得到的可能不是“稍微慢一点”,而是直接失败。一个看起来很快、但只能在理想状态下运行的系统,不能算真正完成。速度必须和恢复能力一起验收。
本地缓存减少 IO,但不替模型缓存背书
另一个容易混淆的动作是把模型复制到本地 SSD。这样做确实有价值:模型读取不必每次经过更慢的外部存储,启动阶段的重复 IO 可以减少,常驻服务启动时也更稳定。但它解决的是文件到内存之间的读取路径,不等于模型已经在进程中驻留,更不等于模型已经在加速设备上执行。
这件事表面上都是“缓存”,实际上至少有两层:本地 SSD 缓存解决磁盘访问,常驻服务缓存解决模型生命周期。前者让读取更快,后者让读取不必重复发生。只做第一层而不改变调用方式,仍然可能每次启动进程、重新初始化模型;只做第二层而不管模型文件位置,服务首次启动又可能被慢 IO 拖住。
所以我没有把“模型已复制到本地”写成最终结论,而是把它作为冷启动链路中的一个环节单独测量。真正有意义的比较,应该至少区分:设备探测耗时、模型加载耗时、单次处理耗时、进程通信耗时。只有这样,才能知道本地缓存到底减少了哪一段时间。
MPS 状态不能替代真实性能验收
排查中最需要克制的一步,是面对设备诊断结果。工具能报告 MPS,并不代表所有模型算子都支持 MPS,也不代表当前模型已经实际把计算交给了 MPS。那次测试里,LaMa 模型在 MPS 配置下仍然会回落到 CPU。也就是说,配置状态和运行事实之间存在距离。
错误捷径就是看到 doctor 报 MPS,就宣布 GPU 加速完成。这个结论的问题不只是说得过早,还会污染后续比较:如果把常驻服务带来的速度改善归给 GPU,就无法知道真正收益来自模型复用、磁盘缓存,还是设备变化。以后再次遇到类似情况,所有“加速了”的表述都应该回答一个更具体的问题:哪一段计算实际使用了什么设备?证据是什么?
我判断应当如实记录 CPU 回落,是因为验收的目标不是让报告听起来更先进,而是让下一次优化有可靠起点。承认模型没有按预期使用 MPS,并不会抹掉常驻服务的收益;反而能把收益归因分开:热路径变快,主要说明生命周期复用有效;设备探测通过,只说明环境具备某种配置能力;模型回落 CPU,则说明硬件加速仍需另行处理。
性能报告最有价值的部分,不是“加速成功”这句结论,而是把实际设备、耗时分段和回退条件写清楚。
把优化拆成可复用的验收流程
最后的测试覆盖了设备探测、批量处理、热路径和冷回退。这个顺序很重要。先确认环境能识别哪些设备,再测批量是否只冷启动 1 次;接着测常驻服务的首次热请求和后续热请求;最后停止服务,确认冷路径仍然可用。每一项都对应一个问题,也对应一种可能的失败。
这套流程可以抽象成 4 步。第一步,测设备探测,但只把它当作环境事实,不当作性能结论。第二步,把模型加载、单次处理和通信分别计时,找出真正的等待来源。第三步,按收益最确定的顺序消除重复工作:先批量复用,再考虑本地模型缓存,再引入常驻服务。第四步,为热路径设置健康检查和冷路径兜底,并在结果里记录实际执行设备。
这里还有一个简单的判断规则:如果一次改动没有改变耗时分段,就不要急着把它称为提速;如果只在服务健康时变快,就必须同时报告回退耗时;如果配置状态和模型实际设备不一致,就以实际运行结果为准。方法的意义不在于保证每台机器都一样快,而在于让差异能够被解释。
这件事让我重新意识到,所谓性能优化,往往不是把某个数字压到最低,而是把等待的来源逐一说清。真正难的不是打开一个开关,而是承认一个开关可能没有解决当前问题,并继续追问:这次请求到底在等待什么?
下次我会怎么做
下次遇到类似的图像处理任务,我会先画出一次请求的时间链:设备探测、进程启动、模型加载、单次处理、通信和结果写出。没有这张分解表之前,我不会把总耗时直接归因于 GPU 或 CPU。
然后我会先判断请求形态。输入可集中到一起,就优先批量处理;输入零散但调用频繁,就考虑常驻服务;服务可能被停止,就保留冷路径。模型放到本地 SSD 可以做,但会把它定位为 IO 优化,不把它和进程内缓存混为一谈。
最后,我会把“配置上启用了什么”和“实际运行用了什么”写成两栏,并分别记录热路径、冷路径和失败后的表现。一个流程成熟的标志,不是它能在理想环境里跑出一次漂亮结果,而是下次换输入、换设备或服务失效时,仍然知道该看哪一段、该相信哪条证据。
我更愿意把这看成一种工作习惯:先拆等待,再做复用,最后用真实运行设备验收,而不是用配置状态替性能背书。