Netflix 自建 LLM 推理平台笔记:四个层层相扣的决策
- 约 13 分钟读完 - 全文 5114 字Netflix 自建 LLM 推理平台笔记:四个层层相扣的决策
原文:In-House LLM Serving at Netflix(Netflix TechBlog,2026-07,AI Platform 的 Model Runtime 与 Inference 团队) 目标读者:正在或准备自建 LLM 推理服务的工程师,尤其是要在"用托管 API"和"自己跑全栈"之间做取舍的团队。 阅读目标:看清 Netflix 这套平台的四个核心决策(引擎、打包、API 面、发布)各自约束了什么,以及哪些坑是设计阶段想不到、只有生产流量能教会的。
〇、为什么不直接用托管 API
多数组织通过托管 API 消费 LLM,Netflix 选择在既有生产环境内跑全栈——从模型部署到推理,而不是另起一个 ML 孤岛。原文明确说:这篇文章只讲"曾经认真考虑过备选方案"的决策,而且重点是为什么这么选,以及生产暴露了哪些设计阶段没预料到的东西。这个姿态本身就值得学:决策文档的价值不在结论,在被否决的选项和事后才显形的代价。
一、架构总览:LLM 不是特例
Netflix 的会员规模 ML 由一个统一的 JVM serving 系统打头,负责路由、A/B 测试逻辑、候选生成、特征获取、推理、后处理和逐级日志,实时与离线批量缓存两条路径都支持。调用方到达推理有两条路:经 serving 系统的 gRPC 路径,以及新兴 LLM 应用用的直连 HTTP 路径。
推理在哪跑取决于模型大小:小的 CPU 模型直接在进程内跑,省掉远程调用开销;大模型需要 GPU,serving 系统本地做前后处理,把推理委托给远端服务 MSS(Model Scoring Service)。MSS 是共享推理后端,XGBoost、TensorFlow、PyTorch、LLM 全走同一套接口,底下是 NVIDIA Triton Inference Server 管模型加载、批处理和 GPU 调度。Triton 之上还有一个 Java 控制面,管部署、版本、健康检查、自动扩缩和多 region rollout——模型作者只管打包产物和配置,控制面负责 provisioning GPU 实例和零停机升级。
flowchart TD
subgraph CALLER["调用方两条路径"]
GRPC["gRPC 路径<br/>经 JVM serving 系统<br/>路由 A/B 特征 后处理 日志"]
HTTP["HTTP 直连路径<br/>新兴 LLM 应用"]
end
subgraph MSS["MSS 共享推理后端<br/>XGBoost TF PyTorch LLM 统一接口"]
TRITON["Triton Inference Server<br/>模型加载 批处理 GPU 调度"]
CP["Java 控制面<br/>部署 版本 健康检查 扩缩 多 region"]
CP --> TRITON
end
GRPC --> MSS
HTTP --> TRITON
INPROC["小模型 CPU 进程内推理"] -.->|"省远程调用开销"| GRPC
style MSS fill:#e8f5e9
一句话:LLM 不做特殊雪花。XGBoost 集成和大型 LLM 用同一个 gRPC 调用打分,客户端库、健康检查、部署流水线全部复用。这个"不特殊化"原则后面会反复出现。
二、决策一:引擎从 TensorRT-LLM 换到 vLLM
平台最初建在 TensorRT-LLM 上——当时性能领先,且已和 Triton 集成。到 2025 年夏天两件事变了:开源引擎的性能差距基本抹平;工作负载也从单一形态扩展到 embedding 生成、ranking/retrieval 的 prefill-only 推理、自回归解码、以及带逐步约束逻辑的自定义模型。重新 benchmark 后选 vLLM 作为 paved-path 引擎,理由是运营契合度而非峰值性能:
- 自定义模型架构免多步编译流水线,非标模型迭代更快;
- 有自定义解码逻辑的扩展钩子(后面约束解码全靠它);
- 可调试性好——编译型引擎里查失败和中间状态很痛;
- 研究侧很多同学本来就在用 vLLM,研究到生产的交接成本低。
注意选型口径:不是"谁快选谁",是"谁在我们的负载组合 + 团队现状下总成本最低"。
三、决策二:打包方式,选 vLLM backend 但要锁版本
Triton 支持两种打包,区别在模型产物与前端升级的耦合紧度:
- Python backend:作者在打包时显式定义输入输出 tensor 规格,规格冻结在产物里,必须和前端 request builder 的期望匹配——前端每次动 I/O 规格,打包代码都要配合改,否则运行时才挂。
- vLLM backend:产物只有一个指向权重和 tokenizer 的 JSON 配置,I/O 规格由 Triton 在部署时动态生成。模型和前端各自独立演进。
vLLM backend 是架构上正确的默认,但生产咬了两口:
- Triton 与 vLLM 版本漂移。Triton 的 vLLM backend 是针对特定 vLLM API 面编译的,一漂移就整个加载失败——真实案例是 Triton 25.09 还在 import
vllm.engine.metrics,而这个模块在 vLLM 0.11.2 里已经删了。对策:烘焙镜像时锁死兼容版本对,并禁止模型作者在打包时覆盖 vLLM 版本。 - 自定义模型逻辑。vLLM backend 假设标准 HuggingFace 兼容模型并接管全生命周期;需要自定义前后处理、ensemble 流水线、非标 tokenization 的模型,只能退回 Python backend 拿
execute()的完全控制权。这个逃生门对一部分模型会长期必要。
四、决策三:API 面——gRPC 不动,加一层 OpenAI 兼容前端
内部打分协议保持统一 gRPC 不变,但 OpenAI 兼容 API 已成 LLM 生态事实标准——推理引擎、编排框架、评估工具、客户端库全说它——所以平台把它作为附加前端暴露。回报体现在实验到生产的路径上:从托管模型毕业到自托管微调模型(为质量、延迟、成本或数据隐私),几乎是平滑迁移,同一套 API,代码改动极小。
实现上直接复用 NVIDIA 的 Triton OpenAI 兼容前端:内嵌一个 Triton server,用 TritonLLMEngine 把请求 schema 转成 Triton 推理请求,FastAPI 对外服务;KServe 的 HTTP/gRPC 前端同时开启,Java 控制面仍走 gRPC 访问同一实例。但直接采用暴露了一个缺口:response_format 字段 schema 里接受、实际在到达 vLLM 之前被静默丢弃——调用方以为自己要了 JSON 输出,实际没有任何 guided decoding 约束,拿到畸形 JSON 也收不到任何报错。Netflix 的解法是 git-subtree 掉这个前端并打补丁,在请求时把 response_format 翻译成 vLLM 的 guided decoding 参数。静默丢参数比报错危险得多,这是个通用教训。
五、决策四:发布策略——Red-Black 与 Versioned
GPU 部署拉起比 CPU 服务慢,模型版本间 I/O schema 还可能变,发布要解决的是"新版本上线不掉请求"。平台给两种策略:
flowchart TD
Q["新版本要改 I/O schema 吗<br/>例如 tensor 维度变了"] -->|"否 接口稳定"| RB["Red-Black<br/>新旧并存 健康检查后分阶段切流<br/>新扩容与老缩容同速率 失败原子回滚"]
Q -->|"是 无法避免"| V["Versioned<br/>每个 modelId + modelVersion 独立部署<br/>多版本同时在线服务"]
RB --> RBGAP["已知缺口<br/>消费者要等新版全量才能改配置<br/>迁移窗口内老请求打到新部署会失败"]
V --> VCOST["代价<br/>过渡期 GPU 成本临时上涨<br/>平台按闲置清理旧版本 但永远保留最新"]
style RB fill:#e8f5e9
style V fill:#fff3cd
Red-Black 的协调缺口很微妙:新版本要改 schema 时,上游消费者没法提前改配置(改了会打到还在线上的旧版),只能等新版全量后再切,于是迁移窗口内必然有"老请求打新部署"的失败。Versioned 用冗余部署彻底解耦模型发布与消费者更新。Netflix 的建议是把可变配置(如 tensor shape)直接嵌进推理模型让它版本无关,尽量走便宜的 Red-Black,Versioned 只留给接口变更无法避免的少数情况。
六、两个运维细节
启动序列。直接启动时从 S3 或 Hugging Face 拉大模型太慢,冷启动会超出调度器容忍度;对策是在模型"announce"时就把模型物化到 Amazon FSx,warm start 命中高性能文件系统而非对象存储。另外 OpenAI 兼容前端内嵌 Triton 还是独立跑 Triton,是打包时按部署配置的。其余步骤是机械的:解包、经 Python entry_points 装自定义 vLLM 插件、清理 Prometheus multiprocess 目录、引擎就绪前闸住 gRPC 端口。
统一 metrics 端点。vLLM 把指标写成 PROMETHEUS_MULTIPROC_DIR 下的 .db 文件,Triton 走自己的 Prometheus 端点,两边互不知情;Triton 内置桥接只暴露 40 多个 vLLM 指标里的 9 个,token 吞吐、KV cache 利用率、prefix cache 命中率这些关键指标全缺。解法是加了一个轻量 HTTP 代理:HTTP 拉 Triton 指标,用 MultiProcessCollector 从磁盘读 vLLM 指标,合并成单个 /metrics 输出——存量 dashboard 和告警零改动。
七、深水区:约束解码的规模问题
这是最硬核的一节。Netflix 一些生产负载需要细粒度控制 token 生成,思路是把约束推进解码循环内部——不为非法生成付钱再重试或修补,而是让模型"构造性地"生成合规输出。实现走 vLLM 的自定义 logits processor 接口:每个约束建模为随已生成 token 历史演进的状态机,每步吐出 token 可用性掩码;每个请求配自己的 processor 实例,因为不同请求规则不同。
V0 为什么撑不住:初版纯 Python 实现功能正确,但 vLLM V0 里自定义 logits processor 是逐请求跑的——GPU 为整个 batch 算出 logits,CPU 拷贝过来等传输完,然后串行对每个请求跑约束逻辑(GIL 挡住了并行)。CPU 耗时随 batch 线性增长,尾延迟爆炸,端到端变成 CPU-bound,尽管 GPU 前向是高效批处理的。这个瓶颈在单请求 benchmark 里完全不可见,只在真实并发下现形。
V1 的结构修复(2025 Q4 迁移):vLLM V1 把 logits 处理提升到批级,processor 改写为对批级数据结构操作、跨请求一起算掩码,热路径用 C++ 多线程重写绕过 GIL。代价是 V1 API 要求经 update_state(batch_update) 显式跟踪 batch 成员变化——比 V0 的逐请求接口复杂,但动态演化的 batch 里要维持正确状态别无选择。效果是 logits 处理时间随 batch 增长保持平坦。
两个设计阶段没想到的状态陷阱:
- Partial prefill:V1 做 chunked prefill,一个请求可能跨多个引擎 step 才填完,而
BatchUpdate的粒度分不清"填完了"还是"填了一半",只能内部自己跟踪。 - Preemption:显存压力下 vLLM 可能逐出半完成请求的 KV cache,之后带着不同的 prompt 和输出 token 列表重新调度——这直接打破状态机"输出 token 列表单调增长"的假设。对策是检测 token 历史在解码步之间变短,发现就重置状态机、按新 prompt 重新初始化。
八、工程实践联想
- 单请求 benchmark 会撒谎。 V0 约束解码的瓶颈只在真实并发下出现——GPU 批处理越高效,串行 CPU 段相对越刺眼(阿姆达尔定律的实战版)。凡是有"批 GPU + 逐请求 CPU"形态的系统,压测必须带真实并发模型,否则上线即踩雷。
- 生态兼容比自研协议值钱。 OpenAI 兼容前端让"托管换自托管"几乎零成本,本质是买了个退出期权。但采用第三方组件时要主动审计静默行为——
response_format那种"schema 接受、执行丢弃"的缺口,只有端到端验证能抓到,类型系统和 schema 校验都救不了你。 - 锁版本不是洁癖,是边界合同。 Triton 25.09 对 vLLM 0.11.2 的事故说明:两个各自快速迭代的组件之间,API 面就是合同,合同必须显式 pin 住,且要封死用户绕过 pin 的后门(禁止打包时覆盖版本)。
- 接口变更的协调成本要摆上台面。 Red-Black 的"老请求打新部署"缺口是典型的分布式系统两将军问题变体;Versioned 用资源换解耦是标准解法,但更便宜的做法是从源头消灭破坏式变更——把可变配置嵌进模型。设计 schema 时就该问:这个字段未来变了,谁要协调谁?
- 有状态逻辑要对底层语义变更做回归审计。 partial prefill 和 preemption 两个坑,根因都是状态机的隐含假设(一次填完、token 单调增长)被引擎新语义打破。引擎升级时不只跑功能测试,要把每条状态假设列出来逐条对账。
本篇小结
- Netflix 在既有 JVM serving 体系内自建 LLM 推理全栈,核心原则是 LLM 不做特例:所有模型统一 gRPC 打分,共享 MSS + Triton + Java 控制面。
- 四个决策层层相扣:引擎选 vLLM(运营契合度胜过峰值性能)→ 打包选 Triton vLLM backend(I/O 规格部署时动态生成,代价是版本锁死,自定义模型留 Python backend 逃生门)→ API 面加 OpenAI 兼容前端(顺手修了
response_format静默丢失)→ 发布双策略 Red-Black / Versioned 应对 schema 变更协调。 - 运维两个坑:模型先物化到 FSx 解决冷启动;自建代理合并 Triton 与 vLLM 双 metrics 源(官方桥接只暴露 9/40+ 指标)。
- 约束解码是全文精华:约束建模为状态机 + 逐步 token 掩码;V0 逐请求串行被 GIL 卡死、CPU 随 batch 线性增长,V1 批级化 + C++ 热路径后耗时拉平;partial prefill 与 preemption 两个状态陷阱靠内部跟踪和"token 历史缩短即重置"解决。
- 后续方向:system prompt 压缩、vLLM V1 异步调度、logits processor 向量化成 GPU 融合算子、低精度模型变体。
- 全文最值得带走的元教训:设计评审看不出这些坑,生产负载是最终的验收测试——所以平台要为"发现设计阶段想不到的问题"留好观测口和逃生门。
参考资料
- In-House LLM Serving at Netflix,Netflix TechBlog,2026-07-17(原文)。
- vLLM:开源 LLM 推理引擎,本文的 paved-path 选择。
- NVIDIA Triton Inference Server:MSS 底层的模型加载、批处理与 GPU 调度层。
- TensorRT-LLM:平台初代引擎,被替换前的 baseline。
- KServe:与 OpenAI 前端并置启用的 HTTP/gRPC 前端。
- Amazon FSx:模型物化的高性能文件系统,用于压低冷启动。