你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

使用 GPU 分析优化Azure Kubernetes 服务 (AKS)上的 GPU 工作负荷(预览版)

基于 GPU 的工作负载(如 AI 推理服务)可能占用大量内存,难以优化和调试,而无需深入了解 GPU 实际执行的操作。 你可能会看到内存不足(OOM)错误、意外延迟峰值或 GPU 内存压力上升,但传统的 Kubernetes 指标不会告诉你在代码中分配内存 的位置 。 分析可帮助你了解负责 GPU 内存使用情况的确切函数。

Important

AKS 预览功能可在自助服务和自愿选择的基础上启用。 预览版按“现状”和“视供应情况”提供,它们不包括在服务级别协议和有限保证范围内。 AKS 预览功能是由客户支持尽最大努力部分覆盖。 因此,这些功能并不适合用于生产。 有关详细信息,请参阅以下支持文章:

本文逐步讲解如何在 AKS 上使用 GPU 可观测性:

  1. 部署实时 GPU 可观测性代理 - 使用基于 eBPF 的检测来跟踪和分析 GPU 内存分配。
  2. 读取火焰图 - 了解如何解释分析输出,以查找消耗最多 GPU 内存的确切函数。

Important

AKS 文档和示例中都提到了开源软件。 您部署的软件被排除在 AKS 服务级别协议、有限保修和 Azure 支持之外。 将开源技术与 AKS 一起使用时,请查阅相应社区和项目维护者提供的支持选项来制定计划。

Microsoft 将负责生成我们在 AKS 上部署的开源包。 该责任包括拥有构建、扫描、签名、验证和快速修复流程的完整所有权,并掌控容器镜像中的二进制文件。 如需了解详细信息,请参阅 AKS 漏洞管理AKS 支持范围

部署 GPU 可观测性 - AKS 上的 GPU 内存分析

先决条件

  • 至少具有一个启用了 GPU 的节点池的 AKS 群集。
  • Azure CLI 2.72.0 或更高版本安装。 运行 az --version 以检查。
  • 已安装 k8s-extension Azure CLI 加载项。 运行 az extension add --name k8s-extension 以安装。
  • 已安装 Helm 3.x 或更高版本。 运行 helm version 以检查。
  • Azure Monitor(可选),可以根据需要使用自己的监视设置。
  • Azure 托管 Grafana(可选,用于可视化)。

步骤 1:通过 Inspektor 小工具扩展启用 GPU 分析

Inspektor Gadget是一个面向 Kubernetes 的基于 eBPF 的开源可观测性框架。 针对 GPU 性能分析,它可跟踪计算统一设备架构 (CUDA) 内存分配调用,无需修改代码、添加边车或重启 Pod。 通过运行以下扩展命令在 AKS 群集上启用 GPU 分析:

az k8s-extension create \
  --extension-type microsoft.inspektorgadget \
  --subscription <your-subscription-id> \
  -g <your-resource-group> \
  -c <your-cluster-name> \
  -t managedClusters \
  --release-train preview \
  -n inspektor-gadget \
  --configuration-settings gpuObservability.enabled=true \
  --configuration-settings azureMonitor.enabled=true \

注释

此步骤假定已在 AKS 群集上启用Azure Monitor。 如果您计划使用自有的 Prometheus 配置,请删除 --configuration-settings azureMonitor.enabled=true

验证 Pod 是否正在运行:

kubectl get pods -n gadget -l k8s-app=gadget

Tip

GPU 内存分析可在内存分配事件发生时捕获这些事件。 如果工作负荷在启用 GPU 分析之前分配 GPU 内存,探查器不会捕获这些分配事件。 对于在启动期间预分配 GPU 内存的 vLLM,请在部署工作负荷之前启用 GPU 分析,或重启工作负荷以捕获初始内存分配路径。

步骤 2:使用 Pyroscope 启用性能分析可视化

注释

如果群集中有现有的 Grafana/Pyroscope 堆栈,则可以跳过此步骤。

Pyroscope 是一个开放源代码项目,可用于可视化和存储性能配置文件,这是内存优化和故障排除所必需的。 运行以下命令,将单个 Pyroscope 实例部署到群集

