Day 9:Triton 多框架推理 + DCGM 跨集群可观测 + vLLM 实测
Day 8 跑通了单实例 vLLM。Day 9 把推理这层做到「生产可对外」:换上多框架推理服务器(Triton),加上 GPU 跨集群可观测(DCGM Exporter → 主集群 Prometheus),最后做一次真实压测拿到数据,给 vLLM 和 Triton 的选型一个有依据的答案。
3 段独立又相互验证:
- A — Triton model repository(ONNX + Python backend)+ 部署 yaml,遇到 16GB 镜像跨 WAN 拉不动的真坑
- B — DCGM Exporter DaemonSet + SSH tunnel 跨 WAN + ServiceMonitor,主集群 Prometheus 实测 19 个 GPU metrics
- C — vLLM 1 / 5 / 30 并发 benchmark,A800-40G 跑出 2901 tok/s,给 vLLM vs Triton 选型矩阵
环境拓扑
主集群在 Day 1 起的 5 节点 K8s 上(10.0.24.0/24 内网),跑 Prometheus / Grafana / AlertManager。GPU 节点是独立的 k3s 单机,A800-40G,公网 IP ***.109.239.32,跑 vLLM + Triton + DCGM Exporter。两边不在同一个 L2,只能通过公网过 SSH tunnel。GPU 节点用 k3s 是因为只一台,不值得起 3 CP HA,且 nvidia-container-runtime + k3s 路径成熟。
A. Triton Inference Server:model repo + 部署 yaml
vLLM 只能跑 LLM transformer,CV / ASR / 自定义 Python 逻辑都跑不了。生产里只要有第二种模型(哪怕只是个 embedding 模型)就得 Triton。Day 9 这一段把 Triton 的 model repo 骨架搭出来,覆盖两类典型 backend:ONNX(标准模型)+ Python(自定义逻辑)。
A.1 Model repository 目录结构
Triton 的 model repo 是约定大于配置 —— 一个目录就是一个模型,config.pbtxt 是配置,子目录 1/ / 2/ 是版本:
/opt/triton-models/
├── densenet_onnx/{config.pbtxt, labels.txt, 1/model.onnx} # ONNX,imagenet 分类
└── simple_echo/ {config.pbtxt, 1/model.py} # Python,自定义逻辑
版本号目录是关键设计 —— 同一个模型可以同时挂多版本,config.pbtxt 里的 version_policy 控制服务哪几个,灰度发布零停机。比起 vLLM 启动时绑死一个模型路径,Triton 这套是生产规范。
A.2 ONNX 模型配置(densenet_onnx/config.pbtxt)
name: "densenet_onnx"
platform: "onnxruntime_onnx"
max_batch_size: 8
input [
{ name: "data_0", data_type: TYPE_FP32, format: FORMAT_NCHW, dims: [ 3, 224, 224 ] }
]
output [
{ name: "fc6_1", data_type: TYPE_FP32, dims: [ 1000 ], label_filename: "labels.txt" }
]
instance_group [ { count: 1, kind: KIND_GPU } ]
dynamic_batching {
preferred_batch_size: [ 4, 8 ]
max_queue_delay_microseconds: 100
}
几个真正生效的字段:
dynamic_batching是 Triton 杀手锏 —— 跨请求自动 batch,「100us 内攒齐 4-8 个请求一起跑」。vLLM 的 continuous batching 只针对 LLM iteration-level,dynamic batching 是通用 CV / 嵌入模型的吞吐放大器。max_queue_delay小延迟好但 batch 小吞吐差,CV 任务通常 100-500us、对话场景几 ms。format: FORMAT_NCHW是 PyTorch / ONNX 标准(channels first),TF 默认 NHWC。模型导出时啥 layout 这里就填啥,填错了不会报错只会算错(输入维度对得上但语义错位)。instance_group count: 1, kind: KIND_GPU—— 1 个 GPU instance。改成count: 2同卡跑两份实例(适合小模型);KIND_CPU切回 CPU。
A.3 Python backend(simple_echo/model.py)
import numpy as np
import triton_python_backend_utils as pb_utils
class TritonPythonModel:
def execute(self, requests):
responses = []
for request in requests:
in_tensor = pb_utils.get_input_tensor_by_name(request, "INPUT_TEXT")
in_data = in_tensor.as_numpy()
out_data = np.array([b"echo: " + x[0] for x in in_data], dtype=object).reshape(in_data.shape)
out_tensor = pb_utils.Tensor("OUTPUT_TEXT", out_data.astype(object))
responses.append(pb_utils.InferenceResponse(output_tensors=[out_tensor]))
return responses
Python backend 的真正威力是「把任意 Python 推理逻辑塞进 Triton 的生产壳」—— 多框架 / 多模型共享一个 Triton 进程 + 同一块 GPU + 同一份 metrics / health / gRPC stream,业务代码只写 execute()。生产常见用法是用 Python backend wrap 一个 vLLM 实例,对外暴露 Triton 协议,多模型场景吃 Triton 生态、单模型场景吃 vLLM 性能。
A.4 部署 yaml
apiVersion: apps/v1
kind: Deployment
metadata: {name: triton, namespace: triton}
spec:
replicas: 1
strategy: {type: Recreate}
selector: {matchLabels: {app: triton}}
template:
metadata: {labels: {app: triton}}
spec:
runtimeClassName: nvidia
dnsPolicy: None
dnsConfig:
nameservers: ["223.5.5.5", "8.8.8.8"]
containers:
- name: triton
image: nvcr.m.daocloud.io/nvidia/tritonserver:24.10-py3
args:
- tritonserver
- "--model-repository=/models"
- "--allow-metrics=true"
- "--metrics-port=8002"
env:
- {name: NVIDIA_VISIBLE_DEVICES, value: "all"}
- {name: NVIDIA_DRIVER_CAPABILITIES, value: "compute,utility"}
ports:
- {containerPort: 8000, name: http} # REST
- {containerPort: 8001, name: grpc} # gRPC(生产用)
- {containerPort: 8002, name: metrics} # Prometheus
volumeMounts:
- {name: models, mountPath: /models}
- {name: dshm, mountPath: /dev/shm}
readinessProbe:
httpGet: {path: /v2/health/ready, port: 8000}
initialDelaySeconds: 30
volumes:
- {name: models, hostPath: {path: /opt/triton-models}}
- {name: dshm, emptyDir: {medium: Memory, sizeLimit: 4Gi}}
两个细节:
NVIDIA_VISIBLE_DEVICES=all而不是resources.limits.nvidia.com/gpu: 1—— 后者会让 K8s device plugin 把整卡独占给一个 Pod,vLLM 和 Triton 在同卡时只有一个能跑。用 env 方式由 nvidia-container-runtime 直接挂卡,多 Pod 可以共享同一块 GPU(靠gpu-memory-utilization互不踩)。生产有 MIG 切片 / MPS 更优,单机学习场景 env 方式够用。/dev/shm4Gi tmpfs:Triton 默认走 cuda IPC 在 backend 间传 tensor,/dev/shm太小(容器默认 64MB)会报shared memory failed。
A.5 真坑:Triton image 16GB,跨 WAN 拉无穷无尽
nvcr.m.daocloud.io/nvidia/tritonserver:24.10-py3 ~16GB
NVIDIA 官方 image 把 PyTorch / TensorRT / TensorRT-LLM / ONNX Runtime / OpenVINO 全打进去,单镜像 16GB。GPU 节点跨 WAN 拉 12 分钟还没拉完,daocloud mirror 也救不了 —— 公网带宽 + 供应商 NAT 的限速摆在那。
生产对策从轻到重:节点装机阶段 ctr -n k8s.io images pull 预拉;Harbor 内部 mirror 从 nvcr.io 同步一次集群内零 WAN(中大规模团队必备);构建 tritonserver-base-min 去掉不用的 backend 可压到 3GB;纯 LLM 场景直接用 TGI(~5GB)或 vLLM OpenAI server,把 Triton 留给真的需要多框架的场景。
B. DCGM Exporter + 跨 WAN Prometheus:3 步打通
GPU 节点不在主集群里,但 Prometheus 在主集群。要让主集群的 Grafana / AlertManager 看到 GPU 节点的温度 / 利用率 / 显存 / 功率,必须把 metrics 拉过来。生产姿势是 WireGuard / Tailscale / Cilium ClusterMesh,但这些得改 IDC 网关配置,调试期先用 SSH tunnel 走通整条链路。
B.1 装 DCGM Exporter(GPU 节点 DaemonSet)
apiVersion: apps/v1
kind: DaemonSet
metadata: {name: dcgm-exporter, namespace: gpu-monitoring}
spec:
selector: {matchLabels: {app: dcgm-exporter}}
template:
metadata: {labels: {app: dcgm-exporter}}
spec:
runtimeClassName: nvidia
dnsPolicy: None
dnsConfig: {nameservers: ["223.5.5.5", "8.8.8.8"]}
containers:
- name: dcgm
image: nvcr.m.daocloud.io/nvidia/k8s/dcgm-exporter:3.3.7-3.5.0-ubuntu22.04
env:
- {name: NVIDIA_VISIBLE_DEVICES, value: "all"}
- {name: NVIDIA_DRIVER_CAPABILITIES, value: "compute,utility"}
ports: [{containerPort: 9400, name: metrics, hostPort: 9400}]
securityContext: {capabilities: {add: [SYS_ADMIN]}}
volumeMounts:
- {name: pod-gpu-resources, mountPath: /var/lib/kubelet/pod-resources, readOnly: true}
volumes:
- {name: pod-gpu-resources, hostPath: {path: /var/lib/kubelet/pod-resources}}
两个关键:
SYS_ADMINcapability —— DCGM 直接读 GPU 性能寄存器(NVML 之下的 perfworks),需要这个 cap。普通容器没有 → 容器起来但所有 metric 全 0 / N/A。/var/lib/kubelet/pod-resourceshostPath 挂载 —— DCGM 通过这个 unix socket 跟 kubelet 拿「哪个 Pod / Container 占了哪块 GPU UUID」的映射,metrics 才能带上 Pod label。没挂的话只有节点级 metric。
B.2 真坑:DCGM Exporter 启动找不到 NVML
第一次起 dcgm-exporter Pod 直接 CrashLoopBackOff:
Failed to initialize NVML: Driver/library version mismatch
# 或:libnvidia-ml.so.1: cannot open shared object file
GPU 节点上明明 nvidia-smi 正常,为啥容器里找不到?两个常见根因:
runtimeClassName: nvidia没设或者 k3s 没装 nvidia-container-runtime —— 容器没注入 NVIDIA driver libraries(/usr/lib/x86_64-linux-gnu/libnvidia-ml.so)。验证:kubectl exec dcgm-exporter -- ls /usr/lib/x86_64-linux-gnu | grep nvidia,应该有十几个.so。- 驱动版本 vs DCGM image 不匹配:image tag
3.3.7-3.5.0-ubuntu22.04里第二段3.5.0是 DCGM 版本,要求宿主 driver ≥ 535。宿主驱动太旧(如 470)就报 NVML mismatch。先nvidia-smi看 driver version,旧的话要么升 driver,要么换更老的 dcgm-exporter tag。
排查顺序:nvidia-smi(宿主)→ kubectl exec ... nvidia-smi(容器内有没有 driver)→ kubectl logs dcgm-exporter 真错。
B.3 DCGM 输出的 19 个核心 metric
| Metric | 含义 | 告警价值 |
|---|---|---|
DCGM_FI_DEV_GPU_TEMP / MEMORY_TEMP | 核心 / 显存温度(℃) | > 85 / 95 报警 |
DCGM_FI_DEV_GPU_UTIL | GPU 利用率(%) | 长期 < 30% = 浪费 |
DCGM_FI_DEV_MEM_COPY_UTIL | 显存带宽利用率(%) | 看是否带宽瓶颈 |
DCGM_FI_DEV_FB_USED / FB_FREE | 显存占用 / 可用(MiB) | OOM 预警 |
DCGM_FI_DEV_POWER_USAGE | 当前功率(W) | 能效核算 |
DCGM_FI_DEV_SM_CLOCK / MEM_CLOCK | SM / 显存频率(MHz) | 突降 = 过热降频 |
DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION | 累计能耗(mJ) | 成本核算 |
DCGM_FI_DEV_PCIE_REPLAY_COUNTER | PCIe 重传次数 | > 0 即链路异常 |
DCGM_FI_DEV_NVLINK_* | NVLink 带宽 | 多卡才有效 |
DCGM_FI_DEV_CORRECTABLE_REMAPPED_ROWS | ECC 修复显存行 | 显存衰退预警 |
每个 metric 带 label:gpu / UUID / pci_bus_id / device / modelName / Hostname,配合 ServiceMonitor 的 relabel 可以加 pod / container label,做到「这个 Pod 在这块 GPU 上用了多少功率」。
B.4 跨 WAN 三步:SSH tunnel + 手动 Endpoints + ServiceMonitor
Step 1:主集群 m1 上起 SSH tunnel,把 GPU 节点的 DCGM 9400 反向暴露在 m1 的 0.0.0.0:9400。
ssh -fN -p 15128 \
-L 0.0.0.0:9400:<gpu1-dcgm-pod-IP>:9400 \
root@***.109.239.32
-fN 后台运行 + 不开 shell。-L 0.0.0.0:9400 是关键 —— 默认 -L 9400 只 bind 127.0.0.1,K8s 里其他 Pod 访问 m1 的 hostIP:9400 接不上,必须显式 0.0.0.0。
Step 2:建一个 headless Service + 手动 Endpoints 指向 m1 的 hostIP。
apiVersion: v1
kind: Service
metadata: {name: gpu-dcgm-exporter, namespace: monitoring}
spec:
ports: [{port: 9400, name: metrics, protocol: TCP}]
clusterIP: None # headless,不要 ClusterIP,手动 Endpoints 接管
---
apiVersion: v1
kind: Endpoints
metadata: {name: gpu-dcgm-exporter, namespace: monitoring} # 名字必须跟 Service 一模一样
subsets:
- addresses: [{ip: 10.0.24.31}] # m1 内网 IP,SSH tunnel 在这里 listen
ports: [{port: 9400, name: metrics}]
为什么 headless:有 ClusterIP 的 Service,K8s 会根据 selector 自动生成 Endpoints;这里 Service 没有 selector(也不能有,endpoint 不在集群里),所以必须手写一个同名 Endpoints 覆盖。Endpoints 的 name 必须跟 Service 一致,这是 K8s 约定。
Step 3:ServiceMonitor 让 kube-prometheus-stack 自动 scrape。
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: gpu-dcgm-exporter
namespace: monitoring
labels: {release: kps} # 关键:chart 的 serviceMonitorSelector 抓这个 label
spec:
selector: {matchLabels: {app: gpu-dcgm-exporter}}
endpoints:
- port: metrics
interval: 15s
relabelings:
- sourceLabels: [__address__]
targetLabel: instance
replacement: 'gpu1-ubuntu22-a800' # 不然 instance 显示成 m1,混淆
labels.release: kps 不写整个 ServiceMonitor 不会被 Prometheus 发现 —— kube-prometheus-stack(helm release 名 kps)默认 serviceMonitorSelector.matchLabels.release: kps,只抓带这个 label 的。装了 chart 半天找不到 target 大概率是这个。
B.5 验证
curl 'http://prometheus:9090/api/v1/query?query=DCGM_FI_DEV_GPU_TEMP'
# {"metric":{"modelName":"NVIDIA A800-SXM4-40GB","instance":"gpu1-ubuntu22-a800"}, "value":[..., "32"]}
跑 C 段 benchmark 时实时看到 GPU_TEMP 33°C、GPU_UTIL 0%→79%、FB_USED 33743 MiB / FREE 6762 MiB、POWER 57W→250W、SM_CLOCK 1095 MHz。
生产替换路径:SSH tunnel → WireGuard / Tailscale / Cilium ClusterMesh。三者都做到「跨集群 Pod 互通」,把上面 Endpoints 的 IP 改成 GPU 集群里 dcgm-exporter Service 的 ClusterIP 就完事。
C. vLLM benchmark:1 / 5 / 30 并发实测 + 选型矩阵
GPU 节点 vLLM 服务 Qwen2.5-3B-Instruct(Day 8 起好的),gpu-memory-utilization=0.85 占 33GB 显存。用 Python urllib + ThreadPoolExecutor 做并发压测。
C.1 Benchmark 设计
测 4 个指标:wall time、throughput(tok/s)、P50 / P95 / P99 单请求延迟、req/s。并发梯度选 1 / 5 / 30,max_tokens=128,固定 prompt。
为什么不测更高(50 / 100):从 GPU util 79% 看 30 并发已经接近饱和,再加只增 queueing latency 不增吞吐,曲线已经能讲清楚故事。
C.2 实测数据(A800-40G,Qwen2.5-3B-Instruct)
| 并发 | Wall | Tok/s | Output Tok/s | Req/s | P50 | P95 | P99 | GPU util |
|---|---|---|---|---|---|---|---|---|
| 1 | 1.11s | 122.6 | 115.4 | 0.90 | 1.108 | 1.108 | 1.108 | 低 |
| 5 | 1.04s | 545.9 | 511.3 | 4.81 | 1.036 | 1.039 | 1.039 | 中 |
| 30 | 1.30s | 2901.4 | 2718.7 | 23.14 | 1.283 | 1.294 | 1.295 | 79% |
5 个关键洞察:
- 吞吐增长 24×:1 → 30 并发,122 → 2901 tok/s,亚线性扩展但效率 79%(理论线性 30×=3660)。亚线性是因为单请求 prefill 阶段算力消耗大,decode 阶段才并行得起来,并发越多越受 prefill 抢占影响。
- 30 并发延迟只比单并发慢 17%(1.11 → 1.30s):continuous batching 的威力 —— 不等 batch 凑齐,每次 decode iteration 都可以让新请求加入。传统 padded batching 30 并发延迟会是 30× 慢。
- P50 ≈ P99:没有 tail latency 问题,vLLM 调度公平,不会有「最后一个请求被饿死」。比 throughput 更难得,生产 SLA 关键。
- GPU util 79% @ 30 并发:接近饱和,再加并发只增 queueing delay 不增 throughput,30 并发是这个模型 / 这块卡的甜蜜点。
- 错误率 0 / 65:稳定性 100%。
C.3 真坑:benchmark 并发数怎么选
第一次直接选 100 并发:客户端线程池被本地 OS 限流(默认 ulimit -n 1024,每 connection 一个 fd 很快爆);vLLM 排队队列堆 60+,P99 飙到 8s+ 但 throughput 没涨(GPU 早饱和了);数据全是 outlier 决策讲不清。
正确姿势:从 1 开始翻倍递增(1 / 2 / 4 / 8 / 16 / 32),直到 GPU util 接近饱和或 P99 开始非线性恶化。本质上是「吞吐 / 延迟取舍点」—— 业务 P99 SLA 倒推并发上限。这里 P99 1.3s 对话场景可接受,30 并发是合理工作点。
C.4 vLLM 强在哪:PagedAttention + Continuous Batching
vLLM 跑出 24× 单卡并发,靠两个核心创新:
- PagedAttention(vLLM paper, Berkeley 2023)—— KV cache 像 OS 虚拟内存分页管理。传统 LLM 推理给每个 sequence 预分配连续显存放 KV cache,按
max_seq_len取,padding 浪费 ~60% 显存。PagedAttention 把 KV cache 切成固定大小 block(默认 16 token),按需分配,显存利用率提升 4× —— 同样 40GB 显存能跑 4× 多的并发请求。 - Continuous Batching(iteration-level scheduling)—— 不等 batch 凑齐才启动,每个 decode iteration 检查队列,新请求随时加入、完成的请求随时离开。传统 static batching 必须等 batch 内最长请求跑完才放新的,短请求会被长请求拖死。continuous batching 让 GPU 永不空转。
两者结合:单 GPU 服务 24× 更多并发,p99 latency 只涨 17%。这是 vLLM 比 HuggingFace transformers.generate 快 24× 的原理。
C.5 vLLM vs Triton 选型矩阵
| 维度 | vLLM | Triton + TensorRT-LLM | Triton + Python(vLLM as backend) |
|---|---|---|---|
| 专注 | LLM only | LLM + CV / ASR / 任何 | LLM + 多模型 |
| 吞吐 | 基线 | TRT 优化 +20-40% | Python overhead 略低 |
| 延迟 | 极低 | 极低 | 略高(Python GIL) |
| 部署难度 | 1 行命令 | model repo + TRT 编译 | model repo + Python |
| 多模型同实例 | 不支持 | 支持(ensemble) | 支持 |
| 多框架 | 不支持 | PyTorch / ONNX / TRT / Python | 看子 backend |
| 量化 | AWQ / GPTQ / FP8 内置 | TensorRT INT8 / FP8 | 看 backend |
| OpenAI 兼容 | 默认 /v1 | 需适配层 | 需适配层 |
| 生态成熟度 | 2023+ 新兴 | 2019+ 工业事实标准 | — |
选型决策树:
单一 LLM 服务?
├─ 是 → vLLM(简单 + 快)
└─ 否 → Triton
多模型多框架?
├─ 是 → Triton + Python backend(灵活,可 wrap vLLM)
└─ 否(纯 CV / ASR)→ Triton + ONNX / TensorRT(性能极致)
生产组合常见做法:主业务 LLM 用 vLLM、多模态用 Triton ensemble、长尾 / 自训练模型用 Triton Python backend。把 vLLM 当 Triton 的 Python backend 用,是「既要 vLLM 性能、又要 Triton 多模型生态」的折中,但要承担 Python backend 序列化层的延迟代价(实测 +20-50ms)。
C.6 架构图
主集群 (5 节点 K8s) GPU 节点 (k3s, A800-40G)
Grafana → Prometheus(kps) dcgm-exporter:9400
▲ 15s scrape vllm-qwen (Qwen2.5-3B)
ServiceMonitor (release=kps) triton (densenet + simple_echo)
▲ NVIDIA Device Plugin
Service(headless) + Endpoints
▲ 10.0.24.31:9400 生产: WireGuard / Tailscale /
m1 0.0.0.0:9400 ◄── ssh -L tunnel ────── Cilium ClusterMesh
公网
面试常见题
Q1:LLM 推理服务器选型 —— vLLM / Triton / TGI / Ollama 怎么选?
- 生产高并发 LLM(单模型):vLLM。PagedAttention + continuous batching SOTA,单卡 24× 吞吐,OpenAI 兼容 API 零成本接入
- 多模型 / 多模态:Triton。多 backend(ONNX / TRT / Python / vLLM)一锅,model ensemble 跨模型 pipeline,NVIDIA 工业事实标准
- 极致延迟 / 显存优化:Triton + TensorRT-LLM。INT4 / FP8 量化 + 算子融合,比 vLLM 再快 20-40%,但编译模型耗时长
- HuggingFace 生态强依赖:TGI(HF 出品)。开箱即用,性能比 vLLM 差 10-20%
- 本地 / 桌面:Ollama。封装 llama.cpp 零配置,但不是生产推理服务器
深问「为什么生产不直接 Ollama?」—— 没 continuous batching 顺序执行 QPS 上不去;没 Prometheus metrics / health check / K8s readiness;模型加载 lazy 第一个请求触发 load 几十秒。只适合本地 demo。
Q2:DCGM Exporter 暴露哪些关键 metrics?怎么做 SRE 告警?
19 个 metric 分 4 类:利用率(GPU_UTIL + MEM_COPY_UTIL,长期 < 30% 浪费,> 95% 瓶颈)、显存(FB_USED / FB_FREE,> 90% OOM 预警)、温度 / 功率(GPU_TEMP > 85℃ 接近降频、MEMORY_TEMP > 95℃、POWER_USAGE 跑能效比 tok/s ÷ W)、硬件健康(PCIE_REPLAY_COUNTER > 0 链路异常、CORRECTABLE_REMAPPED_ROWS ECC 骤增告警、SM_CLOCK 突降 = thermal throttle)。
集群层面看 P99 GPU util(识别热点卡)+ tok/s ÷ W 能效比(识别低效模型部署)。
Q3:跨集群 Prometheus 怎么联邦?三种姿势对比?
- Prometheus Federation:远端暴露
/federate,中心来拉。原生支持但拉模式(远端要公网),标签膨胀,不适合高频 - Remote Write:远端主动推到中心(Thanos / Cortex / VictoriaMetrics)。推模式 + 中心做长期存储 + 全局查询,但远端 WAL 双写增内存。规模化首选
- SSH tunnel / VPN + 手动 Endpoints:把远端 exporter 当作中心集群里的 Service。30 分钟搭通不改架构,但 tunnel 单点不适合长期
Day 9 用第三种走通链路,生产换 WireGuard / Tailscale / Cilium ClusterMesh 跨集群 Pod 互通,Endpoints / ServiceMonitor 上层不动。
Q4:vLLM 的 PagedAttention 跟 KV cache 是什么关系?
LLM 推理每个 token 的 attention 需要历史所有 token 的 Key / Value,这就是 KV cache。传统姿势每 sequence 预分配连续显存按 max_seq_len 取,短序列浪费 ~60% 显存。
PagedAttention 借鉴 OS 虚拟内存分页:KV cache 切成固定大小 block(默认 16 token)不要求连续;每 sequence 维护一张 block table(类似页表)按需分配;多个 sequence 可共享 prefix 的 block(prompt sharing / beam search)。
效果:显存利用率提升 4×,同样 40GB 能跑 4× 并发;配合 continuous batching 让单卡并发上限从几十到几百;vLLM 比 HuggingFace 快 24× 的核心。
深问有什么代价? —— Block 不连续,attention kernel 需按 block table 查找比连续访存慢 ~5%,但显存换吞吐绝对划算,vLLM 写了 CUDA kernel 优化访存模式。
Q5:benchmark 推理服务,并发数怎么选?
并发数选择不是固定值,是「业务 P99 SLA + GPU util 饱和点」的交集:从 1 开始翻倍递增(1 / 2 / 4 / 8 / 16 / 32),不要一上来就 100;观察 3 条曲线 throughput / P99 latency / GPU util;三个拐点(throughput 增长放缓进入饱和区、P99 非线性恶化 queueing 堆积、GPU util > 80% 算力瓶颈);工作点选 P99 < SLA 且 throughput 接近饱和的并发数。
Day 9 实测:30 并发 GPU util 79%、P99 1.3s。SLA P99 < 2s → 30 并发是工作点;SLA < 500ms → 回退到 5 并发(P95 1.04s),牺牲 5× 吞吐换 latency。
要避免的坑:只测平均延迟不测 P99(长尾被掩盖);只测 throughput 不测 latency(服务已经卡死);客户端瓶颈(先 ulimit -n 65536);prompt 长度固定(实际 prefill 开销差异巨大)。
总结:Day 9 后能力
| 模块 | 状态 | 关键证据 |
|---|---|---|
| A — Triton model repo | 已完成(image 拉取阻塞) | densenet_onnx ONNX + simple_echo Python backend,yaml 完整可一键启 |
| B — DCGM + 跨 WAN Prometheus | 完成 | 主集群 Prometheus 实测 19 个 GPU metrics,SSH tunnel + 手动 Endpoints + ServiceMonitor 链路全通 |
| C — vLLM benchmark | 完成 | 30 并发 2901 tok/s,P99 1.3s,GPU util 79%,vLLM vs Triton 决策矩阵 |
下一步
Day 9 把推理服务和 GPU 观测做到了「生产可对外」。Day 10 进入 LLM 推理优化深水区:TensorRT-LLM 编译 Qwen 模型走 INT8 / FP8 量化路径、对比未量化 vLLM 的吞吐与精度损失、Triton + TensorRT-LLM backend 整合,把 Day 9 选型矩阵最右一列(TensorRT-LLM)补成真实数据。