你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
基于 GPU 的工作负载(如 AI 推理服务)可能占用大量内存,难以优化和调试,而无需深入了解 GPU 实际执行的操作。 你可能会看到内存不足(OOM)错误、意外延迟峰值或 GPU 内存压力上升,但传统的 Kubernetes 指标不会告诉你在代码中分配内存 的位置 。 分析可帮助你了解负责 GPU 内存使用情况的确切函数。
Important
AKS 预览功能可在自助服务和自愿选择的基础上启用。 预览版按“现状”和“视供应情况”提供,它们不包括在服务级别协议和有限保证范围内。 AKS 预览功能是由客户支持尽最大努力部分覆盖。 因此,这些功能并不适合用于生产。 有关详细信息,请参阅以下支持文章:
本文逐步讲解如何在 AKS 上使用 GPU 可观测性:
- 部署实时 GPU 可观测性代理 - 使用基于 eBPF 的检测来跟踪和分析 GPU 内存分配。
- 读取火焰图 - 了解如何解释分析输出,以查找消耗最多 GPU 内存的确切函数。
Important
AKS 文档和示例中都提到了开源软件。 您部署的软件被排除在 AKS 服务级别协议、有限保修和 Azure 支持之外。 将开源技术与 AKS 一起使用时,请查阅相应社区和项目维护者提供的支持选项来制定计划。
Microsoft 将负责生成我们在 AKS 上部署的开源包。 该责任包括拥有构建、扫描、签名、验证和快速修复流程的完整所有权,并掌控容器镜像中的二进制文件。 如需了解详细信息,请参阅 AKS 漏洞管理和 AKS 支持范围。
部署 GPU 可观测性 - AKS 上的 GPU 内存分析
先决条件
- 至少具有一个启用了 GPU 的节点池的 AKS 群集。
-
Azure CLI 2.72.0 或更高版本安装。 运行
az --version以检查。 - 已安装
k8s-extensionAzure 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 推理工作负载捕获的。
Tip
在 Grafana 的火焰图面板中,使用 “展开所有组” 可查看完整的调用堆栈,无需折叠。 使用 “搜索 ”框查找特定函数或关键字。 若要防止面板再次折叠,请在查看已展开的火焰图时暂停仪表板自动刷新(将 Grafana 仪表板右上角的刷新间隔设为 关闭)。
Tip
使用 焦点块 专注于特定的分配路径。
读取符号
火焰图标签遵循以下约定:
| 符号格式 | 含义 |
|---|---|
Foo → bar |
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 的宽条形 — 仅调用其他函数 |