
本文记录了一套生产级 LLM 推理服务的云原生落地过程,包含 GPU 调度、Triton 配置、Istio 金丝雀、KEDA 混合伸缩以及 5 个典型故障的排查日志。所有 YAML 和脚本均经过 vLLM 0.6.1 + K8s 1.29 验证。
helm repo add nvidia https://nvidia.github.io/gpu-operator
helm install gpu-operator nvidia/gpu-operator \
--set devicePlugin.enabled=true \
--set mig.strategy=single \
--set mig.deviceSplit=1g.10gb,2g.20gb检查节点 MIG 设备:
kubectl get node -o json | jq '.items[].status.capacity | with_entries(select(.key | contains("nvidia")))'DynamicResourceAllocation 特性门控)创建 ResourceClass 和 ResourceClaimTemplate:
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClass
name: gpu-mem
driverName: nvidia.com
---
apiVersion: resource.k8s.io/v1alpha2
kind: ResourceClaimTemplate
metadata:
name: gpu-40gb-template
spec:
spec:
resourceClassName: gpu-mem
parameters:
apiVersion: nvidia.com/v1
kind: GpuClaimParameters
memory: 40Gi在 Deployment 中引用:
spec:
template:
spec:
containers:
- name: triton
resources:
claims:
- name: gpu-resource
resourceClaims:
- name: gpu-resource
source:
resourceClaimTemplateName: gpu-40gb-template说明:此方式可避免因整卡数量不足导致调度失败,允许同节点多 Pod 共享单卡的不同 MIG 实例。
/model-repository/
├── llama/
│ ├── config.pbtxt
│ └── 1/
│ ├── model.py # vLLM Python backend 脚本
│ └── model.pt # 权重软链接(实际存于 PVC)config.pbtxt 核心参数(吞吐优化)name: "llama"
platform: "python"
max_batch_size: 128
dynamic_batching {
preferred_batch_size: [1, 2, 4, 8, 16, 32, 64, 128]
max_queue_delay_microseconds: 500 # 最大攒批等待 500μs
preserve_ordering: false # 允许乱序返回,提高吞吐
}
instance_group [
{
count: 2 # 启动 2 个 Python 实例,利用多线程
kind: KIND_GPU
gpus: [0] # 绑定到同一 GPU(配合 MIG)
}
]
parameters {
key: "vllm_max_num_seqs"
value: { string_value: "256" }
}
parameters {
key: "vllm_max_num_batched_tokens"
value: { string_value: "8192" }
}
parameters {
key: "vllm_gpu_memory_utilization"
value: { string_value: "0.85" }
}调优关键:
preferred_batch_size需与 vLLM 的max_num_seqs匹配,过大显存溢出,过小浪费。
# model.py
import asyncio
from vllm import AsyncLLMEngine, SamplingParams
class TritonPythonModel:
def initialize(self, args):
self.engine = AsyncLLMEngine.from_engine_args(
engine_args=["--model", "/model-repository/llama/1",
"--tensor-parallel-size", "1",
"--dtype", "float16",
"--max-num-seqs", "256"]
)
self.sampling_params = SamplingParams(temperature=0.7, max_tokens=128)
async def execute(self, requests):
coros = [self.engine.generate(req.input_text, self.sampling_params)
for req in requests]
outputs = await asyncio.gather(*coros)
return [output.outputs[0].text for output in outputs]istioctl install --set profile=default -y
kubectl label namespace inference istio-injection=enabledapiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata: name: triton-dr
spec:
host: triton-svc
subsets:
- name: v1
labels: { version: "v1" }
- name: v2
labels: { version: "v2" }
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata: name: triton-vs
spec:
hosts: ["triton-svc"]
http:
- match:
- headers: { model-version: { exact: "v2" } }
route: [{ destination: { host: triton-svc, subset: v2 }, weight: 100 }]
- route:
- destination: { host: triton-svc, subset: v1, weight: 90 }
- destination: { host: triton-svc, subset: v2, weight: 10 }apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata: name: triton-dr
spec:
trafficPolicy:
connectionPool:
tcp: { maxConnections: 200 }
loadBalancer: { simple: LEAST_REQUEST }
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50
retry:
attempts: 3
perTryTimeout: 2s
retryOn: "5xx,reset,connect-failure"注意:对 LLM 推理不宜过度重试,可能加剧延迟,设置
perTryTimeout应小于整体 SLA。
apiVersion: tekton.dev/v1beta1
kind: Pipeline
spec:
tasks:
- name: validate-model
taskSpec:
steps:
- image: nvcr.io/nvidia/tritonserver:24.05-py3
script: |
model-analyzer -m /workspace/model-repository --run-profiling
- name: build-oci
taskSpec:
steps:
- image: oras:1.1.0
script: |
oras push registry.internal/models/llama:v2.1 \
--manifest-config /dev/null:application/vnd.unknown.config.v1+json \
./model-repository/ --media-type application/vnd.model-repository.v1.tar
- name: trigger-rollout
taskSpec:
steps:
- image: bitnami/kubectl:latest
script: |
kubectl patch rollout -n inference triton-rollout \
-p '{"spec":{"template":{"metadata":{"annotations":{"model-version":"v2.1"}}}}}'apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: name: triton-rollout
spec:
replicas: 5
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 5m } # 人工观察
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
trafficRouting:
istio:
virtualService: { name: triton-vs, routes: [primary] }
selector: { matchLabels: { app: triton } }
template:
metadata: { labels: { app: triton } }
spec:
containers:
- name: triton
image: triton-server:latest
env:
- name: MODEL_REPO
value: "oci://registry.internal/models/llama:v2.1" # 由镜像拉取模型通过 ServiceMonitor 抓取 Triton 的 8002 端口:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata: name: triton-monitor
spec:
selector: { matchLabels: { app: triton } }
endpoints:
- port: metrics
path: /metrics
interval: 10s关键查询(用于 HPA):
avg(rate(nv_gpu_memory_used_bytes[1m])) / avg(nv_gpu_memory_total_bytes) * 100histogram_quantile(0.99, sum(rate(triton_request_queue_duration_ms_bucket[1m])) by (le))使用 Fluent Bit 的 filter 和 lua 脚本实现 1% 采样,且只记录延迟 > 500ms 的请求:
function filter(tag, timestamp, record)
if math.random() < 0.01 or record.latency_ms > 500 then
return 1, timestamp, record
end
return -1, timestamp, record
end配置 Fluent Bit:
filters:
- Name: lua
Match: triton.*
script: /etc/fluent-bit/sample.lua
call: filter注入 annotations 启用 Jaeger:
spec:
template:
metadata:
annotations:
sidecar.opentelemetry.io/inject: "true"
instrumentation.opentelemetry.io/inject-python: "true"在 vLLM backend 中添加 span:
from opentelemetry import trace
tracer = trace.get_tracer("vllm.backend")
with tracer.start_as_current_span("generate") as span:
span.set_attribute("input_length", len(prompt))
output = await engine.generate(...)
span.set_attribute("output_length", len(output))helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/kedaapiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: name: triton-scaler
spec:
scaleTargetRef:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
name: triton-rollout
minReplicaCount: 2 # 至少 2 个实例预热
maxReplicaCount: 12
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring:9090
metricName: gpu_util
threshold: '65'
query: |
avg(avg_over_time(nv_gpu_utilization{job="triton"}[2m])) by (pod)
- type: prometheus
metadata:
metricName: queue_latency_p99
threshold: '300' # 单位 ms
query: |
histogram_quantile(0.99, sum(rate(triton_request_queue_duration_ms_bucket[1m])) by (le))
advanced:
horizontalPodAutoscalerConfig:
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # 5 分钟稳定窗口
policies:
- type: Percent
value: 10
periodSeconds: 60注意:避免因单次突发扩容过多,结合
scaleUp策略限制步长。
activator 缓冲 + 预热 Pod为热门模型创建常驻 Pause Pod 提前挂载模型文件:
apiVersion: apps/v1
kind: Deployment
metadata: name: model-warmup
spec:
replicas: 2
template:
spec:
containers:
- name: warm
image: registry.k8s.io/pause:3.9
volumeMounts:
- name: model-pvc
mountPath: /models
volumes:
- name: model-pvc
persistentVolumeClaim: { claimName: llama-model-pvc }当 KEDA 扩容时,新 Pod 挂载同 PVC,模型文件已在本地(通过 CSI 挂载),加载时间从 120s 降至 15s。
OutOfMemoryError 因 max_num_seqs 过高RuntimeError: CUDA out of memory。max_num_seqs=256 配合 max_num_batched_tokens=8192,峰值显存占用超过 70GB(A100 80GB 实际可用约 78GB)。max_num_seqs=128,gpu_memory_utilization=0.75,并设置 --enforce-eager 关闭 CUDA Graph 以降低显存碎片(牺牲少量性能)。weight: 10 但 Istio 访问日志显示全部流量仍到 v1。subset 标签匹配,DestinationRule 未正确关联。template.metadata.labels 包含 version: v2,并重启 istio-proxy kubectl rollout restart deployment -n istio-system istiod。kubectl get hpa 显示 unable to get metric。metadata.trigger.authenticationRef,使用 kind: Secret 存储 Bearer Token,并设置 insecureSkipVerify: true(内网环境)。max_queue_delay_microseconds 设置为 5000μs(5ms),在低并发时请求被积压。preferred_batch_size 中的小尺寸(1,2,4),减少等待时间。ImagePullBackOff。使用 perf_analyzer 进行压测(输入 512,输出 128):
扩缩容响应时间:KEDA 检测到指标变化后 45s 开始扩容,每 30s 新增一个 Pod。
组件 | 配置项 | 推荐值 |
|---|---|---|
Triton | max_batch_size | 128 |
Triton | max_queue_delay_microseconds | 100 |
vLLM | --max-num-seqs | 128 |
vLLM | --gpu-memory-utilization | 0.75 |
Istio | maxConnections | 200 |
Istio | perTryTimeout | 2s |
KEDA | scaleDown.stabilizationWindowSeconds | 300 |
KEDA | GPU threshold | 65% |
KEDA | 队列延迟阈值 | 300ms |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。