做 Serverless GPU 的团队迟早要面对一个问题。函数平时缩到零,来请求了再起机器。从零到能干活,中间那段等待就是冷启动。
延迟分两层,常被混为一谈
用户感受到的延迟分两层。一层是平台把容器拉起来、把模型权重加载进显存的时间,这部分平台能优化。另一层是物理上的硬约束,模型越大,往显存里搬的东西越多,再怎么优化也压不到零,一个七百亿参数的模型,光把权重搬进显存就要实打实的时间。很多宣传里说的毫秒级,指的是空载容器的唤醒,不是模型真正就绪。客户拿空载延迟当推理延迟,上线就翻车,实测出来是十几秒,投诉就来了。
别每次都从零开始:挂起
把冷启动压下去,靠的是别每次都从零开始。一种做法是把起好的容器先挂起,不是销毁。显存里的模型权重留着,只是把计算资源暂时让出来。下次请求来了,把容器唤醒,权重还在,省掉了重新加载的那一大段。
autoscale: min_replicas: 0 suspend_threshold_sec: 300 # 连续 5 分钟无请求,转入挂起 resume_target_p99_ms: 200 # 唤醒后首字延迟目标 suspend_cost_model: shared # 挂起显存由平台统一调度计费这个做法有个前提,挂起的容器占着显存,这部分资源不能算纯浪费。我们见过两种处理:谁挂起谁承担成本,账上清楚;或者把挂起状态当成可交易资源,平台统一调度,让出闲置显存的团队少付钱。后者更灵活,但账要算得明白。
落地差异很大
一个做语音转写的接口,平时缩到零不花钱,突发来一波请求,从挂起唤醒,第一笔转写在两百毫秒级别回来,而不是让用户等十几秒。我们实测过一类中等体量的语音模型,从挂起唤醒对比从零冷起,首字延迟能差出一个数量级。反过来看图像生成这类本身就要跑几秒的任务,冷启动的差距没那么要命,就不一定值得常驻挂起。
代价:显存不能彻底释放
挂起换来速度,代价是显存不能彻底释放。一张 80G 的卡,只挂起一个 24G 的模型,剩下 56G 没法分给别人。平台要找平衡,哪些模型值得常驻挂起,哪些可以接受慢一点从零起。流量平稳又对延迟敏感的,常驻挂起划算;流量稀疏且不差那几秒的,缩到零更省。
把选择权交给用户
我们的处理方式,是让客户自己划那条线,而不是平台替他一刀切。平台把两种状态的切换成本压到最低,客户按自己业务选,随时能改。我们也给默认值:延迟敏感的中小模型默认开挂起,大体量默认缩零,但他随时能覆盖。新接口上线头一周先按缩零跑,我们看它的实际冷启动占比,再建议他要不要开挂起,不让他凭想象定。
一组我们实测的对比
同样一个中等体量的语音模型,三种状态的首次响应延迟我们测过:
从零冷起 1800 ms挂起唤醒 180 ms常驻热机 40 ms挂起唤醒比从零冷起快一个数量级,和常驻热机差几倍但显存成本低得多。所以我们的默认不是全挂起也不是全缩零,是按模型画像分:延迟敏感的中小模型默认挂起,大体量本身启动就慢的默认缩零。新接口上线头一周先按缩零跑,我们看它的实际冷启动占比,再建议他要不要开挂起。
冷启动本身不是敌人,它是弹性伸缩必须接受的代价。真正的功夫在后面,怎么让常驻和按需之间那根线划得准,让大多数请求感觉不到等待,同时闲置资源又不被白白供着。我们衡量自己做得好不好,不是挂起率多高,是用户那头首字延迟的 p99 稳在可接受范围,同时账单上的闲置成本又压得够低,这两条同时成立才算数。