针对 llm-d 进行推理
本文档将向您展示如何与模型服务器和推理调度器进行交互。
若要对 llm-d 栈进行性能测试,请查看我们的基准测试文档。
先决条件
假设您已根据指南部署了 llm-d 推理栈(使用模型服务 Helm chart),或遵循了 llm-d 关于部署推理调度器和模型服务器的规范。
暴露您的网关
首先,我们需要选择暴露网关或与网关交互的策略。需要注意的是,这会受到您在按照特定指南安装 llm-d-infra chart 时所使用的值的影响。请选择与您的环境匹配的选项卡。
注意: 如果您不确定使用哪种方式,请从端口转发 (port-forward) 开始,因为它是最可靠且最简单的方法。对于任何需要共享的场景,请使用 Ingress/Route。如果您的云服务商支持且您只需要原始 L4 访问,请使用 LoadBalancer。
- 端口转发 (集群内部)
- 外部 IP (LoadBalancer)
- Ingress 控制器
端口转发 (集群内部)
对于安装在集群内的网关提供商,您可以直接对网关部署进行端口转发。
GATEWAY_SVC=$(kubectl get svc -n "${NAMESPACE}" -o yaml | yq '.items[] | select(.metadata.name | test(".*-inference-gateway(-.*)?$")).metadata.name' | head -n1)
注意: 此命令假设您在指定的 ${NAMESPACE} 中只有一个网关,即使有多个,它也只会获取按字母顺序排列的第一个网关服务的名称。如果您在单个命名空间中运行多个网关部署,则必须显式将 $GATEWAY_SVC 设置为相应的网关端点。
k get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
gaie-inference-scheduling-epp ClusterIP 10.16.3.250 <none> 9002/TCP,9090/TCP 18s
gaie-inference-scheduling-ip-18c12339 ClusterIP None <none> 54321/TCP 12s
gaie-sim-epp ClusterIP 10.16.1.220 <none> 9002/TCP,9090/TCP 80m
infra-inference-scheduling-inference-gateway-istio LoadBalancer 10.16.3.226 10.16.4.3 15021:34529/TCP,80:35734/TCP 22s
infra-sim-inference-gateway LoadBalancer 10.16.1.62 10.16.4.2 80:38348/TCP 81
export GATEWAY_SVC="infra-inference-scheduling-inference-gateway-istio"
获取网关服务名称后,我们就可以对其进行端口转发
export ENDPOINT="https://:8000"
kubectl port-forward -n ${NAMESPACE} service/${GATEWAY_SVC} 8000:80
注意: 端口 8000 是我们指南中的默认网关服务端口。您可以通过更改 llm-d-infra helm chart 的值并相应地更新端口转发命令来修改它。
外部 IP (LoadBalancer)
这要求 llm-d-infra chart 的发布版本必须将 .gateway.serviceType 设置为 LoadBalancer。目前这是默认值,但仍值得注意。
这需要您的 K8s 集群部署在具有 LB 集成的云提供商(EKS/GKE/AKS/AWS/…)上。
如果您使用的是 GKE 网关,或者为网关使用了 LoadBalancer 的默认服务类型,并且您处于具有负载均衡能力的云平台上,则可以使用网关服务的 External IP(在 kubectl get gateway 下的网关条目中您应该能看到相同的信息)。
export ENDPOINT=$(kubectl get gateway --no-headers -n ${NAMESPACE} -o jsonpath=``{.items[].status.addresses[0].value}``)
注意: 此命令假设您在指定的 ${NAMESPACE} 中只有一个网关,如果有多个,它只会随机获取一个。因此,如果您有多个网关,应找到正确的网关并进行针对性配置。
kubectl get gateway -n ${NAMESPACE}
NAME CLASS ADDRESS PROGRAMMED AGE
infra-inference-scheduling-inference-gateway kgateway af805bef3ec444a558da28061b487dd5-2012676366.us-east-1.elb.amazonaws.com True 11m
infra-sim-inference-gateway kgateway a67ad245358e34bba9cb274bc220169e-1351042165.us-east-1.elb.amazonaws.com True 45
GATEWAY_NAME=infra-inference-scheduling-inference-gateway
export ENDPOINT=$(kubectl get gateway ${GATEWAY_NAME} --no-headers -n ${NAMESPACE} -o jsonpath=``{.status.addresses[0].value}``)
Ingress 控制器
这要求 llm-d-infra chart 的发布版本必须将 .ingress.enabled 设置为 true,且将 .gateway.service.type 设置为 ClusterIP。此外,还需要为您的集群/Ingress 控制器配置负载均衡器。这可以是云提供商集成,也可以是像 MetalLB 这样的工具。
在所有选项中,此方式最依赖于环境,且配置起来可能比较复杂。有关更多信息,请参阅我们的网关自定义文档。您应该能够通过以下方式从 Ingress 获取端点:
export ENDPOINT=$(kubectl get ingress --no-headers -o jsonpath=``{.items[].status.loadBalancer.ingress[0].ip}``)
注意: 此命令假设您在指定的 ${NAMESPACE} 中只有一个 ingress,如果有多个,它只会随机获取一个。因此,如果您有多个网关,应找到正确的网关并进行针对性配置。
kubectl get ingress -n ${NAMESPACE}
NAME CLASS HOSTS ADDRESS PORTS AGE
infra-inference-scheduling-inference-gateway traefik * 166.19.16.120 80 21m
infra-sim-inference-gateway traefik * 166.19.16.132 80 7
INGRESS_NAME=infra-inference-scheduling-inference-gateway
export ENDPOINT=$(kubectl get ingress ${GATEWAY_NAME} --no-headers -n ${NAMESPACE} -o jsonpath=``{.status.loadBalancer.ingress[0].ip}``)
注意: 您还可以使用其他特定于平台的网络选项,例如 Openshift Routes。在通过 OCP routes 对 pd-disaggregation 示例进行基准测试时,我们注意到 Openshift Networking 会对网关请求强制执行超时,在重负载下这会影响我们的测试结果。如果您希望在基准测试设置中使用平台相关的选项,请务必检查您的平台文档。
上述每条路径都应导出 ${ENDPOINT} 环境变量,我们可以向该变量发送请求。
发送请求
/v1/models 端点
我们可以访问的第一条路径是 /v1/models。该端点仅依赖于您的 InferencePool 中运行的 vllm 服务器,因此即使您遵循宽泛的专家并行 (expert-parallelism) 指南,并且只有一个解码 (decode) 或预填充 (prefill) 实例启动,您也应该能获得响应(这也取决于 EPP 使用的配置)。这通常是最安全的请求方式,因为尽管不同指南中的模型各异,但 /v1/models 始终可用。
- 尝试使用 curl 访问
/v1/models端点
curl -s ${ENDPOINT}/v1/models \
-H "Content-Type: application/json" | jq
预期输出
`{
"data": [
{
"created": 1752727169,
"id": "random",
"object": "model",
"owned_by": "vllm",
"parent": null,
"root": "random"
}`,
`{
"created": 1752727169,
"id": "",
"object": "model",
"owned_by": "vllm",
"parent": "random",
"root": ""
}`
],
"object": "list"
}
/v1/completions
现在让我们尝试访问 /v1/completions 端点(这取决于模型,请确保您的模型与服务器针对 v1/models curl 返回的结果匹配)。
curl -X POST ${ENDPOINT}/v1/completions \
-H 'Content-Type: application/json' \
-d ``{
"model": "random",
"prompt": "How are you today?"
}`` | jq
预期输出
`{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "Today is a nice sunny day.",
"role": "assistant"
}`
}
],
"created": 1752727735,
"id": "chatcmpl-af42e9e3-dab0-420f-872b-d23353d982da",
"model": "random"
}
您可以在 /v1/completions 中使用的一些其他有用技巧包括控制 max_tokens 和传递 seed。
max_tokens 非常有用,因为它可以限制服务器返回的 token 数量。通过将其设置为 1,您可以模拟 TTFT(首个 token 响应时间)测试。您可以按照以下方式操作:
curl -s http://${ENDPOINT}/v1/completions \
-H "Content-Type: application/json" \
-d ``{
"model": "deepseek-ai/DeepSeek-R1-0528",
"prompt": "Hi, how are you?",
"max_tokens": "1"
}`` | jq
seed 非常有用,因为它允许推理服务器将每个请求视为唯一的。需要注意的是,这不会影响任何关于前缀缓存或 inference-scheduler 层面的内容,因此请求仍会被路由到相同位置,但它不会在 vllm 服务器层面进行任何缓存。
curl -s http://${ENDPOINT}/v1/completions \
-H "Content-Type: application/json" \
-d ``{
"model": "deepseek-ai/DeepSeek-R1-0528",
"prompt": "Hi, how are you?",
"seed": "'$(date +%M%H%M%S)'"
}`` | jq
有关 /v1/completions 端点可设置选项的更多信息,请参阅 openAI API server /v1/completions 文档
/v1/chat/completions (即将推出)
虽然 /v1/completions 是旧版 API,但目前仍受支持。我们正在将其转换为 /v1/chat/completions,这项工作正在多个地方进行跟踪。
kv-cache-manager 包的初始实现已作为 v0.2 冲刺的一部分落地。
llm-d-inference-scheduler 镜像中的支持正在进行中,属于 v0.3 路线图的一部分。此外,对上游 epp 镜像的进一步支持也在进行中,并在此处进行跟踪。
跟随请求日志
我们推荐使用 stern 工具来跟随请求,因为它允许您同时跟随多个 pod 日志。这在 llm-d 的上下文中特别有用,因为大多数部署都有多个 decode pod。有关更多信息,请参阅我们的可选工具文档。
要获取命名空间中的所有 decode pod,我们可以执行以下操作
DECODE_PODS=$(kubectl get pods --no-headers -n ${NAMESPACE} -l "llm-d.ai/role=decode" -o custom-columns=":metadata.name")
然后您可以使用 stern 同时查看这些日志
stern -n ${NAMESPACE} "$(echo "$DECODE_PODS" | paste -sd'|' -)"
就像使用 kubectl logs 一样,您可以在 pod 名称后指定容器名称,以仅获取特定容器的日志(stern 默认会打印 pod 中所有容器的日志)。操作如下:
stern -n ${NAMESPACE} "$(echo "$DECODE_PODS" | paste -sd'|' -)" -c routing-proxy # for routing sidecar logs
stern -n ${NAMESPACE} "$(echo "$DECODE_PODS" | paste -sd'|' -)" -c vllm # for vllm logs
要获取 prefill pod,您可以重复使用之前的命令,只需更换角色标签即可
PREFILL_PODS=$(kubectl get pods --no-headers -n ${NAMESPACE} -l "llm-d.ai/role=prefill" -o custom-columns=":metadata.name")
通常这没有那么必要,因为 prefill 在较低并行度下表现更好,目前我们所有的指南都使用 1 个 prefill 实例。
从日志中 grep 掉干扰信息
我们提供了一些有用的 grep -v 命令,可以帮助您清除 vllm 和 routing-proxy 容器中的干扰信息。
routing-proxy 容器干扰信息
从 K8s 的角度来看,一旦 vllm 容器上线(当其 status 变为 ready 时),sidecar 就会开始尝试连接并与其通信。然而,在 vllm pod 启动后,vllm API 服务器仍需要启动才能响应请求。在此时间差内,sidecar 中会产生一些不美观的日志,如下所示:
ms-inference-scheduling-llm-d-modelservice-decode-8ff7fd5bjvpbm routing-proxy E0824 16:49:51.115884 1 proxy.go:268] "waiting for vLLM to be ready" err="dial tcp [::1]:8200: connect: connection refused" logger="proxy server"
...
为了避免这种情况,请考虑在日志命令中添加以下内容
stern ... | grep -v "waiting for vLLM to be ready"
vllm 容器干扰信息
vllm 容器会记录对其指标 (metrics) 端点的任何访问,该端点应该会被持续轮询。您可以使用以下命令将它们过滤掉:
stern ... | grep -v "GET /metrics HTTP/1.1"
在某些情况下,您可能还想忽略 vllm 经常记录的使用情况信息,以便隔离每个请求的日志。vllm 的示例使用情况日志可能如下所示:
ms-inference-scheduling-llm-d-modelservice-decode-8ff7fd5bxf7lt vllm DEBUG 08-24 18:09:51 [loggers.py:122] Engine 000: Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 0.0 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.0%, Prefix cache hit rate: 0.0%
要针对所有这些使用情况日志,您可以直接过滤掉它们的共同前缀
stern ... | grep -v "Avg prompt throughput"
不过,您也可以自定义此 grep -v 命令以包含 0 值,从而仅在服务器开始接收流量后查看使用指标。您还可以使用 \| 分隔符将多个术语链接在一起进行 grep 过滤,例如:grep -v "日志中不显示 a\|日志中也不显示 b"。
此内容由 llm-d/llm-d 仓库 main 分支下的 docs/getting-started-inferencing.md 自动同步。