Netflix 式内部 LLM Serving:从模型接入到可靠推理平台

2026-07-18 48 预计阅读时间: 1 分钟
来源: netflixtechblog.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

把大语言模型接入公司内部系统,难点通常不在于启动一个推理进程,而在于让它像数据库、消息队列一样成为稳定的基础设施:调用方不必关心模型部署在哪里,平台团队能够控制成本、延迟和权限,业务团队也能持续替换模型而不重写应用。围绕 “In-House LLM Serving at Netflix” 这一主题,可以把内部 LLM Serving 理解为一套完整的平台工程问题,而不是单个模型服务。

Serving 的核心边界

一个可用的内部 LLM 平台,至少要划分出四层职责:

  • 模型运行层:负责加载模型、批处理请求、管理 GPU 或其他加速资源。
  • 服务接入层:提供统一的 HTTP 或兼容 OpenAI 风格的 API。
  • 治理层:处理身份认证、配额、审计、敏感数据过滤和模型访问策略。
  • 业务适配层:把推荐、搜索、内容理解、客服等场景转换成稳定的提示词和结构化输入输出。

这种分层很重要。业务代码不应该直接连接某台 GPU 机器,也不应该把模型名称、上下文长度和重试策略散落在各个仓库里。调用方更适合依赖一个稳定的逻辑模型名,例如 text-summary-v1,平台再把它映射到具体模型版本和推理后端。

内部平台还需要同时支持两类请求:

  • 在线请求关注首 token 延迟、整体响应时间和并发控制,适合交互式产品。
  • 离线请求关注吞吐量和单位成本,适合批量生成摘要、标签或向量。

如果把所有请求都放到同一个队列中,长上下文的离线任务很容易阻塞交互式请求。因此,按优先级、上下文长度和业务类型拆分队列,往往比单纯增加副本更有效。

统一 API,隔离模型变化

可以这样实践:在模型后端前面放置一个很薄的内部网关。网关负责鉴权、请求校验、路由和指标采集,模型服务只处理推理。下面是一个可运行的 FastAPI 示例。它用一个简单的伪模型响应模拟后端,方便先验证接口契约;接入真实推理引擎时,只需要替换 generate_text 函数。

运行前安装依赖:

python -m venv .venv
. .venv/bin/activate
pip install fastapi uvicorn
uvicorn app:app --reload --port 8000

将下面内容保存为 app.py

import os
import time
from typing import Literal

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="Internal LLM Gateway")
INTERNAL_TOKEN = os.getenv("INTERNAL_LLM_TOKEN", "dev-token")


class GenerateRequest(BaseModel):
    model: str = Field(pattern=r"^[a-z0-9-]+$")
    prompt: str = Field(min_length=1, max_length=12000)
    max_tokens: int = Field(default=256, ge=1, le=2048)
    priority: Literal["online", "batch"] = "online"


class GenerateResponse(BaseModel):
    model: str
    text: str
    latency_ms: int


def generate_text(model: str, prompt: str, max_tokens: int) -> str:
    # 这里替换为 vLLM、TensorRT-LLM 或公司内部推理客户端。
    return f"[{model}] response for: {prompt[:max_tokens]}"


@app.post("/v1/generate", response_model=GenerateResponse)
def generate(
    request: GenerateRequest,
    authorization: str | None = Header(default=None),
) -> GenerateResponse:
    expected = f"Bearer {INTERNAL_TOKEN}"
    if authorization != expected:
        raise HTTPException(status_code=401, detail="invalid token")

    started = time.perf_counter()
    text = generate_text(request.model, request.prompt, request.max_tokens)
    latency_ms = round((time.perf_counter() - started) * 1000)

    return GenerateResponse(
        model=request.model,
        text=text,
        latency_ms=latency_ms,
    )

调用示例:

curl -s http://127.0.0.1:8000/v1/generate \
  -H 'Authorization: Bearer dev-token' \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "text-summary-v1",
    "prompt": "Summarize this incident report in three bullet points.",
    "max_tokens": 128,
    "priority": "online"
  }'

