想象你开了一家烧烤店。开业头一天中午就来了客人,你掀开烤炉盖,炭还没点着,肉还锁在冷库里,连菜单都没写全。客人从坐下到吃上第一串,等了四十分钟。第二天你学乖了:提前两小时到店,解冻、切块、串好,炭烧得通红,上菜顺序也私下演练了一遍。客人一进门,第一串五分钟上桌。 大模型推理服务就是一家这样的烧烤店。新拉起的 GPU 实例相当于还没开火的店面:几十 GB 的“食材”还堆在仓库里,炭火没有点燃,后厨动线也没演练过。第一个请求进来,既要搬运食材、又要现点火、还得临时编排流程,延迟比平稳期高出好几个数量级。这就是大模型服务的“冷启动”。 本文用“开店备灶”这条主线,讲清楚冷启动为什么慢,以及模型预加载、权重预热、编译缓存、弹性伸缩这四把“火钳”如何让服务在扩容和重启后迅速进入稳定状态。掌握了这些,你就明白为什么头部大模型 API 的延迟曲线那么稳,也清楚自己该从哪个环节下手。
对 AI 基础设施工程师来说,冷启动优化是平台 SLA 的关键手艺:一次扩容、一次重启,用户在毫秒级还是分钟级就绪,决定了 API 的质量和口碑。
目录
可视化演示
展示不同卷积核对图像的滤波效果:边缘检测、锐化、模糊和浮雕,动态演示3×3卷积核在图像上的滑动卷积过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
边缘检测卷积核 3×3拉普拉斯算子,中心权重为8 检测图像中亮度变化剧烈的区域 out=Σ(kernel×patch) 边缘检测核突出像素与邻域的差异,提取边缘特征
冷启动成因:为什么第一串等四十分钟
烧烤店开业第一天的混乱,就是冷启动的完整写照。你回想一下那天:客人点单后,你才发现肉还在冷库里,当场解冻要二十分钟;炭火从零开始点,熏得眼睛疼,又花了十几分钟;连烤串的先后顺序都没定,三个人挤在案板前手忙脚乱。客人没有等待的耐心,四十多分钟后第一串才送上桌。
LLM 服务的“开业第一天”对应的是副本刚刚启动的时刻。一个 7B 参数的模型,权重文件有十几 GB 到几十 GB,从对象存储拉下来再加载进显存,这是第一道坎;紧接着,推理框架要把 Python 描述的计算图翻译成 GPU 可执行算子,触发算子融合和内存规划,这是第二道坎;第三道坎是显存里什么都没有——没有 KV 缓存、没有分配好的页、采样器也未初始化,第一个请求被迫承担所有“首次初始化”的成本。
用一条加法描述这段“冷”时间:
只要服务不重启、不扩容,这种“冷”的代价只出现一次;但流量高峰一来需要扩容,新副本就要再经历一次“点火”,相当于每次扩张都要重开一家分店。
- 冷启动总耗时整个冷启动期间用户等待的总时间,是三个阶段开销的叠加。
- 权重加载耗时从磁盘或对象存储把权重搬到 GPU 显存的时间,通常占据冷启动大头。
- 计算图编译耗时把模型算子图翻译成 GPU 可执行内核的时间,和算子数量、融合策略有关。
- 预热执行耗时运行一次虚拟推理,把内核、KV 缓存和采样器全部实例化所需的时间。
模型预加载:开店前把肉搬上案板
开店第二天,你学到的第一课是提前备货。凌晨五点到店,把肉从冷库搬到案板上切好、腌好,客人来时直接就能上烤架。对应到推理服务,就是“模型预加载”:权重不等到第一个请求才拉取,而是在副本创建时就加载进显存。
预加载最朴素的做法是在容器启动阶段执行加载逻辑,比如用 initContainer 先拉权重,或者服务进程用 startup 钩子主动完成加载。更彻底的做法是把权重常驻在共享显存或节点本地缓存里,新副本直接读取,省掉大部分网络传输。与此同时,内存规划也要提前做:给模型权重、KV 缓存和激活值分别预留空间,避免运行时动态分配造成额外停顿。
把预加载从冷启动总时间里剥离出来,用户能感知的时间就变成了:
预加载不是把问题变没,而是把问题移到没人的时候解决。就像烧烤店半夜备料,客人看不见,也就不会抱怨。
- 用户可见冷启动时间从请求到达副本到响应返回的等待,是 SLO 里真正被计量的部分。
- 完整初始化时间加载、编译、预热全部耗时相加得到的原始冷启动时间。
- 预加载节省的时间在请求到来之前已经完成的准备工作量。
- 优化表达式把预加载完成的部分从可见时间中扣除,就是用户实际的等待时长。
权重预热:让炭火先烧到最旺
肉切好了,炭却没点着,客人来了依然要等。烧烤店真正的秘诀是提前两小时点火,让炭烧到通红、炉温均匀,食材一放上去就能听到滋滋声。
推理服务里的“炭火”是那些按需加载的 CUDA 内核。PyTorch 等框架默认懒加载:第一个请求经过某个算子时,它才编译并装载对应的 kernel。预热就是在服务启动后立刻发一个虚拟推理请求,把前向计算涉及的算子全部触发一遍,同时初始化 KV 缓存页和采样器。这样真正的用户请求到达时,所有“炉眼”都已经是热的。
一个普通请求不能替代预热,因为短 prompt 只触达少量算子,batch 形状一变,新的内核又得现场编译。预热请求要覆盖有代表性的输入长度和 batch 配置。预热完成度用已装载内核占全部内核的比例来衡量:
预热不是一次性的仪式,而是每次副本拉起、每次滚动更新都必须重复的动作。服务就该安静地待在预热区,直到炉火全红再迎客。
- 已装载内核数预热请求执行后,真正进入显存并随时可调度的 CUDA 内核数量。
- 全量内核数模型计算图在各类输入形状下可能触发的全部内核数量。
- 预热完成度两者相除得到内核装载比例,平台用它判断预热是否达到可接流量的标准。
图编译缓存:菜谱写一次,下次直接翻本子
烧烤店的菜单如果每天重新画一遍,开店前几小时就耗在写菜单上了。聪明的老板会把经典菜单打印好,客人点单时直接拿出来用。
大模型服务同样会“从零画菜单”:每次新副本启动,都要把计算图重新编译一遍,这在深模型上可能耗费几十秒甚至几分钟。图编译缓存就是把编译产物序列化到磁盘或共享存储,下次启动直接读取,跳过重新编译。一个天然的问题是:什么时候能复用?答案取决于“菜单”是否变了。模型架构、GPU 型号、驱动和 CUDA 版本、推理框架版本,任何一个变化都会导致编译产物不兼容,因此要用哈希指纹校验,匹配成功才读取。
引入缓存后,副本启动时的平均编译开销由命中率决定:
有了编译缓存,每次扩容都像翻出旧菜单直接下单,而不是让新厨师重新发明菜谱。
- 缓存命中率新副本启动时编译产物能直接复用的概率,由哈希匹配结果决定。
- 缓存读取耗时从本地磁盘或共享存储把编译产物读入显存所需的时间。
- 重新编译耗时缓存未命中时必须从头编译一次计算图的时间。
- 期望编译耗时以命中率加权平均,得到任一副本启动时的平均编译开销。
弹性伸缩:高峰期前先开分店
烧烤店周末晚市会爆满,有经验的老板会在下午四点就开始支第二套炉子、叫临时工来帮忙,而不是等店门口排起长队再手忙脚乱。
弹性伸缩策略就是给服务“提前开分店”。常见的做法是基于流量预测扩容:利用历史请求曲线预测高峰时段,提前把副本从 10 个扩到 20 个。关键的约束是“扩容前置”——新副本必须完成加载、编译、预热才能接流量。如果流量到了才开始扩容,新副本还在点火,请求全部压在旧副本上,延迟照样爆炸。
扩容数量需要科学计算。副本太少,请求排队;副本太多,资源浪费。最小副本数可以由排队理论估算:
扩出来的新副本还要配“慢启动”保护:刚就绪时只分配少量流量,让模型服务和底层缓存逐渐热透,而不是被瞬间流量冲垮。
- 单位时间请求量服务入口每秒收到的请求数,决定系统需要多大的并发承载能力。
- 平均请求时长从接受请求到完成生成的单实例占用时间,体现每个负载的大小。
- 期望副本利用率容量规划时希望单副本长期保持的负载率上限。
- 最少副本数按公式计算后向上取整,确保扩出来的实例数能承接峰值流量。
就绪探针:门开之前先试炉子
新分店支好炉子、码好肉串,怎么知道可以开门接客了?聪明的老板会先试烤一串:炭有没有热、串会不会散、调料够不够。全部通过,才挂出“营业中”的牌子。
Kubernetes 里的就绪探针(readiness probe)就是这个试炉动作。服务需要暴露三个关键的预热指标:权重加载进度、计算图编译完成数、预热请求执行状态。副本只有在这三项全部达标时,/readyz 才返回 200,负载均衡才会把流量导进来。探针没过,流量就不会进副本,用户也就不会体验到冷启动的漫长等待。
就绪判定可以用一个逻辑表达式描述:
就绪探针是 SLA 的看门人。有了它,滚动更新和弹性扩容才能放心自动执行—新副本没准备好之前,流量永远只流向老店。
- 权重加载完成度显存中权重是否全部就位,这是提供推理服务的前提。
- 算子编译完成度计算图中每个节点是否都有可执行内核,编译完成前不应接流量。
- 预热完成度虚拟请求是否成功执行,KV 缓存和采样器是否初始化完毕。
- 就绪判定表达式三个条件同时为真,探针才返回 200,服务才进入负载均衡池。
落地实践:开业彩排与流程固化
把前面所有招数串起来,就是一套完整的开业彩排:提前备料、炭火烧红、菜单备好、分店到位、探针把关。最后一步是验证整套流程真的有效。
落地时有三件事最容易踩坑。第一是资源配额:预加载和预热阶段需要额外的 CPU 和内存,Kubernetes 的 Pod 资源请求要预留这份余量,否则预热请求会被系统 OOM 杀掉。第二是调度亲和性:如果每个节点都要重新拉一遍几十 GB 权重,扩容就变成了串行下载,因此要让新副本调度到已缓存权重的节点上。第三是压测验证:发布前对比预热前后副本从创建到就绪的时间,用数字确认策略有效。
整体加速比是衡量所有优化手段的综合指标:
烧烤店从第一天的手忙脚乱到第二天的从容不迫,靠的是一整套流程。LLM 服务的冷启动治理也一样—它不是某一个魔法开关,而是把加载、编译、预热、探针、扩缩容串成一条完整流水线。
- 优化前冷启动耗时没有预加载、预热和缓存时,副本从创建到接流量的总时间。
- 优化后冷启动耗时全链路优化后的等效启动时间。
- 加速比两者相除得到倍数,用来衡量预热策略带来的整体收益。
AI 应用场景
大模型网关的蓝绿发布
阶段:模型部署与上线发布
解决的问题:传统发布直接切流量,新版本副本冷启动几十秒甚至几分钟,灰度用户会看到超高延迟,误以为服务挂了。提前预热后,流量切换时副本已完全就绪,发布过程无感知。
具体案例:某对话产品发布新模型时,平台先创建新副本,执行权重加载、读取图编译缓存、发送多组虚拟请求完成预热,等就绪探针全绿后写入注册中心,灰度 10% 流量,观察 15 分钟无异常再全量放量。P99 首 token 延迟从 8s 降到 800ms。
容量高峰前的弹性扩容
阶段:容量规划与弹性伸缩
解决的问题:流量突增触发扩容时,新副本要几分钟才能就绪,往往错过了高峰期。按预测提前扩容,让新副本在流量到达前完成预热,才能真正承担压力。
具体案例:SRE 团队基于历史流量曲线,在促销开始前 10 分钟把副本数从 10 扩到 30。每个新 Pod 并行执行模型预加载、编译缓存读取和预热请求,流量真正到达时 20 个新副本已全部 ready,整体扩容生效时间从 4 分钟压到 40 秒。
Python 演示
"""演示预热如何降低 LLM 服务的首个 token 延迟"""import time class FakeLLMService: def __init__(self, model_name="llama-7b"): self.model_name = model_name self._loaded = False self._warm_done = False self._kernel_count = 0 self._kv_pages = 0 def load_weights(self): start = time.time() time.sleep(0.8) # 模拟几十 GB 权重从磁盘搬入显存 self._loaded = True return time.time() - start def _infer(self, prompt): if not self._loaded: raise RuntimeError("model not loaded") time.sleep(0.05) # 模拟正常推理耗时 return len(prompt) def warm_up(self): for _ in range(4): time.sleep(0.05) self._kernel_count += 1 self._kv_pages = 16 self._infer("[warmup]") self._warm_done = True def is_ready(self): return self._warm_done and self._kv_pages > 0 def serve(self, prompt): start = time.time() if not self._warm_done: time.sleep(0.2) # 冷启动: 内核未装载, 额外付出编译时间 self._infer(prompt) return round((time.time() - start) * 1000, 1) def main(): svc = FakeLLMService() print("[冷启动阶段]") load_t = svc.load_weights() print("权重加载耗时: %.1f s" % load_t) print("\n[服务已启动, 但还没预热]") cold = svc.serve("hello, what is the weather?") print("首个请求(冷): %.1f ms" % cold) print("\n[执行预热]") svc.warm_up() print("就绪状态:", svc.is_ready()) print("\n[预热后连续请求]") for i in range(3): print("第 %d 个请求: %.1f ms" % (i + 1, svc.serve("tell me a story"))) if __name__ == "__main__": main()
time.sleep(0.8)模拟几十 GB 权重从磁盘读入显存,是冷启动最直观的耗时来源。if not self._warm_done:模拟真实服务内核未装载时的现场编译开销,冷请求因此额外多等 200ms。self._kernel_count += 1每执行一次虚拟请求就多装载一个内核,对应真实场景中算子的懒加载机制。self._infer("[warmup]")发送一次假的推理请求,把前向链路完整跑一遍,内核和 KV 缓存随之就位。svc.warm_up()部署脚本在接流量前调用预热接口,是“烧烤店开门前点火”的直接体现。svc.is_ready()对应 Kubernetes 就绪探针,只有返回 True 才允许副本进入负载均衡池。
常见误区
误区线上服务跑得好好的,冷启动就是第一次上线时的事,之后再也不会遇到。
正解服务一旦扩容、重启、滚动更新或故障转移,新副本就是一次全新的冷启动。烧烤店开三家分店,每家分店都要重新点火、重新备料。容器环境下每次水平扩展都会复制一份“冷”实例,冷启动是高频事件而不是一次性事件。
为何常见:大家习惯用单机视角理解服务,以为进程“活着”就等于服务能用,忽略了新副本要从零加载和编译的完整过程。
误区预热不就是发几个请求吗?发一两次就够了吧,反正内核装完都一样。
正解请求的 prompt 短、batch 小,只能触达少量算子和内核;长上下文和动态 batch 会触发新形状的内核,现场还得重新编译。有效的预热要覆盖有代表性的输入长度和 batch 配置,并执行一次完整的前向计算,让采样器和 KV 缓存也完成初始化。
为何常见:把预热理解成网络连接保活或心跳,忘记了真正要热的是算子级内核和显存状态,而不是连接本身。
误区编译缓存存到磁盘就一劳永逸了,下次直接读,省得再编译。
正解缓存与模型架构、GPU 型号、驱动/CUDA 版本、算子库版本强绑定。升级推理框架或更换显卡后,旧缓存会因哈希指纹不匹配而失效,必须重新编译。所以缓存要做哈希匹配校验,失效时自动回退到全量编译,而不是无条件信任。
为何常见:想当然地把编译产物当作普通数据文件,忽略了底层运行环境对指令集和内核格式的强依赖。
误区就绪探针返回 503 说明服务坏了,Kubernetes 会重启 Pod,所以预热期间探针要赶紧通过。
正解就绪探针失败不等于容器崩溃,它只负责把流量挡在门外。只要 liveness 探针正常,容器进程就会继续运行;就绪探针会反复重试,预热完成后自然转绿。如果为了让探针尽快通过而缩短预热,半热实例就会接入流量,P99 直接被打爆。
为何常见:混淆了 liveness(活着吗)和 readiness(能用吗)两个探针的角色,把“没准备好”误判为“已经死了”。
面试题
Q1. LLM 服务冷启动的典型表现是什么?
- A.首请求延迟远高于后续请求← 正确答案
- B.请求吞吐量随时间线性衰减
- C.模型精度在服务后期变差
- D.显存占用随服务时间持续下降
答案:A。冷启动指新副本从零初始化,第一个请求要承担权重加载、图编译、内核预热等全部开销,延迟显著偏高。吞吐量衰减、精度变化、显存下降都不是冷启动的典型特征。
Q2. “模型预加载”的主要做法是什么?
- A.创建副本时立即把权重读入显存← 正确答案
- B.每个请求都重新读取一次权重
- C.把权重放在 CPU 内存供多次拷贝
- D.用更大的 batch 替代权重加载
答案:A。预加载的核心是“把搬食材的时间放在营业之前”——副本创建时,甚至在请求到达前,权重已从磁盘读入显存。请求到来时直接可用,而不是临时搬运。
Q3. 权重预热的核心目标是什么?
- A.让全部算子的内核装载完毕← 正确答案
- B.让模型 loss 降到 0
- C.让服务进程占用的内存更大
- D.让模型完成一轮反向传播
答案:A。预热通过虚拟推理触发所有算子对应的 CUDA 内核装载,让推理链路完全就绪。loss 训练、内存膨胀、反向传播都和预热无关,那些是训练阶段的事。
Q4. 图编译缓存的哈希匹配中,通常不包含哪个信息?
- A.模型架构版本信息
- B.GPU 驱动与 CUDA 版本
- C.推理框架运行版本
- D.客户端来源 IP 地址← 正确答案
答案:D。缓存指纹由运行环境决定:模型架构变了、GPU 驱动换了、框架升级了,编译产物都可能失效。客户端 IP 不影响计算图编译结果,所以不需要进入哈希 key。
Q5. 预热完成前,就绪探针应该返回什么状态码?
- A.200 表示服务就绪
- B.503 表示暂未就绪← 正确答案
- C.302 表示需要重试
- D.401 表示未授权访问
答案:B。就绪探针的作用是告诉负载均衡“现在别把流量导给我”。预热没完成时返回 503,流量就不会进入副本;预热全部完成后返回 200,副本才正式对外服务。
Q6. 弹性伸缩中的“扩容前置”指的是什么?
- A.新副本先加载预热再接流量← 正确答案
- B.扩容前暂停全部业务流量
- C.扩容前把显存占用量翻倍
- D.扩容前清空所有 KV 缓存
答案:A。扩容前置指新副本必须先完成模型加载、图编译、内核预热,才能参与流量分发。否则流量到了,新副本还在“点火”,延迟就会爆炸。暂停流量、翻倍显存、清空缓存都不是合理的前置条件。
Q7. 图编译缓存命中率为 p,缓存读取耗时 t_c,未命中时重新编译耗时 t_r,则平均编译耗时为多少?
- A.p×t_c + (1−p)×t_r← 正确答案
- B.p×t_r + (1−p)×t_c
- C.t_c + t_r×p
- D.t_c×(1−p) + t_r
答案:A。期望值等于每个分支的耗时乘以其发生概率再求和。命中时花 t_c,概率为 p;未命中时花 t_r,概率为 1−p,所以平均耗时是 p×t_c + (1−p)×t_r。这和“翻菜单的快路径 + 重新画菜单的慢路径”加权平均是一个道理。
Q8. 哪些手段能缩短 LLM 服务冷启动时长?
- A.模型预加载到 GPU 显存← 正确答案
- B.推理图编译缓存落盘← 正确答案
- C.提高 token 化并发线程数
- D.启动时发送虚拟请求预热← 正确答案
答案:ABD。预加载把权重搬运提前,编译缓存省掉重复编译,虚拟请求预热让内核和缓存就位。这三者直击冷启动三大耗时来源。提高 token 化线程数优化的是请求处理吞吐,不是冷启动瓶颈。
Q9. 新副本进入负载均衡池前,哪些条件应当满足?
- A.权重文件加载完成← 正确答案
- B.算子图编译完成← 正确答案
- C.预热请求执行成功← 正确答案
- D.容器端口已监听
答案:ABC。就绪的本质是加载、编译、预热三个状态全部完成。容器端口已监听只能说进程活着,代表 liveness 而非 readiness;端口开了但内核没装载,请求进来照样慢到超时。
Q10. 某 7B 模型副本冷启动分解:T_load=25s、T_compile=60s、T_warm=15s,缓存读取耗时 5s,缓存命中率 80%。启用图编译缓存后,该副本从启动到可接流量的最短时间约为多少?
- A.56 秒← 正确答案
- B.45 秒
- C.100 秒
- D.72 秒
答案:A。有缓存时编译阶段耗时 = 0.8×5 + 0.2×60 = 16 秒。副本可接流量所需总时间 = T_load + 16 + T_warm = 25 + 16 + 15 = 56 秒。45 秒忽略了未命中部分的编译成本,100 秒是完全不读缓存的全量编译时间。