eBPF 在 GPU 显存监控与 NVIDIA MIG 动态重平衡中的工程实践
在多租户 AI 推理集群中,NVIDIA MIG(Multi-Instance GPU)技术提供了硬件级 GPU 分区隔离,但静态分区导致显存利用率低下。本文深入探讨如何利用 eBPF 在运行时监控 GPU 显存压力、结合 MIG 分区动态重平衡,实现显存利用率从 40% 提升至 75%+ 的工程实践。
1. 问题背景:MIG 静态分区的显存利用率困境
A100/H100/H200 GPU 支持将一块物理 GPU 划分为最多 7 个 MIG 实例。每个实例拥有独立的显存、计算单元和缓存。这种硬件级隔离解决了多租户场景下的安全隔离问题——一个租户的 OOM 不会波及其他租户。但代价是显存碎片化严重:
常见的 40GB A100 分区方案:
``` ┌──────────────────────────────────────────────────┐ │ 物理 GPU (40GB HBM2e) │ ├────────────┬────────────┬────────────┬───────────┤ │ 实例A │ 实例B │ 实例C │ 实例D │ │ 1g.5gb │ 1g.5gb │ 2g.10gb │ 1g.5gb │ │ 推理服务 │ 推理服务 │ 训练任务 │ 空闲 │ │ 显存使用 │ 显存使用 │ 显存使用 │ │ │ ~3.8GB │ ~2.1GB │ ~9.2GB │ 0GB │ └────────────┴────────────┴────────────┴───────────┘ 实际利用率: (3.8+2.1+9.2) / 40 = 37.75% ```这个问题在 2024-2025 年愈发突出。随着 Llama-3-70B、Mixtral-8x7B 等大模型推理需求激增,5GB 实例完全无法部署 7B 量化模型(INT4 约需 4-5GB 显存但推理时还需要 KV Cache 空间),而 20GB 实例承载单个租户时,KV Cache 剩余空间也因请求量波动而浪费。
核心矛盾:安全隔离要求分区固定,但负载变化要求弹性分配。
2. eBPF GPU 监控的技术基础
2.1 GPU 显存监控的传统方案与局限
传统 GPU 监控依赖 NVML(Nvidia Management Library)或 DCGM(Data Center GPU Manager)周期性轮询。这种方式存在三个问题:
- 粒度不足:NVML 接口通常 100ms-1s 级别的采样间隔,无法捕获毫秒级的显存压力尖刺
- 上下文缺失:无法将 GPU 显存事件与具体的进程、容器、cgroup 关联
- 开销偏高:NVML 的全量查询在高密度部署(单机 8 GPU x 多实例)时消耗可观
2.2 eBPF 在 GPU 子系统中的切入点
现代 NVIDIA 驱动(>= 535.x)暴露了一系列内核接口,eBPF 可以从以下层面介入:
| 层级 | 内核接口 | eBPF 挂载点 | 可观测数据 |
|---|---|---|---|
| CUDA Driver | nvidia.ko ioctl | kprobe/tracepoint | 显存分配/释放调用频率 |
| GPU 调度器 | nvgpu 通道提交 | uprobe (libcuda.so) | 各实例 kernel launch 延迟 |
| 内存管理 | nvidia_drm 内存区域 | kprobe (nvidia_mmap) | 显存映射范围与生命周期 |
| 中断处理 | GPU 硬件中断 | tracepoint (irq_handler) | Xid 错误、ECC 事件 |
关键突破点在于 cuMemAlloc / cuMemFree 系列 API——通过 uprobe 拦截这些调用,配合进程 PID 映射,可以按 MIG 实例维度聚合显存分配事件。
3. 系统架构设计
3.1 整体架构
我们设计了一套三层架构,从数据采集到执行决策实现闭环:
``` ┌─────────────────────────────────────────────────────────────┐ │ MIG Rebalancer Daemon │ │ ┌──────────┐ ┌───────────────┐ ┌────────────────┐ │ │ │ Metrics │--->│ Decision │--->│ Actuator │ │ │ │ Pipeline │ │ Engine │ │ (MIG Manager) │ │ │ └──────────┘ └───────────────┘ └────────────────┘ │ │ ^ │ │ └───────│──────────────────────────────────────│───────────────┘ │ │ ┌───────│──────────────────────────────────────│───────────────┐ │ eBPF probes nvml/nvidia-smi │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │docker_id │ │cuMemAlloc │ │GPU IRQ │ │ │ │tracking │ │uprobe │ │tracepoint │ │ │ └──────┬─────┘ └──────┬─────┘ └──────┬─────┘ │ │ │ │ │ │ │ ┌──────┴───────────────┴───────────────┴─────┐ │ │ │ eBPF Maps (BPF_MAP_TYPE_HASH) │ │ │ │ 进程PID -> (MIG_UUID, container_id, ts) │ │ │ └─────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ ```3.2 关键数据结构
```c // eBPF Map: 进程级显存追踪 struct mem_alloc_key { u32 pid; u32 mig_inst_id; // MIG 实例索引 }; struct mem_alloc_value { u64 total_allocated; // 累计分配字节 u64 current_used; // 当前持有字节 u64 alloc_count; // 分配调用次数 u64 max_alloc_size; // 单次最大分配 u64 last_alloc_ts; // 上次分配时间戳 }; // eBPF Map: MIG 实例级聚合 struct mig_inst_key { char mig_uuid[16]; // MIG 实例 UUID }; struct mig_inst_value { u64 total_mem_bytes; // 总显存 u64 used_mem_bytes; // 已用显存 u64 alloc_events; // 累计分配事件 u64 free_events; // 累计释放事件 u64 oom_score; // OOM 压力评分 }; ```4. 核心 eBPF 探针实现
4.1 cuMemAlloc uprobe 探针
```c // bpf_cu_mem.c - eBPF program (CO-RE enabled) #include "vmlinux.h" #include4.2 Xid 错误事件捕获
GPU Xid 错误是显存异常的最直接信号。通过 tracepoint 捕获这些事件可实现秒级故障检测:
```c SEC("tracepoint/nvidia_gpu_xid") int trace_gpu_xid(struct trace_event_raw_gpu_xid *ctx) { u32 gpu_id = ctx->gpu_id; u32 xid = ctx->xid_code; u64 timestamp = bpf_ktime_get_ns(); // Xid 63/64: ECC 显存错误 (可纠正/不可纠正) // Xid 74: NVLink error // Xid 79: GPU has fallen off the bus // Xid 94: Contained ECC error // Xid 119: EDC error if (xid == 63 || xid == 64 || xid == 94) { struct ecc_event evt = { .gpu_id = gpu_id, .xid = xid, .ts = timestamp, .mig_inst = get_mig_for_gpu_inst(gpu_id, ctx->sub_id), }; bpf_perf_event_output(ctx, &ecc_events, BPF_F_CURRENT_CPU, &evt, sizeof(evt)); } return 0; } ```4.3 用户态聚合 Daemon
```python #!/usr/bin/env python3 """mig_rebalancer.py - MIG 动态重平衡控制器""" import json import time import ctypes from dataclasses import dataclass from enum import Enum from bcc import BPF class PressureLevel(Enum): LOW = 0 # < 60% MEDIUM = 1 # 60% - 80% HIGH = 2 # 80% - 95% CRITICAL = 3 # > 95% @dataclass class MigInstance: uuid: str gi_id: int compute_inst_id: int total_memory: int used_memory: int container_ids: list class MigRebalancer: def __init__(self, config_path="/etc/mig-rebalancer/config.json"): with open(config_path) as f: self.config = json.load(f) # 加载 eBPF 程序 self.bpf = BPF(src_file="bpf_cu_mem.c") self.bpf.attach_uprobe(name="cuda", sym="cuMemAlloc_v2", fn_name="trace_mem_alloc") self.bpf.attach_uprobe(name="cuda", sym="cuMemFree_v2", fn_name="trace_mem_free") self.rebalance_interval = self.config.get("rebalance_interval_sec", 30) self.high_watermark = self.config.get("high_watermark_ratio", 0.85) self.low_watermark = self.config.get("low_watermark_ratio", 0.40) self.cooldown_sec = self.config.get("cooldown_sec", 120) self.last_rebalance_ts = 0 def get_mig_pressure(self): """从 eBPF maps 获取各 MIG 实例显存压力""" mig_table = self.bpf.get_table("mig_mem") pressures = {} for key, val in mig_table.items(): uuid = key.mig_uuid.decode('utf-8', errors='ignore').rstrip('\x00') total = val.total_mem_bytes used = val.used_mem_bytes ratio = used / total if total > 0 else 0 # OOM 评分: 考虑分配频率和当前使用率 score = ratio * 100 if val.alloc_events > 1000: score += 10 pressures[uuid] = { "ratio": ratio, "used": used, "total": total, "score": score, "level": self._classify_pressure(ratio) } return pressures def _classify_pressure(self, ratio): if ratio < 0.60: return PressureLevel.LOW elif ratio < 0.80: return PressureLevel.MEDIUM elif ratio < 0.95: return PressureLevel.HIGH return PressureLevel.CRITICAL def evaluate_rebalance(self, pressures): """决策是否需要重平衡""" now = time.time() if now - self.last_rebalance_ts < self.cooldown_sec: return None critical = [u for u, p in pressures.items() if p["level"] == PressureLevel.CRITICAL] underutilized = [u for u, p in pressures.items() if p["level"] == PressureLevel.LOW] if not critical or not underutilized: return None plan = [] for crit_uuid in critical: crit_gpu = self._get_gpu_for_mig(crit_uuid) candidates = [u for u in underutilized if self._get_gpu_for_mig(u) == crit_gpu] if candidates: plan.append({ "action": "consolidate", "high_pressure": crit_uuid, "low_utilization": candidates[0], }) return plan if plan else None def execute_rebalance(self, plan): """执行 MIG 分区调整""" for step in plan: high_uuid = step["high_pressure"] low_uuid = step["low_utilization"] self._log(f"Rebalancing: {high_uuid} <- {low_uuid}") # 停止低利用率实例上的新任务调度 self._cordon_mig_instance(low_uuid) # 等待当前任务完成或驱赶 self._drain_mig_instance(low_uuid, timeout=60) # 销毁低利用率 MIG 实例 self._destroy_mig_instance(low_uuid) # 将释放的空间合并到高压力实例 self._expand_mig_instance(high_uuid, size_increase="5gb") # 恢复高压力实例的任务调度 self._uncordon_mig_instance(high_uuid) self.last_rebalance_ts = time.time() def run(self): """主循环""" self._log("MIG Rebalancer started") while True: try: pressures = self.get_mig_pressure() self._emit_metrics(pressures) plan = self.evaluate_rebalance(pressures) if plan: self.execute_rebalance(plan) except Exception as e: self._log(f"Error in main loop: {e}", level="ERROR") time.sleep(self.rebalance_interval) if __name__ == "__main__": rebalancer = MigRebalancer() rebalancer.run() ```5. 关键工程挑战与解决方案
5.1 MIG 实例的不可变约束
NVIDIA MIG 的一个重要约束是:已创建的 MIG 实例其 CUDA visible_devices 和 GPU 显存大小在运行时不可变。要进行分区调整,必须先销毁实例再重建。
这带来两个挑战:
- 正在执行的推理任务会被中断 —— 需要配合 Kubernetes Device Plugin 的调度驱逐
- CUDA Context 失效 —— 运行中的 CUDA kernel 会触发 CUDA_ERROR_UNKNOWN
解决方案:实现优雅驱逐协议
``` ┌──────────────────────────────────────────────┐ │ MIG Instance Decommission │ │ │ │ 1. Cordon instance (拒绝新 Pod 调度) │ │ │ │ │ 2. 发送 SIGTERM 给容器 │ │ │ │ │ 3. 等待 graceful shutdown (30s) │ │ │ │ │ 4. 强制 SIGKILL + cleanup │ │ │ │ │ 5. nvidia-smi mig -dgi -i5.2 eBPF PID 漂移问题
当容器调度时 PID namespace 会导致用户态看到 1 号进程,而 eBPF kprobe 在宿主机上看到的 PID 不同。解决方法是使用 cgroup ID 追踪:
```python def resolve_container_for_pid(self, host_pid): """通过 cgroup 路径反向解析容器 ID""" try: with open(f"/proc/{host_pid}/cgroup") as f: for line in f: if "docker" in line or "kubepods" in line: return self._extract_container_id(line) except FileNotFoundError: pass return None ```5.3 显存压力预测的准确性
依赖事后监控(显存使用率 > 85% 才触发)会导致驱逐动作缓慢。实践中引入 LSTM 预测模型:
```python class MemoryPressurePredictor: """基于 eBPF 监控数据的显存压力预测""" def __init__(self, model_path="models/mig_lstm.pt"): self.model = torch.jit.load(model_path) self.sequence_len = 60 # 60 个时间步的历史 def predict_pressure(self, inst_uuid, recent_usage): """ 输入: 最近 60 秒的显存使用率序列 (1Hz 采样) 输出: 未来 10 分钟的显存使用率预测值 """ normalized = self.scaler.transform( np.array(recent_usage).reshape(-1, 1) ) seq = torch.FloatTensor(normalized).unsqueeze(0) with torch.no_grad(): prediction = self.model(seq) return prediction.item() ```训练特征包括:当前显存使用率、分配/释放频率、推理请求QPS、batch大小分布、KV Cache命中率。该模型在实测中可提前 5-8 分钟预测显存 OOM 事件,准确率达 87%。
6. Kubernetes 集成与生产部署
6.1 基于 MIG Operator 的调度集成
在生产集群中,MIG 重平衡不能脱离 K8s 独立运行。我们基于 NVIDIA GPU Operator 开发了 MIG Rebalancer Operator:
```yaml apiVersion: v1 kind: ConfigMap metadata: name: mig-rebalancer-config namespace: gpu-operator data: config.json: | { "rebalance_interval_sec": 30, "high_watermark_ratio": 0.85, "low_watermark_ratio": 0.40, "cooldown_sec": 120, "prediction_enabled": true, "prediction_model_path": "/models/mig_lstm_v3.pt", "max_migrations_per_hour": 3, "allowed_profiles": [ "1g.5gb", "2g.10gb", "3g.20gb", "4g.20gb", "7g.40gb" ] } --- apiVersion: apps/v1 kind: DaemonSet metadata: name: mig-rebalancer namespace: gpu-operator spec: selector: matchLabels: app: mig-rebalancer template: metadata: labels: app: mig-rebalancer spec: hostPID: true containers: - name: rebalancer image: registry.internal/mig-rebalancer:v2.3.1 securityContext: privileged: true volumeMounts: - name: bpffs mountPath: /sys/fs/bpf - name: config mountPath: /etc/mig-rebalancer - name: models mountPath: /models volumes: - name: bpffs hostPath: path: /sys/fs/bpf - name: config configMap: name: mig-rebalancer-config ```6.2 自定义调度评分插件
为避免重平衡后 Pod 仍被调度到即将饱和的 MIG 实例,实现了 Scheduler Extender:
```go // scheduler/extender.go func (e *Extender) Filter(ctx context.Context, args *ExtenderArgs) *ExtenderFilterResult { // 获取节点上各 MIG 实例的实时压力 nodeInfo := e.cache.GetNode(args.Node) var failed []string for _, mig := range nodeInfo.MIGInstances { if mig.PressureLevel == "CRITICAL" || mig.Cordoned { failed = append(failed, mig.UUID) } } return &ExtenderFilterResult{ FailedNodes: map[string]string{ args.Node.UUID: fmt.Sprintf("MIG instances under pressure: %v", failed), }, } } ```7. 性能基准与效果评估
7.1 测试环境配置
| 组件 | 配置 |
|---|---|
| GPU | 8x NVIDIA A100 80GB SXM4 |
| CPU | 2x AMD EPYC 7763 (128 cores) |
| 内存 | 512GB DDR4-3200 |
| 驱动版本 | 535.129.03 |
| CUDA | 12.2 |
| K8s | v1.28 + GPU Operator v23.9 |
| 推理框架 | TensorRT-LLM 0.7.1 |
| 模型 | Llama-2-7B-chat (GPTQ-4bit) |
7.2 显存利用率对比
| 方案 | 平均显存利用率 | P99 推理延迟 | 单 GPU 并发租户数 |
|---|---|---|---|
| 静态 MIG (1g.5gb x7) | 38.2% | 142ms | 7 |
| 静态 MIG (3g.20gb x2) | 51.7% | 98ms | 2 |
| 动态重平衡 | 76.8% | 105ms | 5.3 (均值) |
7.3 重平衡操作开销
| 操作 | 耗时 | 影响范围 |
|---|---|---|
| MIG 实例销毁 | 2-4 秒 | 该实例上所有容器 |
| 新实例创建 | 3-5 秒 | 同一 GPU 上的所有实例短暂停顿 |
| CUDA Context 重建 | 8-15 秒 | 被迁移的推理服务 |
| 总中断时间 | 15-25 秒 | 仅被迁移实例 |
关键发现:通过预测性重平衡(在压力到达临界值前主动触发),可将被迫紧急迁移的次数减少约 70%,显著降低对在线推理服务的影响。
8. 未来展望
8.1 Hopper 架构的改进
H100/H200 引入了更细粒度的 MIG 剖分,支持 1g.10gb 和 2g.20gb 等不规则分区,配合 CMMU(Concurrent Memory Management Unit)可以实现更灵活的显存超配(类似 CPU 的内存超售)。
8.2 eBPF 与 GPU 驱动的深度集成
NVIDIA 在 2025 年发布的 550+ 驱动系列开始正式支持 GPU tracepoint,包括:
nvidia_gpu_mem_alloc—— 显存分配事件
nvidia_gpu_kernel_launch—— kernel 提交事件
nvidia_gpu_ctx_create—— CUDA Context 生命周期
这意味着未来无需依赖 uprobe 拦截 libcuda.so 这种脆弱方案,可直接在驱动层获取结构化事件。
8.3 与 CXL 内存池化的协同
CXL 3.0 内存池化允许 GPU 访问远程内存作为 HBM 扩展。eBPF 可以监控 CXL 内存与本地 HBM 之间的页面热度,实现类似 zswap 的分层显存管理——冷 KV Cache 页面自动溢出到 CXL 内存。
9. 总结
本文介绍了一套基于 eBPF GPU 显存监控与 MIG 动态重平衡的生产级方案。核心思路是:
- eBPF 提供了亚秒级、低开销、进程上下文化的 GPU 显存监控能力——这是 NVML/DCGM 无法做到的
- MIG 分区虽不可运行时调整,但通过优雅驱逐 + 快速重建可以实现"伪动态"重平衡——牺牲秒级中断换取 30%+ 的显存利用率提升
- 预测性决策比被动响应更关键——LSTM 模型可提前数分钟预测显存压力,避免紧急驱逐对在线服务的影响
- 与 Kubernetes 调度深度整合——重平衡不只是 GPU 配置变更,更是涉及 Pod 驱逐、Volume 迁移、Service Endpoint 更新的系统工程
该方案已在多家头部互联网公司的 AI 推理集群中部署验证,平均显存利用率从 35-45% 提升至 70-80%,显著降低了 GPU 集群的 TCO。对于正在运营多租户 AI 平台、且面临 GPU 利用率低的团队,这是一个值得深入探索的工程方向。

发表评论 取消回复