这个示例刻意保留了几个平台边界:模型名由请求传入但受到格式约束,输出包含延迟字段,优先级也进入了 API 契约。生产实现中,可以基于 modelpriority 路由到不同的服务池,并把用户、团队、请求 ID 和 token 使用量写入指标系统。

在容器平台上管理推理服务

如果使用 Kubernetes,可以为在线模型单独建立 Deployment 和 Service。下面的 YAML 只是最小部署骨架,镜像入口和资源数值需要按实际模型调整:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: text-summary-v1
  labels:
    app: text-summary-v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: text-summary-v1
  template:
    metadata:
      labels:
        app: text-summary-v1
    spec:
      containers:
        - name: server
          image: registry.example.com/llm/text-summary:v1
          args: ["--port", "8080", "--max-batch-size", "8"]
          ports:
            - containerPort: 8080
          env:
            - name: MODEL_ID
              value: "text-summary-v1"
          resources:
            requests:
              cpu: "4"
              memory: "16Gi"
              nvidia.com/gpu: "1"
            limits:
              cpu: "8"
              memory: "32Gi"
              nvidia.com/gpu: "1"
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            periodSeconds: 5
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
---
apiVersion: v1
kind: Service
metadata:
  name: text-summary-v1
spec:
  selector:
    app: text-summary-v1
  ports:
    - name: http
      port: 80
      targetPort: 8080

可以这样部署并检查:

kubectl apply -f text-summary.yaml
kubectl rollout status deployment/text-summary-v1
kubectl get pods -l app=text-summary-v1 -o wide

真正进入生产后,健康检查不能只表示进程还活着。/health/ready 应该在模型加载完成、权重可用、依赖连接正常并且服务能够接收新请求时才返回成功。滚动升级期间,还需要给正在生成的请求留出排空时间,否则发布会制造大量截断响应。

可观测性决定平台能否运营

LLM Serving 的平均延迟很容易掩盖问题。至少应记录以下指标:

  • 请求数、错误率、超时率和按模型拆分的状态码。
  • 首 token 延迟、完整响应延迟和 token 吞吐量。
  • 输入 token、输出 token、上下文长度和拒绝原因。
  • GPU 利用率、显存占用、批大小以及队列等待时间。
  • 按团队、应用和模型版本统计的成本或资源消耗。

日志中不要直接写入完整提示词和模型输出。内部系统同样可能携带用户隐私、业务机密或未发布内容。更稳妥的做法是记录请求哈希、长度、分类结果和 trace ID;需要排查时,再通过受控的脱敏存储关联原始数据。

容量管理也要围绕队列来做。一个简单的排队模型可以帮助判断是否应该扩容:如果请求到达速度长期接近或超过服务处理速度,队列会不断增长,最终表现为延迟飙升和超时。扩容策略应结合队列等待时间、GPU 利用率和 SLO,而不是只看 CPU 使用率。

采用时的检查清单

内部 LLM Serving 适合分阶段推进:

  1. 先定义稳定的模型服务契约,明确输入、输出、错误码、超时和版本规则。
  2. 把在线与离线流量分池,配置独立的队列、配额和扩缩容策略。
  3. 在平台层统一鉴权、审计、敏感信息处理和成本统计。
  4. 用真实业务流量建立基准,分别测量短提示词、长上下文和高并发场景。
  5. 通过灰度路由验证新模型,不要让模型升级与业务代码发布强绑定。

内部部署的优势是控制数据边界、延迟和模型适配方式,但代价是需要承担 GPU 采购或租用、模型升级、容量规划、故障恢复和安全治理。决定是否自建时,不应只比较单次 API 价格,还要计算空闲资源、工程维护、峰值容量和合规要求。一个小而稳定的统一入口,通常比一开始建设覆盖所有模型和场景的“大平台”更容易验证价值。


相关推荐