针对部署在Kubernetes集群中的LLM推理服务,由于LLM推理流量和工作负载的特殊性,经典的负载均衡方法效果不佳,且无法从日志和监控指标中获取LLM推理相关信息。本文将介绍如何在服务网格ASM中为vLLM推理服务声明推理服务池和路由定义,以提升负载均衡性能,并实现推理流量的路由和可观测性。
阅读前提示
阅读本文前,您需要了解:
如何在ACK集群中创建和使用GPU节点池或使用ACS GPU算力。
通过阅读本文,您可以了解到:
大语言模型和vLLM的背景信息。
使用传统方式管理集群内的LLM推理服务会出现的问题。
使用ASM管理集群内的LLM推理服务的原理及实践操作。
背景信息
大语言模型LLM
大语言模型LLM(Large Language Model)指参数数量达到亿级别的神经网络语言模型,例如GPT、通义千问和Llama。模型在超大规模的预训练数据(预训练数据类型多样且覆盖广泛,包括大量网络文本、专业书籍和代码等内容)上进行训练得到,通常用于文本生成式任务,如补全、对话任务等。
在构建基于LLM的应用时,您可以通过两种方式获取LLM提供的文本生成能力:
可以使用类似OpenAI、百炼或Moonshot等平台提供的外部LLM API服务。
也可以基于开源或自研大模型、使用vLLM等推理服务框架构建LLM推理服务,并将LLM推理服务部署在Kubernetes集群中。这种方式适用于希望自行控制LLM推理服务或对LLM的推理能力有较高定制化需求的场景。
vLLM
vLLM是一个高效易用流行的构建LLM推理服务的框架,支持包括通义千问在内的多种常见大语言模型。vLLM通过PagedAttention优化、动态批量推理(Continuous Batching)模型量化等优化技术,可以取得较好的大语言模型推理效率。
负载均衡与可观测
ASM支持管理集群内LLM推理服务流量,当在集群内部署的服务为LLM推理服务时,可以通过InferencePool和InferenceModel CRD,声明提供推理服务的工作负载集合以及服务所提供的模型名称等信息。通过这种方式,ASM可以为LLM推理服务提供针对LLM推理后端的负载均衡、流量路由和可观测能力。
目前仅支持基于vLLM实现的LLM推理服务。
传统负载均衡对于传统的HTTP请求,经典负载均衡算法可以将请求均匀地发送给不同的工作负载。然而,对于LLM推理服务来说,每个请求给后端带来的负载是难以预测的。 在推理过程中,请求处理可分为预填充和解码两个阶段:
| LLM负载均衡ASM提供了基于LLM后端的负载均衡算法。通过推理服务器多个维度的指标来评估推理服务器内部状态,并根据内部状态对多个推理服务器工作负载进行负载均衡。主要包括:
相对于经典的负载均衡算法,此方式可以更好地保证多个推理服务工作负载的GPU负载一致性,显著降低LLM推理请求第一个token的响应时延(ttft)、并提升LLM推理请求的吞吐量。 |
传统可观测LLM推理服务,目前普遍采用OpenAI的请求API格式进行交互。即请求元数据大多位于请求体中,如请求的模型名称、请求最大token数等。而经典的请求路由能力和可观测能力都是基于请求头和请求路径等元数据,不涉及请求体的解析。因此无法满足根据请求模型名称进行流量分配,或观测请求中的模型名称以及请求产生token数等需求。 | 推理流量可观测对于LLM推理请求,ASM在访问日志和监控指标上都提供了针对LLM推理的增强能力。
|
前提条件
场景实践
以下以在集群中部署基于vLLM的Llama2大模型为例,展示ASM管理集群内LLM推理服务流量的实践步骤。
步骤一:部署示例推理服务
使用以下内容,创建vllm-service.yaml。
本文使用的镜像需要GPU显存大于16GiB,T4卡型(16GiB显存)的实际可用显存不足以启动此应用。因此ACK集群卡型推荐使用A10,ACS集群卡型推荐使用8代GPU B。具体对应型号请提交工单咨询。
同时,由于LLM镜像体积较大,建议您提前转存到ACR,使用内网地址进行拉取。直接从公网拉取的速度取决于集群EIP的带宽配置,会有较长的等待时间。
ACS集群ACK集群apiVersion: v1 kind: Service metadata: name: vllm-llama2-7b-pool spec: selector: app: vllm-llama2-7b-pool ports: - protocol: TCP port: 8000 targetPort: 8000 type: ClusterIP --- apiVersion: v1 kind: ConfigMap metadata: name: chat-template data: llama-2-chat.jinja: | {% if messages[0]['role'] == 'system' %} {% set system_message = '<<SYS>>\n' + messages[0]['content'] | trim + '\n<</SYS>>\n\n' %} {% set messages = messages[1:] %} {% else %} {% set system_message = '' %} {% endif %} {% for message in messages %} {% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %} {{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }} {% endif %} {% if loop.index0 == 0 %} {% set content = system_message + message['content'] %} {% else %} {% set content = message['content'] %} {% endif %} {% if message['role'] == 'user' %} {{ bos_token + '[INST] ' + content | trim + ' [/INST]' }} {% elif message['role'] == 'assistant' %} {{ ' ' + content | trim + ' ' + eos_token }} {% endif %} {% endfor %} --- apiVersion: apps/v1 kind: Deployment metadata: name: vllm-llama2-7b-pool namespace: default spec: replicas: 3 selector: matchLabels: app: vllm-llama2-7b-pool template: metadata: annotations: prometheus.io/path: /metrics prometheus.io/port: '8000' prometheus.io/scrape: 'true' labels: app: vllm-llama2-7b-pool alibabacloud.com/compute-class: gpu # 指定使用GPU算力 alibabacloud.com/compute-qos: default alibabacloud.com/gpu-model-series: "example-model" # 指定GPU型号为example-model,请按实际情况填写 spec: containers: - name: lora image: "registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/llama2-with-lora:v0.2" imagePullPolicy: IfNotPresent command: ["python3", "-m", "vllm.entrypoints.openai.api_server"] args: - "--model" - "/model/llama2" - "--tensor-parallel-size" - "1" - "--port" - "8000" - '--gpu_memory_utilization' - '0.8' - "--enable-lora" - "--max-loras" - "4" - "--max-cpu-loras" - "12" - "--lora-modules" - 'sql-lora=/adapters/yard1/llama-2-7b-sql-lora-test_0' - 'sql-lora-1=/adapters/yard1/llama-2-7b-sql-lora-test_1' - 'sql-lora-2=/adapters/yard1/llama-2-7b-sql-lora-test_2' - 'sql-lora-3=/adapters/yard1/llama-2-7b-sql-lora-test_3' - 'sql-lora-4=/adapters/yard1/llama-2-7b-sql-lora-test_4' - 'tweet-summary=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_0' - 'tweet-summary-1=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_1' - 'tweet-summary-2=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_2' - 'tweet-summary-3=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_3' - 'tweet-summary-4=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_4' - '--chat-template' - '/etc/vllm/llama-2-chat.jinja' env: - name: PORT value: "8000" ports: - containerPort: 8000 name: http protocol: TCP livenessProbe: failureThreshold: 2400 httpGet: path: /health port: http scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 readinessProbe: failureThreshold: 6000 httpGet: path: /health port: http scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 resources: limits: cpu: 16 memory: 64Gi nvidia.com/gpu: 1 requests: cpu: 8 memory: 30Gi nvidia.com/gpu: 1 volumeMounts: - mountPath: /data name: data - mountPath: /dev/shm name: shm - mountPath: /etc/vllm name: chat-template restartPolicy: Always schedulerName: default-scheduler terminationGracePeriodSeconds: 30 volumes: - name: data emptyDir: {} - name: shm emptyDir: medium: Memory - name: chat-template configMap: name: chat-template
apiVersion: v1 kind: Service metadata: name: vllm-llama2-7b-pool spec: selector: app: vllm-llama2-7b-pool ports: - protocol: TCP port: 8000 targetPort: 8000 type: ClusterIP --- apiVersion: v1 kind: ConfigMap metadata: name: chat-template data: llama-2-chat.jinja: | {% if messages[0]['role'] == 'system' %} {% set system_message = '<<SYS>>\n' + messages[0]['content'] | trim + '\n<</SYS>>\n\n' %} {% set messages = messages[1:] %} {% else %} {% set system_message = '' %} {% endif %} {% for message in messages %} {% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %} {{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }} {% endif %} {% if loop.index0 == 0 %} {% set content = system_message + message['content'] %} {% else %} {% set content = message['content'] %} {% endif %} {% if message['role'] == 'user' %} {{ bos_token + '[INST] ' + content | trim + ' [/INST]' }} {% elif message['role'] == 'assistant' %} {{ ' ' + content | trim + ' ' + eos_token }} {% endif %} {% endfor %} --- apiVersion: apps/v1 kind: Deployment metadata: name: vllm-llama2-7b-pool namespace: default spec: replicas: 3 selector: matchLabels: app: vllm-llama2-7b-pool template: metadata: annotations: prometheus.io/path: /metrics prometheus.io/port: '8000' prometheus.io/scrape: 'true' labels: app: vllm-llama2-7b-pool spec: containers: - name: lora image: "registry-cn-hangzhou.ack.aliyuncs.com/ack-demo/llama2-with-lora:v0.2" imagePullPolicy: IfNotPresent command: ["python3", "-m", "vllm.entrypoints.openai.api_server"] args: - "--model" - "/model/llama2" - "--tensor-parallel-size" - "1" - "--port" - "8000" - '--gpu_memory_utilization' - '0.8' - "--enable-lora" - "--max-loras" - "4" - "--max-cpu-loras" - "12" - "--lora-modules" - 'sql-lora=/adapters/yard1/llama-2-7b-sql-lora-test_0' - 'sql-lora-1=/adapters/yard1/llama-2-7b-sql-lora-test_1' - 'sql-lora-2=/adapters/yard1/llama-2-7b-sql-lora-test_2' - 'sql-lora-3=/adapters/yard1/llama-2-7b-sql-lora-test_3' - 'sql-lora-4=/adapters/yard1/llama-2-7b-sql-lora-test_4' - 'tweet-summary=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_0' - 'tweet-summary-1=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_1' - 'tweet-summary-2=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_2' - 'tweet-summary-3=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_3' - 'tweet-summary-4=/adapters/vineetsharma/qlora-adapter-Llama-2-7b-hf-TweetSumm_4' - '--chat-template' - '/etc/vllm/llama-2-chat.jinja' env: - name: PORT value: "8000" ports: - containerPort: 8000 name: http protocol: TCP livenessProbe: failureThreshold: 2400 httpGet: path: /health port: http scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 readinessProbe: failureThreshold: 6000 httpGet: path: /health port: http scheme: HTTP initialDelaySeconds: 5 periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 resources: limits: nvidia.com/gpu: 1 requests: nvidia.com/gpu: 1 volumeMounts: - mountPath: /data name: data - mountPath: /dev/shm name: shm - mountPath: /etc/vllm name: chat-template restartPolicy: Always schedulerName: default-scheduler terminationGracePeriodSeconds: 30 volumes: - name: data emptyDir: {} - name: shm emptyDir: medium: Memory - name: chat-template configMap: name: chat-template
使用数据面集群的kubeconfig,部署LLM推理服务。
kubectl apply -f vllm-service.yaml
步骤二:配置ASM网关规则
部署生效于ASM网关的网关规则,该规则表示开启网关8080端口的监听。
使用以下内容,创建gateway.yaml。
apiVersion: networking.istio.io/v1 kind: Gateway metadata: name: llm-inference-gateway namespace: default spec: selector: istio: ingressgateway servers: - hosts: - '*' port: name: http-service number: 8080 protocol: HTTP
创建网关规则。
kubectl apply -f gateway.yaml
步骤三:配置LLM推理服务路由和负载均衡
若您想体验传统的负载均衡与LLM负载均衡的性能对比,推荐先完成(可选)通过可观测大盘与普通负载均衡方式进行性能对比中的步骤,再继续执行后续操作。
使用ASM的kubeconfig,启用LLM推理服务路由能力。
kubectl patch asmmeshconfig default --type=merge --patch='{"spec":{"gatewayAPIInferenceExtension":{"enabled":true}}}'
部署InferencePool资源。
InferencePool资源通过标签选择器声明一组在集群中运行的LLM推理服务工作负载,ASM会根据您创建的InferencePool来对LLM推理服务开启vLLM负载均衡。
使用以下内容,创建inferencepool.yaml。
apiVersion: inference.networking.x-k8s.io/v1alpha1 kind: InferencePool metadata: name: vllm-llama2-7b-pool spec: targetPortNumber: 8000 selector: app: vllm-llama2-7b-pool
部分配置项说明如下:
配置项
说明
配置项
说明
.spec.targetPortNumber
提供推理服务的Pod所暴露的端口。
.spec.selector
提供推理服务的Pod标签。标签key必须为app且值需为对应的Service名称。
使用数据面集群的kubeconfig,创建InferencePool资源。
kubectl apply -f inferencepool.yaml
部署InferenceModel资源。
InferenceModel指定了InferencePool中具体模型的流量分发策略。
使用以下内容,创建inferencemodel.yaml。
apiVersion: inference.networking.x-k8s.io/v1alpha1 kind: InferenceModel metadata: name: inferencemodel-sample spec: modelName: tweet-summary poolRef: group: inference.networking.x-k8s.io kind: InferencePool name: vllm-llama2-7b-pool targetModels: - name: tweet-summary weight: 100
部分配置项说明如下:
配置项
说明
配置项
说明
.spec.modelName
用于匹配请求中的model参数。
.spec.targetModels
配置流量路由规则。上述示例中是将请求头中包含model:tweet-summary的流量100%发送到运行了tweet-summary模型对应的Pod。
创建InferenceModel资源。
kubectl apply -f inferencemodel.yaml
创建LLMRoute资源。
为网关配置路由规则。此路由规则通过引用InferencePool资源的方式,指定ASM网关将8080端口上接收到的请求全部转发给示例LLM推理服务。
使用以下内容,创建llmroute.yaml。
apiVersion: istio.alibabacloud.com/v1 kind: LLMRoute metadata: name: test-llm-route spec: gateways: - llm-inference-gateway host: test.com rules: - backendRefs: - backendRef: group: inference.networking.x-k8s.io kind: InferencePool name: vllm-llama2-7b-pool
创建LLMRoute资源。
kubectl apply -f llmroute.yaml
步骤四:验证执行结果
多次执行以下命令,发起测试。
curl -H "host: test.com" ${ASM网关IP}:8080/v1/completions -H 'Content-Type: application/json' -d '{
"model": "tweet-summary",
"prompt": "Write as if you were a critic: San Francisco",
"max_tokens": 100,
"temperature": 0
}' -v
预期输出:
{"id":"cmpl-2fc9a351-d866-422b-b561-874a30843a6b","object":"text_completion","created":1736933141,"model":"tweet-summary","choices":[{"index":0,"text":", I'm a newbie to this forum. Write a summary of the article.\nWrite a summary of the article.\nWrite a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary of the article. Write a summary","logprobs":null,"finish_reason":"length","stop_reason":null,"prompt_logprobs":null}],"usage":{"prompt_tokens":2,"total_tokens":102,"completion_tokens":100,"prompt_tokens_details":null}}
(可选)步骤五:配置LLM服务可观测指标与可观测大盘
在使用InferencePool和InferenceMode资源声明集群中的LLM推理服务,并配置路由策略后,可以通过在日志和监控指标两方面查看LLM推理的可观测能力。
开启ASM的LLM流量可观测能力,采集服务网格监控指标。
通过操作新增日志字段、新增指标以及新增指标维度增强LLM推理请求的可观测性信息。
完成配置后,ASM监控指标中会新增一个
model
维度。您可以通过将监控指标采集到可观测监控Prometheus版,或者集成自建Prometheus实现网格监控进行指标采集。ASM新增提供两个指标来表示所有请求的输入token数
asm_llm_proxy_prompt_tokens
和输出token数asm_llm_proxy_completion_tokens
。您可以为Prometheus增加以下规则来新增这些指标。scrape_configs: - job_name: asm-envoy-stats-llm scrape_interval: 30s scrape_timeout: 30s metrics_path: /stats/prometheus scheme: http kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: - __meta_kubernetes_pod_container_port_name action: keep regex: .*-envoy-prom - source_labels: - __address__ - __meta_kubernetes_pod_annotation_prometheus_io_port action: replace regex: ([^:]+)(?::\d+)?;(\d+) replacement: $1:15090 target_label: __address__ - action: labelmap regex: __meta_kubernetes_pod_label_(.+) - source_labels: - __meta_kubernetes_namespace action: replace target_label: namespace - source_labels: - __meta_kubernetes_pod_name action: replace target_label: pod_name metric_relabel_configs: - action: keep source_labels: - __name__ regex: asm_llm_.*
采集vLLM服务监控指标。
ASM提供的LLM推理请求监控指标主要监控了外部LLM推理请求的吞吐情况。您可以为vLLM服务Pod增加Prometheus采集相关的注解,以采集vLLM服务暴露的监控指标,监控vLLM服务的内部状态。
... annotations: prometheus.io/path: /metrics # 指标暴露的HTTP Path。 prometheus.io/port: "8000" # 指标暴露端口,即为vLLM Server的监听端口。 prometheus.io/scrape: "true" # 是否抓取当前Pod的指标。 ...
通过Prometheus实例默认的服务发现机制采集vLLM服务相关指标。具体操作,请参见。
在vLLM服务提供的监控指标中,可以通过以下重点指标来直观了解vLLM工作负载的内部状态。
指标名称
说明
指标名称
说明
vllm:gpu_cache_usage_perc
vllm的GPU缓存使用百分比。vLLM启动时,会尽可能多地预先占有一块GPU显存,用于进行KV缓存。对于vLLM服务器,缓存利用率越低,代表GPU还有充足的空间将资源分配给新来的请求。
vllm:request_queue_time_seconds_sum
请求在等待状态排队花费的时间。LLM推理请求在到达vLLM服务器后、可能不会被立刻处理,而是需要等待被vLLM调度器调度运行预填充和解码。
vllm:num_requests_running
vllm:num_requests_waiting
vllm:num_requests_swapped
正在运行推理、正在等待和被交换到内存的请求数量。可以用来评估vLLM服务当前的请求压力。
vllm:avg_generation_throughput_toks_per_s
vllm:avg_prompt_throughput_toks_per_s
每秒被预填充阶段消耗的token以及解码阶段生成的token数量。
vllm:time_to_first_token_seconds_bucket
从请求发送到vLLM服务,到响应第一个token为止的时延水平。该指标通常代表了客户端在输出请求内容后得到首个响应所需的时间、是影响LLM用户体验的重要指标。
配置Grafana大盘检测LLM推理服务。
您可以通过Grafana大盘来观测基于vLLM部署的LLM推理服务:
通过基于ASM监控指标的面板观测服务的请求速率和整体token吞吐;
通过基于vLLM监控指标的面板观测推理工作负载的内部状态。
请确保Grafana使用的数据源Prometheus实例已经采集服务网格和vLLM的监控指标。
将以下内容导入到Grafana,创建LLM推理服务的可观测大盘。具体导入操作,请参见如何导出和导入Grafana仪表盘。
大盘效果如下:
(可选)通过可观测大盘与普通负载均衡方式进行性能对比
基于可观测大盘,可以直观地对比使用LLM推理服务负载均衡和使用传统最少请求负载均衡算法的实际表现:包括vLLM服务缓存使用率、请求排队时间、token吞吐量和ttft指标等。
执行以下命令,创建一个虚拟服务为示例LLM推理服务提供路由和传统负载均衡。
kubectl apply -f- <<EOF apiVersion: networking.istio.io/v1 kind: VirtualService metadata: name: llm-vs namespace: default spec: gateways: - default/llm-inference-gateway hosts: - '*' http: - name: any-host route: - destination: host: vllm-llama2-7b-pool.default.svc.cluster.local port: number: 8000 EOF
通过llmperf工具对LLM推理服务进行压力测试。
通过Grafana大盘比对两种路由和负载均衡方案。
下图直观展示了两种路由和负载均衡方案的对比,可以发现使用LLM推理服务负载均衡时,LLM推理服务拥有更好的延迟、吞吐量和缓存利用率表现。
- 本页导读 (1)
- 阅读前提示
- 背景信息
- 大语言模型LLM
- vLLM
- 负载均衡与可观测
- 传统负载均衡
- LLM负载均衡
- 传统可观测
- 推理流量可观测
- 前提条件
- 场景实践
- 步骤一:部署示例推理服务
- 步骤二:配置ASM网关规则
- 步骤三:配置LLM推理服务路由和负载均衡
- 步骤四:验证执行结果
- (可选)步骤五:配置LLM服务可观测指标与可观测大盘
- (可选)通过可观测大盘与普通负载均衡方式进行性能对比