helm install pyroscope -n gadget \
  oci://ghcr.io/grafana/helm-charts/pyroscope \
  --version 1.15.0 \
  --set pyroscope.image.repository=grafana/pyroscope \
  --set-string pyroscope.image.tag=1.15.0 \
  --set pyroscope.replicaCount=1 \
  --set pyroscope.structuredConfig.self_profiling.disable_push=true \
  --set pyroscope.structuredConfig.storage.backend=filesystem \
  --set pyroscope.service.type=LoadBalancer \
  --set pyroscope.service.port=4040 \
  --set-string pyroscope.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-internal"=true \
  --set-string pyroscope.service.annotations."service\.beta\.kubernetes\.io/azure-pls-create"=true \
  --set-string pyroscope.service.annotations."service\.beta\.kubernetes\.io/azure-pls-name"=pyroscope-pls \
  --set-string pyroscope.service.annotations."service\.beta\.kubernetes\.io/azure-pls-proxy-protocol"=false \
  --set-string pyroscope.service.annotations."service\.beta\.kubernetes\.io/azure-pls-visibility"='*' \
  --set alloy.enabled=false \
  --set minio.enabled=false

验证 Pod 是否正在运行:

kubectl get pods -n gadget pyroscope-0

注释

若要部署高度可用的 Pyoscope 设置,请参阅Pyoscope 微服务文档 以获取配置选项。

步骤 3:将火镜连接到 Azure 托管 Grafana

Tip

您可以使用 kubectl port-forward -n gadget pyroscope-0 4040:4040 直接连接到 Pyroscope UI,查看您的工作负载配置文件。

将 Pyroscope 连接到 Azure 托管 Grafana使你能够可视化 Grafana 仪表板中的 GPU 配置文件。 我们需要一种安全的方式让 AMG 连接到作为 Kubernetes Pod 运行的Pyoscope,因此我们将使用Azure 专用链接建立连接。 首先设置与群集相关的环境变量:

export RESOURCE_GROUP="<your-resource-group>"
export AKS_CLUSTER="<your-aks-cluster-name>"
export LOCATION="<your-aks-cluster-location>"
export GRAFANA_NAME="<your-azure-managed-grafana-name>"
export AKS_NODE_RG=$(az aks show -g "$RESOURCE_GROUP" -n "$AKS_CLUSTER" --query 'nodeResourceGroup' -o tsv)

Tip

如果没有现有的Azure 托管 Grafana实例,请运行az grafana create -n "$GRAFANA_NAME" -g "$RESOURCE_GROUP" --location "$LOCATION" -o none以创建一个实例。

创建专用终结点,用于将 Pyroscope 连接到 Azure 托管 Grafana。 导出专用终结点变量:

export PYROSCOPE_PLS="pyroscope-pls"
export PYROSCOPE_MPE="pyroscope-mpe"
export PYROSCOPE_PORT="4040"

创建专用链接:

注释

创建专用链接可能需要几分钟时间。

# Check amg extension version
ver=$(az extension show --name amg --query version -o tsv)
[[ "${ver%%.*}" -ge 3 ]] && MPE="managed-private-endpoint" || MPE="mpe"

# Ensure Pyroscope PLS is present
until az network private-link-service show -n "$PYROSCOPE_PLS" -g "$AKS_NODE_RG" -o none 2>/dev/null; do
  sleep 10
done

# Get the PLS resource ID
PYRO_PLS_ID=$(az network private-link-service show \
  -n "$PYROSCOPE_PLS" -g "$AKS_NODE_RG" --query 'id' -o tsv)

# Create the MPE in Grafana
az grafana $MPE create \
  --workspace-name "$GRAFANA_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --name "$PYROSCOPE_MPE" \
  --private-link-resource-id "$PYRO_PLS_ID" \
  --location "$LOCATION" -o none

# Wait for MPE to be ready
sleep 30

# Find the pending connection created by Grafana
PYRO_CONN=$(az network private-link-service show \
  -n "$PYROSCOPE_PLS" -g "$AKS_NODE_RG" \
  --query "privateEndpointConnections[?privateLinkServiceConnectionState.status=='Pending' && starts_with(name, 'grafana-${GRAFANA_NAME}')].name | [0]" -o tsv)

# Approve it
az network private-link-service connection update \
  --name "$PYRO_CONN" \
  --service-name "$PYROSCOPE_PLS" \
  --resource-group "$AKS_NODE_RG" \
  --connection-status Approved -o none

# Refresh Grafana so it sees the approval
az grafana $MPE refresh \
  --workspace-name "$GRAFANA_NAME" \
  --resource-group "$RESOURCE_GROUP" -o none

