
做 LLM serving 的人应该都被 GPU 显存折腾过。模型权重、KV cache、临时 activation、请求上下文全都挤在有限的 HBM 里推理引擎一崩整个状态基本归零。最近读到的这篇 Dynamo 论文把问题一下子点透了把 GPU 显存生命周期从推理引擎里解耦出来故障恢复才能做到秒级。这里的核心关键词不是“快”而是“归谁管”——显存不再跟着推理引擎进程生死而是由独立的资源层托管。这个思路对自研推理引擎、LocalAI 这类轻量封装还有 vLLM/TGI 生产部署都有参考价值。想搞清楚 LLM serving 的高可用怎么做、故障恢复怎么设计这篇值得慢慢拆。1. 先说痛处LLM serving 的故障恢复为什么这么慢1.1 本质是有状态服务不只是进程重启很多人在设计推理服务高可用时第一反应是“进程挂了就拉起一个新进程”。这个想法放在普通 Web 服务上没问题放在 LLM serving 上就完全不够。LLM serving 的有状态性体现在三块模型权重通常有几十 GB 到上百 GB从磁盘加载一次就是漫长的等待KV cache 是请求执行过程中不断增长的动态状态长上下文请求可能占掉十几 GB 显存调度器还记着每个请求的进度、优先级、显存块分配关系。这些状态全都在 GPU 显存里推理引擎进程一退出显存就会被驱动回收状态等于全部蒸发。恢复代价最隐蔽的是 KV cache。你可能会想KV cache 本来就是推理动态算出来的丢掉后让客户端重试不就行了对于短请求确实可以但长上下文、多轮对话场景代价非常高用户的上下文可能几百上千 token重新 prefill 一遍GPU 又要从头跑一遍 attention 计算。越长的上下文恢复成本越高客户端体感也越差。所以 LLM serving 的故障恢复绝不能简单理解成“重启进程”。1.2 常规高可用方案为什么显得笨重目前生产环境里常用的恢复手段我大概梳理了一下都有明显的短板。方案恢复时间显存开销核心问题冷备重启分钟级低模型权重重新加载KV cache 全部丢失主备热备亚秒到秒级接近 2 倍成本太高显存同步本身复杂检查点快照秒到分钟级额外磁盘/内存快照与实时状态有时间差回放会丢增量客户端重试看请求长度低长上下文重复 prefill代价可能超过恢复本身主机热备看着快但等于把所有请求的状态再复制一份。检查点快照又受 PCIe 带宽和显存容量的双重限制快照频率和恢复精度只能二选一。更麻烦的是这些方案都在“重新建立一个状态”上做文章没有人去质疑一个更底层的问题原来的显存状态为什么一定要丢Dynamo 这篇论文的核心反而不是什么新奇的加速技巧而是把“恢复”重新定义成“重新挂载”。如果崩溃前显存里的数据还在那恢复就不是从磁盘拉权重而是让新引擎接管这块还热着的显存。这个思路绕开了重加载、重 prefill、重算 KV cache 这些最费时的环节。2. Dynamo 的核心设计把显存生命周期从引擎里拿出来2.1 解耦的不是模块是资源所有权传统推理引擎同时承担两件事算 attention 和管显存。引擎启动时创建显存池运行中申请、释放、整理显存退出时驱动把这些资源一并回收。这看起来没问题但它把资源的生命周期和计算进程的生命周期死死绑在了一起。Dynamo 的做法是引入一个独立于推理引擎的显存生命周期管理者。推理引擎从“显存的主人”变成“显存的租户”。正常工作时引擎向资源层申请显存块、加载权重、写入 KV cache崩溃时资源层并不会因为引擎进程退出就把显存释放而是继续保留这些显存块和对应的元数据。新引擎起来后通过资源层提供的句柄重新挂载到同一批显存块上。我用一个厨房的类比来理解这件事传统方式是厨师把所有备好的食材、调料、灶台都放在自己面前厨师一离开后厨就开始清台下一个厨师来必须从头准备。Dynamo 相当于把备料间独立出来厨师换人了已经切好的菜、配好的料都还在新厨师直接上手炒就行。2.2 “生命周期”具体包含哪几段显存生命周期被拆出来以后至少包含四个阶段每个阶段都有自己的管理职责。申请与预留资源层预先划出若干显存块记录块的大小、地址、归属引擎、用途。挂载与使用推理引擎启动后通过资源层接口 attach 到显存块开始读写权重和 KV cache。接管与重挂载引擎故障后资源层冻结相关显存块等待新引擎按元数据重新挂载。释放与回收请求全部结束引擎正常下线后显存块才真正归还给资源池。这四个阶段里最容易做砸的是第三段。因为“接管”不是说资源层什么都不做就行它需要保证显存块不被其他请求占用、元数据不被覆盖还要在引擎恢复期间维持一个一致的资源视图。这个设计和操作系统的进程间共享内存很像只不过对象从普通内存换成了 GPU 显存操作粒度从字节变成了大块 tensor 和 KV cache 页。2.3 秒级恢复成立的三个前提想做到秒级恢复不能只嘴上说“显存还在”还需要三个前提同时成立。模型权重所在的显存块没有被释放且分配拓扑可重建。KV cache 的分配关系能从共享元数据恢复而不是靠扫描全卡内存。调度状态、请求上下文和显存对象之间有一份可恢复的账本。第一个前提相对容易满足只要资源层不主动释放即可。第二个前提需要显存块管理做到足够细KV cache 被切分成若干固定大小的块资源层知道每个块属于哪个请求、哪一层、哪个 token 区间。第三个前提最容易被忽略引擎崩溃时如果只有一个“显存还活着”的结论而不知道每个请求跑到哪一步新引擎还是没法接管。很多工程实现会把这份账本放到 CPU 内存或者独立的状态服务里每次显存分配、释放、KV cache 扩容都先写日志。故障恢复时先恢复账本再恢复显存映射。这个思路和数据库的 WAL 很像不是重新算而是靠日志重放到崩溃前的位置。3. 秒级恢复的实操机制从故障检测到请求续跑3.1 先拆一段可量化的恢复链路“秒级恢复”是一句很漂亮的话但落到工程上必须拆成可测量的环节。我一般会把恢复链路切成五段故障检测、资源接管、引擎重启、显存重挂载、服务就绪。阶段我比较关注的点参考量级故障检测心跳超时还是主动探测会不会误判500ms 到 1s资源接管显存冻结、租约转移、新引擎认领数百毫秒引擎重启CUDA context 初始化、引擎模块加载1s 左右显存重挂载从资源层枚举显存块并重新映射1s 以内服务就绪KV cache 账本恢复、调度器对外提供地址数百毫秒论文里给出的秒级结果应该是这五段叠加后的整体数字而不是单指“进程重新启动”的时间。这里我建议不要只看平均值重点要看 P99。故障恢复如果 95% 的请求都做到 1 秒但某一次因为显存块定位慢了 30 秒线上照样会被放大成事故。3.2 故障检测宁可慢一点也别误杀故障恢复的第一步是确认“谁挂了”。听起来简单做起来全是细节。GPU 卡住的时候心跳线程本身可能还在跑因为它是 CPU 侧的而一个进程如果进入了死循环TCP 连接可能还正常普通健康检查根本探不出来。我的经验是故障检测需要分层。CPU 侧做进程存活检测GPU 侧做运行状态探针资源层再做租约超时判断。租约机制尤其重要引擎在正常运行时周期性续约一旦超时未续约资源层才判定该引擎已经失去对显存块的控制权。租约时间不能设得太短否则 CUDA context 初始化瞬间、容器迁移、瞬时线程卡顿都会造成误杀也不能太长不然真正的故障会拖到很晚才被发现。很多系统把租约设在 1 到 3 秒之间再配合 CPU 侧的秒级心跳。3.3 显存重挂载与 KV cache 元数据重建新引擎起来后首先要做的是向资源层查询显存块列表。这里的关键不是把显存“复制”一遍而是“重新建立映射”。资源层保存了每个显存块的 ID、大小、偏移和用途新引擎拿到这些信息后在自身进程里重建指向这些显存块的 tensor 视图。KV cache 的恢复比权重更麻烦。权重是只读的只要显存块还在映射回来就能用。KV cache 是动态的每个请求的块分配情况、每个序列的长度、block table 的位置都要从元数据账本里恢复。我见过比较稳的实现是显存资源层只负责保存“哪些物理块属于哪个逻辑块”调度器负责保存“逻辑块和请求序列的对应关系”。两层账本结合起来新引擎才能准确知道当前可以继续 decode 到哪个 token 位置。这一步听起来抽象本质就一句话恢复的是“地址关系”和“逻辑关系”不是再做一遍推理计算。这也是 Dynamo 能比检查点回放快出一个量级的原因。3.4 客户端视角请求重放与幂等显存和 KV cache 恢复好以后还有最后一个工程细节客户端怎么继续请求。如果引擎故障时某个请求已经生成了 30 个 token 但还没返回给客户端新引擎恢复后不能把这个请求当作全新请求从头生成一遍。否则客户端会收到前面 29 个 token 的重复内容语义直接错乱。我比较推荐的做法是在接入层做一个“已发送偏移量”的记录。每个请求在流式返回时由接入层记录最后成功发给客户端的 token 位置故障恢复后新引擎从 KV cache 里对应的位置继续生成接入层从记录位置开始续传。这类似于消息队列里的 consumer offset 管理只是 offset 对应的不是消息序号而是生成序列的 token 位置。很多做故障恢复的团队会在这一步栽跟头只恢复了模型服务没恢复客户端连接状态最后照样被用户投诉。4. 评估体系与实验设计别只盯着“秒级”4.1 恢复时间不是唯一指标一个故障恢复方案是不是好用至少有六个维度要看。恢复时间只是其中之一。指标为什么重要平均恢复时间(MTTR)最容易宣传但容易被极少数极端情况拉偏P99 恢复时间决定线上真实体感比平均值更值得盯正常推理性能损失持久化显存分配器可能带来额外开销显存预留开销预留的显存池会压缩可用于 batch 的容量恢复成功率恢复路径越长失败概率越高必须单独统计故障类型覆盖engine 崩溃、OOM、GPU 卡死、整节点故障要分开测很多论文都擅长把恢复时间画得很漂亮却不告诉你为了让显存可恢复平时要多占多少显存、每次显存分配要多做多少元数据写入。真正要落地的时候这些才是决定方案能不能上的关键。4.2 故障场景要覆盖“最坏情况”我在评估这类系统时会特别看两件事故障注入到底打了哪些场景以及恢复过程的“最后一次成功状态”离故障点有多近。最基础的场景是主动 kill 掉推理引擎进程这部分 Dynamo 思路大概率表现很好因为显存没有被回收。但 GPU 硬件故障、CUDA error 导致 context 失效、显存 ECC 错误这类更贴近生产的场景问题就复杂了。硬件故障发生时显存里的数据可能已经损坏资源层就算把显存块冻结住也没法保证恢复出来的 KV cache 是可用的。所以如果团队做高可用设计别把 Dynamo 当成所有故障的万能药它更擅长的是“引擎级故障”和“软件级崩溃”。另外像 LocalAI 这类把推理引擎、模型文件、启动脚本打包在一起的轻量方案直接套用这种架构会比较吃力。因为解耦的前提是显存资源层能独立于引擎存在如果本地封装把所有东西都塞在一个进程里故障恢复时依然要从头拉起整条依赖链。4.3 从论文指标到生产结论读到一篇论文不能只记住“秒级恢复”这个结论。我会先看它的 baseline 是什么如果对比的是冷重启那秒级恢复并不稀奇冷重启本来就要重新加载权重如果对比的是热备和检查点那意义就不一样。还要看它的负载类型。长上下文请求占比越高KV cache 的恢复价值就越大短请求占比高客户端重试的成本本身就不高恢复快慢的收益会被削弱。也就是说同一个方案在 chatbot 和 RAG 场景里的收益完全不同。决策的时候先把业务流量结构摸清楚再判断这套架构值不值得引入。5. 落地时的避坑指南从论文到生产环境5.1 显存预留和资源碎片的权衡第一个坑来自显存池预留。想要故障后还能接管显存资源层通常要预先保留一部分显存或者保证显存分配的元数据足够轻量。预留做多了正常推理能开的最大 batch 就变小吞吐直接受影响预留做少了引擎峰值显存增长时又可能申请不到资源反而增加 OOM 风险。实际操作中我建议先做显存曲线采集统计不同并发、不同上下文长度下的显存峰值分布再决定预留水位。不要在没数据的情况下拍脑袋预留 20%。预留的粒度也很重要建议按“显存块”而不是按“整卡”预留这样多个请求可以共享预留区间避免单请求放大开销。显存碎片是另一个隐蔽问题。故障恢复后新引擎重挂载显存块的顺序如果和原引擎不一致某些本可以连续分配的块会变成碎片长期运行后可用显存逐渐缩小。比较稳的做法是资源层在正常运行时尽量维持确定的分配顺序并把顺序信息写进账本恢复时严格按账本里的顺序重建映射不重新做一次贪心分配。5.2 跨容器/跨实例的显存共享限制Dynamo 这种设计能够成立有一个技术前提新引擎进程能访问旧引擎持有的显存块。这在同一进程内没问题跨进程依赖 CUDA IPC 之类的共享机制。落地到容器环境以后情况会复杂不少。同一台物理机、同一个 GPU、容器运行时配置一致共享显存句柄通常可行。但一旦涉及 MIG 切分、GPU 虚拟化、不同容器镜像、不同 CUDA 版本显存句柄能不能跨容器传递就是未知数。不要看论文里有个漂亮的架构图就直接认为所有环境都能这么干。上线前先做一个最小验证进程 A 申请一块 GPU 显存并写入数据进程 B 在同一环境里能否通过句柄读取同一块显存。这一步不通过后面的设计全得改。5.3 一致性与日志顺序别把恢复做成数据损坏恢复方案里还有个经常翻车的点显存写入和元数据日志并不是天然一致的。引擎可能刚往显存里写完一段新的 KV cache还没来得及更新账本里的序列长度就崩溃了。恢复时账本显示旧长度显存里却已经有新数据新引擎如果按旧长度继续生成就会覆盖一部分已经计算好的状态。解决思路有两种。一种是写日志和写显存放在同一个事务里但 GPU 侧很难做原子操作。另一种是接受“丢失最后一段增量”的代价恢复后回退到账本记录的最后一个一致点再让接入层从那里续跑。这比追求完全无损失更现实。我的建议是明确“可丢失窗口”是多少别把恢复系统设计成必须在崩溃瞬间达到严格一致否则复杂度会失控。5.4 先说一个最值得做的最小闭环如果你也想在项目里尝试这套思路我不建议一上来就做完整的资源层、租约机制、KV cache 账本。先从最小闭环开始单机部署两个推理引擎实例公共资源层只负责管理一份模型权重的显存块不做 KV cache 恢复故障注入只做引擎进程 kill。等这个闭环跑通了再逐步加入 KV cache 账本、请求续传、跨实例共享。这个顺序是我实际做过故障恢复之后的体会。第一步能验证最关键的技术假设进程退出后显存是否真的可以被接管。如果这一步都因为驱动、容器或加速库的限制走不通后面做再多都是白费。反过来第一步跑通后你会对“显存生命周期解耦”这件事有非常具体的感知再去看论文里的各种设计决策也能一眼判断哪些是核心、哪些是包装。这套方案的边界也很清楚它擅长处理推理引擎崩溃不能替代整机热备和训练侧容错。把合适的工具放到合适的位置比追求一个“万能秒级恢复”靠谱得多。