echo "Successfully created private-link"

在Azure 托管 Grafana中创建数据源:

# Check amg extension version
ver=$(az extension show --name amg --query version -o tsv)
[[ "${ver%%.*}" -ge 3 ]] && MPE="managed-private-endpoint" || MPE="mpe"

# Grab the private IP
PYRO_IP=$(az grafana $MPE show \
  --workspace-name "$GRAFANA_NAME" \
  --resource-group "$RESOURCE_GROUP" \
  --name "$PYROSCOPE_MPE" \
  --query 'privateLinkServicePrivateIP' -o tsv)

# Prepare Pyroscope URL
export PYROSCOPE_URL="http://${PYRO_IP}:${PYROSCOPE_PORT}"

# Create Pyroscope data source in Grafana
az grafana data-source create -n "$GRAFANA_NAME" -g "$RESOURCE_GROUP" --definition "{
  \"name\": \"local-pyroscope\",
  \"uid\": \"local-pyroscope\",
  \"type\": \"grafana-pyroscope-datasource\",
  \"access\": \"proxy\",
  \"url\": \"${PYROSCOPE_URL}\",
  \"jsonData\": { \"keepCookies\": [\"pyroscope_git_session\"] }
}" -o none

echo "Successfully created local-pyroscope data-source"

注释

如果群集中有现有的 Grafana/Pyroscope 堆栈,则可以跳过此步骤。

验证数据源是否具有有效的 URL:

az grafana data-source show -n $GRAFANA_NAME --data-source local-pyroscope

步骤 4:将 Grafana 连接到 Prometheus Azure Monitor托管服务

注释

这些步骤基于 将 Prometheus 的 Azure Monitor 托管服务连接到 Grafana。 确保 Grafana 的托管标识在 Azure Monitor 工作区上拥有监视数据读取者角色,尤其是在其位于不同资源组或订阅中的情况下。

导出所需的变量:

export AMP_NAME="<your-amp-workspace-name>"
export AMP_RESOURCE_GROUP="<your-amp-resource-group>"

Tip

运行az resource list --resource-type Microsoft.Monitor/accounts -g $AMP_RESOURCE_GROUP -o table以列出Azure Monitor工作区信息。 Azure Monitor 工作区通常与 AKS 群集和 Azure 托管 Grafana 实例位于不同的资源组中,例如,当托管式 Prometheus 在区域 MA_<region>_<…> 资源组中自动创建工作区时,或者当中央平台团队管理一个共享工作区时。 若要改为在整个订阅范围内搜索,请省略 -g 并运行 az resource list --resource-type Microsoft.Monitor/accounts -o table

# Get AMW endpoint
AMP_ENDPOINT=$(az resource show --resource-type Microsoft.Monitor/accounts \
  -n "$AMP_NAME" -g "$AMP_RESOURCE_GROUP" \
  --query properties.metrics.prometheusQueryEndpoint -o tsv)

# Create Prometheus data source with MSI auth
az grafana data-source create -n "$GRAFANA_NAME" -g "$RESOURCE_GROUP" \
  --definition "{
    \"name\": \"$AMP_NAME\",
    \"type\": \"prometheus\",
    \"access\": \"proxy\",
    \"url\": \"$AMP_ENDPOINT\",
    \"jsonData\": {
      \"httpMethod\": \"POST\",
      \"azureCredentials\": { \"authType\": \"msi\" }
    }
  }"

验证数据源:

az grafana data-source show -n $GRAFANA_NAME --data-source $AMP_NAME

步骤 5:在 Grafana 中设置仪表板

az grafana dashboard create \
  -n "$GRAFANA_NAME" \
  -g "$RESOURCE_GROUP" \
  --definition "$(curl -sSL https://raw.githubusercontent.com/inspektor-gadget/grafana-dashboards/refs/heads/main/dashboards/gpu-observability/AdvancedGPUObservability.json)"

访问以下位置的仪表板:

GRAFANA_URL=$(az grafana show -n "$GRAFANA_NAME" -g "$RESOURCE_GROUP" --query properties.endpoint -o tsv)

echo "${GRAFANA_URL}/d/AdvancedGPUObservability"

有关读取 Grafana 中显示的火焰图的详细信息,请参阅 阅读火焰图

清理资源

若要删除群集内 GPU 可观测性堆栈,请执行以下操作:

helm uninstall inspektor-gadget -n gadget
helm uninstall pyroscope -n gadget
kubectl delete namespace gadget

解读火焰图

分析数据流入Pyoscope 和 Grafana 后,你将看到火焰图,其中显示了哪些函数消耗了最多的 GPU 内存。 以下部分介绍如何阅读这些可视化效果。

什么是火焰图?

火焰图是对已分析调用栈的可视化呈现。 每个条表示一个函数,条形图堆积起来以显示调用链,谁调用了谁。 每个条形图的宽度表示流经该函数的测量资源(CPU 时间、GPU 内存等)的数量。

关键规则:条形越宽,测量的资源越流过该函数。

以下示例火焰图是从 vLLM 推理工作负载捕获的。

Grafana 中 vLLM 工作负荷的初始折叠火焰图视图的屏幕截图,其中显示了表示 GPU 内存分配调用堆栈的堆积条形图。

Tip

在 Grafana 的火焰图面板中,使用 “展开所有组” 可查看完整的调用堆栈,无需折叠。 使用 “搜索 ”框查找特定函数或关键字。 若要防止面板再次折叠,请在查看已展开的火焰图时暂停仪表板自动刷新(将 Grafana 仪表板右上角的刷新间隔设为 关闭)。

在选择“展开所有组”后,Grafana 中 vLLM 工作负载的火焰图屏幕截图,显示了完整的调用栈以及 GPU 内存分配对应的各个函数帧。

Tip

使用 焦点块 专注于特定的分配路径。

读取符号

火焰图标签遵循以下约定:

符号格式 含义
Foobar class Foo: method def bar()
Foo__init__ Foo 的构造函数
bar (单独) 独立的 def bar() 函数
<interpreter trampoline> CPython 开销——忽略
<raw-address> 例如 0x7f151 原生 C/CUDA 代码——无可用的 Python 符号

例子:

  • GPUModelRunner / _allocate_kv_cache_tensors—类中的方法。 显示为 class GPUModelRunner: def _allocate_kv_cache_tensors(self)
  • LlamaMLP / __init__— 构造函数。 创建 LlamaMLP(...) 对象时调用。
  • _compile_fx_inner- 不在任何类内的独立模块级函数。

理解自身值与总值差异

此理解是分析火焰图时最重要的概念。

  • 总计 - 函数消耗的资源 及其调用的所有内容。 一个函数的总量可以很大,但它本身并不分配任何内存——它只是调用链的一部分。
  • 自身——函数直接消耗的资源,不包括其子函数。 较高的自身值表示资源实际上是在此函数中消耗的。

示例:GPUModelRunner._allocate_kv_cache_tensors 自身占用 55.1 GB — 该函数实际调用 torch.empty() 创建 KV 缓存张量。

导航提示:

  • 叶节点(上方无其他内容的条形)— 其整个宽度均为自身占用。 从这里开始查找分配热点。
  • 自身占用为 0 的宽条形 — 仅调用其他函数的业务流程协调程序函数。 排查内存分配时可安全跳过此项。
  • 较宽且自身数值较高的柱条——即你的优化目标。

查找最大的资源使用者

使用以下步骤识别热点:

  • 查看图表顶部最宽的条形——这些是实际分配内存的叶函数,条形越宽,消耗越多。
  • 检查自身值与总值差异 — 底部带有 self: 0 的宽条形仅为一条调用链。 向上追溯,直至找到自身分配量较高的条形。
  • 读取调用堆栈从下到上 - 排序会告知调用函数的原因。 例如:
      <raw-address> e.g 0x7f151               → native code entry
      <interpreter trampoline>                → CPython dispatch
      <module>                                → script top-level
      EngineCoreProc.run_engine_core          → vLLM engine startup
      EngineCore.__init__                     → engine initialization
      EngineCore._initialize_kv_caches        → KV cache setup
      Worker.initialize_from_config           → worker setup
      GPUModelRunner.initialize_kv_cache      → model runner
      GPUModelRunner._allocate_kv_cache_tensors → 💥 actual allocation
    
目标 应该注意什么
哪个分配得最多 最宽的叶条 (堆栈顶部)
占用资源最多的内容 最宽的条(位于堆叠底部)
优化目标 自身占用宽的条形 — 资源即消耗于此
要忽略的函数 自身占用为 0 的宽条形 — 仅调用其他函数

后续步骤