文章

GPU 裸金属服务器运维

GPU 裸金属服务器运维

GPU 集群的稳定性从物理层开始。裸金属服务器是整个 AI 基础设施的”地基”——BMC 远程管理、固件一致性、功耗散热、PCI 拓扑、硬件诊断,每一层的问题都会向上传导为”训练任务莫名 OOM""NCCL 超时""GPU 掉卡”等上层故障。

概述

裸金属服务器运维覆盖服务器的完整生命周期:上架 → 固件配置 → OS 部署 → 运行监控 → 硬件诊断 → 维护更换 → 退役下架。与虚拟化环境不同,裸金属直接操作物理硬件,没有 Hypervisor 隔离层,因此对硬件状态、固件版本、物理拓扑的掌控要求更高。

flowchart TB
    subgraph 裸金属生命周期
        A[物理上架<br/>布线/电源] --> B[固件配置<br/>BIOS/BMC/NIC/GPU VBIOS]
        B --> C[OS 部署<br/>PXE/IPXE/MAAS]
        C --> D[驱动安装<br/>GPU/CUDA/NIC/OFED]
        D --> E[运行监控<br/>BMC传感器/DCGM/IPMI]
        E --> F{硬件诊断}
        F -->|正常| E
        F -->|异常| G[故障隔离<br/>隔离节点/更换部件]
        G --> H[修复验证]
        H --> E
        E --> I[退役下架<br/>数据擦除/固件重置]
    end

一、BMC 远程管理

1.1 BMC 是什么

BMC(Baseboard Management Controller)是服务器主板上的独立管理芯片,独立于主 OS 运行,提供带外(Out-of-Band)管理能力。即使服务器宕机、OS 崩溃,BMC 仍可通过独立网口远程访问。

厂商BMC 品牌常见型号访问方式
DelliDRACiDRAC9 (Lifecycle Controller)Web / SSH / IPMI / Redfish API
HPEiLOiLO 5 / iLO 6Web / SSH / IPMI / RESTful API
浪潮BMC不定Web / IPMI / Redfish
超聚变iBMC不定Web / IPMI / Redfish
SupermicroBMC / IPMIAST2500 / AST2600Web / IPMI / Redfish
LenovoXCCXClarity ControllerWeb / IPMI / Redfish

1.2 IPMI 命令速查

# ====== 基本信息 ======
# 查看传感器数据(温度/风扇/电压/电源)
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> sensor list

# 查看传感器精简列表
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> sdr

# 查看系统事件日志 (SEL)
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> sel list

# 查看系统信息
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> fru

# ====== 电源管理 ======
# 远程开机
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power on

# 远程关机(硬关机)
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power off

# 软关机(通过 ACPI)
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power soft

# 重启(硬重启)
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power cycle

# 查看电源状态
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power status

# ====== 远程控制台 / 虚拟介质 ======
# 通过 SOL (Serial Over LAN) 远程查看控制台
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> sol activate

# 退出 SOL
# 按 ~. 退出

# ====== SEL 管理 ======
# 清除 SEL
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> sel clear

# 查看 SEL 事件详情
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> sel elist

# ====== MCU / 固件信息 ======
# 查看 BMC 固件版本
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> mc info

# 查看 BIOS 版本
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> fru | grep -i bios

1.3 Redfish API(现代管理接口)

Redfish 是 DMTF 标准的 RESTful API,逐步取代 IPMI 成为带外管理的主流方式。

# 查看系统信息
curl -s -k -u <user>:<pass> \
  https://<BMC_IP>/redfish/v1/Systems/1 | python3 -m json.tool

# 远程开机
curl -s -k -u <user>:<pass> \
  -X POST https://<BMC_IP>/redfish/v1/Systems/1/Actions/ComputerSystem.Reset \
  -H "Content-Type: application/json" \
  -d '{"ResetType": "On"}'

# 查看 SEL(事件日志)
curl -s -k -u <user>:<pass> \
  https://<BMC_IP>/redfish/v1/Managers/1/LogServices/SEL/Entries | python3 -m json.tool

# 虚拟介质挂载 ISO(以 iDRAC 为例)
curl -s -k -u <user>:<pass> \
  -X POST https://<BMC_IP>/redfish/v1/Managers/1/VirtualMedia/CD/Actions/VirtualMedia.InsertMedia \
  -H "Content-Type: application/json" \
  -d '{"Image": "https://nfs-server/os-install.iso", "Inserted": true}'

1.4 BMC 批量管理

"""BMC 批量管理器 —— 统一管理集群中所有裸金属服务器"""

from dataclasses import dataclass, field
from typing import Optional
import subprocess
import json
import concurrent.futures


@dataclass
class BMCHost:
    hostname: str
    bmc_ip: str
    bmc_user: str
    bmc_password: str
    vendor: str = ""          # dell / hpe / supermicro
    model: str = ""
    rack: str = ""
    u_position: str = ""
    os_ip: str = ""           # 业务网络 IP
    status: str = "unknown"   # online / offline / maintenance


@dataclass
class SensorReading:
    name: str
    value: float
    unit: str
    status: str          # ok / warning / critical
    lower_threshold: float = 0
    upper_threshold: float = 0


class BMCManager:
    """BMC 批量管理器"""

    def __init__(self, hosts: list[BMCHost]):
        self.hosts = {h.hostname: h for h in hosts}

    def get_power_status_all(self) -> dict[str, str]:
        """批量获取电源状态"""
        results = {}
        with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
            futures = {
                executor.submit(self._get_power_status, host): name
                for name, host in self.hosts.items()
            }
            for future in concurrent.futures.as_completed(futures):
                name = futures[future]
                try:
                    results[name] = future.result(timeout=10)
                except Exception as e:
                    results[name] = f"error: {e}"
        return results

    def get_sensors(self, hostname: str) -> list[SensorReading]:
        """获取传感器数据"""
        host = self.hosts.get(hostname)
        if not host:
            return []

        cmd = [
            "ipmitool", "-I", "lanplus",
            "-H", host.bmc_ip,
            "-U", host.bmc_user,
            "-P", host.bmc_password,
            "sensor", "list",
        ]
        try:
            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=15
            )
            return self._parse_sensors(result.stdout)
        except subprocess.TimeoutExpired:
            return []
        except Exception:
            return []

    def get_sel_events(self, hostname: str) -> list[dict]:
        """获取系统事件日志"""
        host = self.hosts.get(hostname)
        if not host:
            return []
        cmd = [
            "ipmitool", "-I", "lanplus",
            "-H", host.bmc_ip,
            "-U", host.bmc_user,
            "-P", host.bmc_password,
            "sel", "elist",
        ]
        try:
            result = subprocess.run(
                cmd, capture_output=True, text=True, timeout=15
            )
            return self._parse_sel(result.stdout)
        except Exception:
            return []

    def power_on(self, hostname: str) -> bool:
        return self._power_action(hostname, "on")

    def power_off(self, hostname: str) -> bool:
        return self._power_action(hostname, "off")

    def power_cycle(self, hostname: str) -> bool:
        return self._power_action(hostname, "cycle")

    def _power_action(self, hostname: str, action: str) -> bool:
        host = self.hosts.get(hostname)
        if not host:
            return False
        cmd = [
            "ipmitool", "-I", "lanplus",
            "-H", host.bmc_ip,
            "-U", host.bmc_user,
            "-P", host.bmc_password,
            "chassis", "power", action,
        ]
        try:
            result = subprocess.run(cmd, capture_output=True, timeout=15)
            return result.returncode == 0
        except Exception:
            return False

    def _get_power_status(self, host: BMCHost) -> str:
        cmd = [
            "ipmitool", "-I", "lanplus",
            "-H", host.bmc_ip,
            "-U", host.bmc_user,
            "-P", host.bmc_password,
            "chassis", "power", "status",
        ]
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
            return result.stdout.strip()
        except Exception as e:
            return f"error: {e}"

    @staticmethod
    def _parse_sensors(output: str) -> list[SensorReading]:
        sensors = []
        for line in output.strip().split("\n"):
            parts = [p.strip() for p in line.split("|")]
            if len(parts) >= 4:
                name = parts[0]
                try:
                    value = float(parts[1]) if parts[1] not in ("na", "") else 0
                except ValueError:
                    value = 0
                unit = parts[2]
                status = parts[3]
                sensors.append(SensorReading(
                    name=name, value=value, unit=unit, status=status
                ))
        return sensors

    @staticmethod
    def _parse_sel(output: str) -> list[dict]:
        events = []
        for line in output.strip().split("\n"):
            parts = [p.strip() for p in line.split("|")]
            if len(parts) >= 4:
                events.append({
                    "id": parts[0],
                    "timestamp": parts[1],
                    "sensor": parts[2],
                    "event": parts[3],
                })
        return events


# 使用示例
if __name__ == "__main__":
    hosts = [
        BMCHost("gpu-node-01", "10.0.10.1", "admin", "calvin",
                vendor="dell", model="R760xa", rack="R01", u_position="U18-U21"),
        BMCHost("gpu-node-02", "10.0.10.2", "admin", "calvin",
                vendor="dell", model="R760xa", rack="R01", u_position="U22-U25"),
    ]
    mgr = BMCManager(hosts)
    # 批量获取电源状态
    power_status = mgr.get_power_status_all()
    for name, status in power_status.items():
        print(f"{name}: {status}")
    # 获取传感器数据
    sensors = mgr.get_sensors("gpu-node-01")
    for s in sensors:
        if s.status != "ok":
            print(f"  [!] {s.name}: {s.value}{s.unit} ({s.status})")

二、BIOS/UEFI 配置

2.1 GPU 服务器关键 BIOS 设置

GPU 服务器的 BIOS 配置直接影响 GPU 性能和稳定性,以下是关键项:

BIOS 设置项推荐值说明
Above 4G DecodingEnabled必须!GPU 需要大于 4GB 的 MMIO 空间
SR-IOVEnabled虚拟化场景需要(MIG/vGPU)
VT-d / IOMMUEnabledPCI Passthrough 需要
NUMA Nodes per SocketNPS4 (AMD) / Node Interleave=Disabled保持 NUMA 拓扑,不要 Interleave
Memory Mapped I/O≥ 64GB8卡 GPU 服务器 MMIO 空间需求
PCIe Link SpeedGen5 / Auto确保不降速
Power ProfilePerformance关闭节能模式
C-StatesC0/C1 only深度省电会导致 GPU 通信延迟抖动
Hyper-ThreadingEnabled多数训练场景受益
Secure BootDisabled(或配置自定义密钥)GPU 驱动签名兼容性

2.2 BIOS 批量配置

# Dell iDRAC: 通过 racadm 批量配置 BIOS
racadm -r <BMC_IP> -u <user> -p <pass> set BIOS.MemSettings.Above4GDecoding Enabled
racadm -r <BMC_IP> -u <user> -p <pass> set BIOS.IntegratedDevices.SRIOVEnable Enabled
racadm -r <BMC_IP> -u <user> -p <pass> set BIOS.ProcSettings.VirtualizationTechnology Enabled
racadm -r <BMC_IP> -u <user> -p <pass> set BIOS.ProcSettings.C1E Disabled
racadm -r <BMC_IP> -u <user> -p <pass> set BIOS.SysProfileSettings.SysProfile Performance

# 配置变更后需要重启生效
racadm -r <BMC_IP> -u <user> -p <pass> jobqueue create BIOS.Setup.1-1
# 重启服务器
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> chassis power cycle
"""BIOS 配置批量校验"""

from dataclasses import dataclass


@dataclass
class BIOSConfig:
    hostname: str
    above_4g_decoding: bool = False
    sriov: bool = False
    vt_d: bool = False
    numa_nodes_per_socket: int = 1
    mmio_size: str = ""
    c_states_disabled: bool = False
    power_profile: str = ""
    secure_boot: bool = False


class BIOSValidator:
    """BIOS 配置校验器"""

    REQUIRED_SETTINGS = {
        "above_4g_decoding": True,
        "sriov": True,
        "vt_d": True,
        "c_states_disabled": True,
        "power_profile": "Performance",
    }

    def validate(self, config: BIOSConfig) -> list[str]:
        """校验 BIOS 配置,返回问题列表"""
        issues = []
        if not config.above_4g_decoding:
            issues.append("[严重] Above 4G Decoding 未启用 —— GPU 无法正常工作")
        if not config.sriov:
            issues.append("[警告] SR-IOV 未启用 —— 影响 MIG/vGPU 功能")
        if not config.vt_d:
            issues.append("[警告] VT-d/IOMMU 未启用 —— 影响 PCI Passthrough")
        if not config.c_states_disabled:
            issues.append("[建议] C-States 未禁用 —— 可能导致 NCCL 通信延迟抖动")
        if config.power_profile.lower() != "performance":
            issues.append("[建议] Power Profile 非 Performance —— 影响训练性能")
        if config.secure_boot:
            issues.append("[注意] Secure Boot 已启用 —— 需确认 GPU 驱动签名兼容")
        return issues

三、固件管理

3.1 固件类型与版本矩阵

GPU 裸金属服务器涉及多个固件层,版本一致性是集群稳定性的基础。

固件类型管理工具更新方式频率
BMC 固件iDRAC/iLO Web在线/虚拟介质季度
BIOSiDRAC/iLO通过 BMC季度
NIC 固件mlxup (Mellanox) / ethtool -e在线 OS 内季度
HBA/NVMe 固件厂商工具在线半年
GPU VBIOSnvidia-flashrom在线 OS 内跟随驱动
NVLink Switch 固件nvswitch CLI在线半年
PSU 固件BMC通过 BMC年度
CPLD 固件BMC通过 BMC年度

3.2 GPU VBIOS 与驱动版本矩阵

# 查看当前 GPU VBIOS 版本
nvidia-smi -q | grep -i "vbios"

# 查看当前驱动版本
nvidia-smi --query-gpu=driver_version --format=csv,noheader

# 查看 CUDA 版本
nvcc --version
cat /usr/local/cuda/version.json | grep version

# 查看固件兼容性
nvidia-smi -q | grep -A2 "GPU Firmware"

版本兼容性矩阵(示例):

GPU 驱动CUDAVBIOS (H100)推荐状态
535.129.0312.296.00.49.00✅ 生产验证
535.161.0712.296.00.49.00✅ 生产验证
545.23.0612.396.00.52.00⚠️ 测试中
550.40.0712.496.00.55.00⚠️ 新功能

关键原则:GPU 驱动、CUDA、VBIOS 三者版本必须兼容。集群内所有节点版本必须一致。升级时先在测试节点验证,再灰度推广。

3.3 固件批量更新脚本

"""固件批量更新管理器"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Optional


class FirmwareType(Enum):
    BMC = "bmc"
    BIOS = "bios"
    NIC = "nic"
    GPU_VBIOS = "gpu_vbios"
    NVSWITCH = "nvswitch"
    PSU = "psu"
    CPLD = "cpld"


class UpdateStatus(Enum):
    PENDING = "pending"
    DOWNLOADING = "downloading"
    UPLOADING = "uploading"
    INSTALLING = "installing"
    REBOOTING = "rebooting"
    SUCCESS = "success"
    FAILED = "failed"
    ROLLBACK = "rollback"


@dataclass
class FirmwareVersion:
    component: FirmwareType
    current_version: str
    target_version: str
    compatible_driver: str = ""
    release_notes: str = ""


@dataclass
class UpdateTask:
    hostname: str
    component: FirmwareType
    current_version: str
    target_version: str
    status: UpdateStatus = UpdateStatus.PENDING
    error: str = ""
    started_at: str = ""
    completed_at: str = ""


class FirmwareUpdateManager:
    """固件批量更新管理器"""

    def __init__(self, firmware_dir: str):
        self.firmware_dir = firmware_dir
        self.tasks: list[UpdateTask] = []

    def plan_update(self, hosts: list[str],
                    component: FirmwareType,
                    target_version: str,
                    current_versions: dict[str, str]) -> list[UpdateTask]:
        """规划更新任务,只更新版本不一致的节点"""
        for host in hosts:
            current = current_versions.get(host, "")
            if current != target_version:
                self.tasks.append(UpdateTask(
                    hostname=host,
                    component=component,
                    current_version=current,
                    target_version=target_version,
                ))
        return [t for t in self.tasks if t.status == UpdateStatus.PENDING]

    def execute_update(self, task: UpdateTask) -> bool:
        """执行单个更新任务"""
        task.status = UpdateStatus.INSTALLING
        # 实际执行固件更新命令
        # Dell iDRAC: racadm firmware update
        # HPE iLO: ilorest firmware upload
        # GPU: nvidia-flashrom
        # NIC: mlxup
        # 返回 True/False
        return True

    def batch_update(self, max_concurrent: int = 1) -> dict:
        """批量更新(串行或限并发)"""
        results = {"success": 0, "failed": 0, "skipped": 0}
        for task in self.tasks:
            if task.status != UpdateStatus.PENDING:
                results["skipped"] += 1
                continue
            success = self.execute_update(task)
            if success:
                task.status = UpdateStatus.SUCCESS
                results["success"] += 1
            else:
                task.status = UpdateStatus.FAILED
                results["failed"] += 1
        return results

四、功耗与散热管理

4.1 功耗监控

GPU 服务器是”电老虎”,单台 8 卡 H100 服务器满载功耗可达 10-12kW。

# GPU 功耗实时监控
nvidia-smi --query-gpu=index,name,power.draw,power.limit,temperature.gpu,clocks.sm,clocks.mem \
  --format=csv -l 1

# 查看功耗限制
nvidia-smi -q -d POWER

# 设置功耗限制(W)
sudo nvidia-smi -pl 700    # 限制到 700W

# GPU 持久化模式(减少驱动初始化开销)
sudo nvidia-smi -pm 1

# 查看节点总功耗(通过 BMC)
ipmitool -I lanplus -H <BMC_IP> -U <user> -P <pass> dcmi power reading

4.2 散热与热节流

"""GPU 热管理监控器"""

from dataclasses import dataclass, field
from enum import Enum
import subprocess
import json


class ThermalState(Enum):
    NORMAL = "normal"           # < 70°C
    WARM = "warm"              # 70-80°C
    HOT = "hot"                # 80-85°C (热节流阈值附近)
    CRITICAL = "critical"       # > 85°C (强制降频)
    EMERGENCY = "emergency"     # > 90°C (自动关机保护)


@dataclass
class GPUThermalStatus:
    gpu_index: int
    temperature: float           # GPU 温度 °C
    power_draw: float            # 当前功耗 W
    power_limit: float           # 功耗限制 W
    clock_sm: int                # SM 时钟 MHz
    clock_mem: int               # 显存时钟 MHz
    clock_throttle_reasons: str  # 降频原因
    fan_speed: int = 0           # 风扇转速 % (如适用)
    state: ThermalState = ThermalState.NORMAL


class GPUThermalMonitor:
    """GPU 热管理监控器"""

    THRESHOLDS = {
        "warm": 70,
        "hot": 80,
        "throttle": 83,      # H100 默认热节流阈值
        "critical": 85,
        "emergency": 90,
    }

    def get_status(self) -> list[GPUThermalStatus]:
        """获取所有 GPU 的热状态"""
        cmd = [
            "nvidia-smi",
            "--query-gpu=index,temperature.gpu,power.draw,power.limit,"
            "clocks.sm,clocks.mem,clocks_throttle_reasons.active,pstate",
            "--format=csv,noheader,nounits",
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        statuses = []
        for line in result.stdout.strip().split("\n"):
            parts = [p.strip() for p in line.split(",")]
            if len(parts) >= 7:
                temp = float(parts[1])
                statuses.append(GPUThermalStatus(
                    gpu_index=int(parts[0]),
                    temperature=temp,
                    power_draw=float(parts[2]),
                    power_limit=float(parts[3]),
                    clock_sm=int(parts[4]),
                    clock_mem=int(parts[5]),
                    clock_throttle_reasons=parts[6],
                    state=self._classify_temp(temp),
                ))
        return statuses

    def check_thermal_issues(self) -> list[dict]:
        """检查热管理问题"""
        statuses = self.get_status()
        issues = []
        for s in statuses:
            if s.state == ThermalState.EMERGENCY:
                issues.append({
                    "severity": "critical",
                    "gpu": s.gpu_index,
                    "message": f"GPU {s.gpu_index} 温度 {s.temperature}°C "
                               f"超过紧急阈值 {self.THRESHOLDS['emergency']}°C",
                    "action": "立即降低负载或关闭节点",
                })
            elif s.state == ThermalState.CRITICAL:
                issues.append({
                    "severity": "critical",
                    "gpu": s.gpu_index,
                    "message": f"GPU {s.gpu_index} 温度 {s.temperature}°C "
                               f"触发强制降频",
                    "action": "检查散热系统和风扇",
                })
            elif s.state == ThermalState.HOT:
                issues.append({
                    "severity": "warning",
                    "gpu": s.gpu_index,
                    "message": f"GPU {s.gpu_index} 温度 {s.temperature}°C "
                               f"接近热节流阈值",
                    "action": "降低功耗限制或改善散热",
                })
            # 检查是否已热节流
            if "Thermal" in s.clock_throttle_reasons:
                issues.append({
                    "severity": "warning",
                    "gpu": s.gpu_index,
                    "message": f"GPU {s.gpu_index} 正在热节流降频",
                    "action": "散热不足,检查风扇/液冷",
                })
        return issues

    def _classify_temp(self, temp: float) -> ThermalState:
        if temp >= self.THRESHOLDS["emergency"]:
            return ThermalState.EMERGENCY
        if temp >= self.THRESHOLDS["critical"]:
            return ThermalState.CRITICAL
        if temp >= self.THRESHOLDS["throttle"]:
            return ThermalState.HOT
        if temp >= self.THRESHOLDS["warm"]:
            return ThermalState.WARM
        return ThermalState.NORMAL

4.3 数据中心散热与功耗规划

GPU 型号单卡 TDP8卡服务器 TDP建议机柜功率散热方式
A100 80GB SXM400W~4.2kW8-10kW风冷/液冷
H100 80GB SXM5700W~10kW15-20kW液冷(推荐)
H200 141GB SXM700W~10kW15-20kW液冷
B200 192GB SXM1000W~14kW25-30kW液冷(必须)
L40S 350W350W~3.5kW6-8kW风冷

关键:H100/B200 级别 GPU 服务器满载功耗远超传统风冷机柜的 8kW 限制,必须规划液冷(冷板式/浸没式)和高密度供电(双路 30A/三相)。

五、PCI 拓扑与 GPU 枚举

5.1 查看 PCI 拓扑

# 查看 GPU 的 PCI 拓扑(关键!)
nvidia-smi topo -m

# 示例输出(8卡 H100 服务器):
#         GPU0  GPU1  GPU2  GPU3  GPU4  GPU5  GPU6  GPU7  NIC0    NIC1    NIC2    NIC3
# GPU0     X    NV12  NV12  NV12  NV12  NV12  NV12  NV12  PIX     NODE    SYS     SYS
# GPU1    NV12   X    NV12  NV12  NV12  NV12  NV12  NV12  PIX     NODE    SYS     SYS
# ...
# 解读:
#   NV12 = 12条 NVLink 连接
#   PIX = 同一 PCIe Switch 下
#   NODE = 同一 NUMA Node 但不同 PCIe Switch
#   SYS = 跨 CPU Socket(跨 NUMA Node),延迟最高

# 查看 PCIe 链路信息
nvidia-smi -q | grep -A5 "PCI"

# 查看详细 PCIe 拓扑
lspci -tv | grep -i nvidia

# 查看 NUMA 亲和性
numactl --hardware
cat /sys/bus/pci/devices/<pci_address>/numa_node

# 查看 GPU 与 NIC 的 NUMA 亲和性
# 确保训练任务的 GPU 和 NIC 在同一 NUMA Node
nvidia-smi topo -m | grep -E "NIC|NIC"

5.2 GPU PCI 地址映射

"""GPU PCI 地址与设备号映射管理"""

from dataclasses import dataclass
import subprocess
import re


@dataclass
class GPUDeviceInfo:
    gpu_index: int           # nvidia-smi 中的 GPU 编号
    pci_address: str         # PCI 地址 (0000:01:00.0)
    numa_node: int           # NUMA 节点
    pcie_link_gen: int       # PCIe 代数
    pcie_link_width: str     # PCIe 带宽 (x16)
    nvlinks: int             # NVLink 连接数
    uuid: str                # GPU UUID


class GPUDeviceMapper:
    """GPU 设备映射管理器"""

    def get_all_gpus(self) -> list[GPUDeviceInfo]:
        """获取所有 GPU 的设备信息"""
        cmd = [
            "nvidia-smi",
            "--query-gpu=index,pci.bus_id,pci.link.gen.current,"
            "pci.link.width.current,uuid",
            "--format=csv,noheader",
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        gpus = []
        for line in result.stdout.strip().split("\n"):
            parts = [p.strip() for p in line.split(",")]
            if len(parts) >= 5:
                pci_addr = self._normalize_pci_addr(parts[1])
                numa = self._get_numa_node(pci_addr)
                gpus.append(GPUDeviceInfo(
                    gpu_index=int(parts[0]),
                    pci_address=pci_addr,
                    numa_node=numa,
                    pcie_link_gen=int(parts[2]),
                    pcie_link_width=parts[3],
                    nvlinks=0,  # 需要从 topo -m 获取
                    uuid=parts[4],
                ))
        return gpus

    def check_pcie_health(self, gpus: list[GPUDeviceInfo]) -> list[str]:
        """检查 PCIe 链路健康度"""
        issues = []
        for gpu in gpus:
            if gpu.pcie_link_gen < 5:
                issues.append(
                    f"GPU {gpu.gpu_index}: PCIe Gen{gpu.pcie_link_gen} "
                    f"(期望 Gen5) —— 可能降速"
                )
            if "x16" not in gpu.pcie_link_width:
                issues.append(
                    f"GPU {gpu.gpu_index}: PCIe {gpu.pcie_link_width} "
                    f"(期望 x16) —— 带宽不足"
                )
        return issues

    def get_numa_affinity_map(self, gpus: list[GPUDeviceInfo]) -> dict[int, list[int]]:
        """获取 NUMA → GPU 映射"""
        numa_map: dict[int, list[int]] = {}
        for gpu in gpus:
            numa_map.setdefault(gpu.numa_node, []).append(gpu.gpu_index)
        return numa_map

    @staticmethod
    def _normalize_pci_addr(bus_id: str) -> str:
        """将 nvidia-smi 的 bus_id 转为标准 PCI 地址"""
        # 00000000:01:00.0 → 0000:01:00.0
        match = re.match(r'([0-9a-f]+):([0-9a-f]+):([0-9a-f]+)\.([0-9a-f]+)',
                         bus_id, re.I)
        if match:
            return f"0000:{match.group(2)}:{match.group(3)}.{match.group(4)}"
        return bus_id

    @staticmethod
    def _get_numa_node(pci_addr: str) -> int:
        """读取 GPU 的 NUMA 节点"""
        try:
            with open(f"/sys/bus/pci/devices/{pci_addr}/numa_node") as f:
                return int(f.read().strip())
        except FileNotFoundError:
            return -1

六、硬件诊断与故障隔离

6.1 常见硬件故障类型

故障类型表现诊断工具处理方式
GPU ECC 错误训练任务出错/崩溃nvidia-smi -q -d ECC记录/隔离/更换
GPU Xid 错误GPU 掉卡/卡死dmesg + nvidia-smiGPU Xid 错误排查手册
GPU 掉卡nvidia-smi 看不到 GPUlspci | grep NVIDIA重新插拔/更换
NVLink 降级训练速度下降nvidia-smi nvlink -s检查物理连接
NIC 故障NCCL 超时ibstat / ethtool更换网卡/线缆
内存故障内核 oops / 训练 OOMdmidecode / mcelog更换内存条
PSU 故障BMC 告警 / 随机关机IPMI sensor更换电源
风扇故障温度告警IPMI sensor更换风扇
CPU 故障随机 crashmcelog / dmesg更换 CPU
PCIe 故障GPU/NIC 间歇性消失lspci -vvv检查插槽/riser

6.2 硬件健康检查脚本

"""裸金属服务器硬件健康检查"""

from dataclasses import dataclass, field
from enum import Enum
import subprocess


class HealthStatus(Enum):
    HEALTHY = "healthy"
    WARNING = "warning"
    CRITICAL = "critical"
    OFFLINE = "offline"


@dataclass
class HealthCheckResult:
    hostname: str
    status: HealthStatus
    checks: dict[str, dict] = field(default_factory=dict)
    summary: str = ""

    @property
    def is_healthy(self) -> bool:
        return self.status == HealthStatus.HEALTHY


class HardwareHealthChecker:
    """硬件健康检查器"""

    def check_all(self, hostname: str) -> HealthCheckResult:
        """执行全套硬件检查"""
        result = HealthCheckResult(hostname=hostname, status=HealthStatus.HEALTHY)

        # 1. GPU 健康
        result.checks["gpu"] = self._check_gpu()

        # 2. 内存健康
        result.checks["memory"] = self._check_memory()

        # 3. 磁盘健康
        result.checks["disk"] = self._check_disk()

        # 4. 网卡健康
        result.checks["nic"] = self._check_nic()

        # 5. BMC 传感器
        result.checks["bmc_sensors"] = self._check_bmc_sensors()

        # 汇总
        critical = sum(1 for c in result.checks.values()
                       if c.get("status") == "critical")
        warning = sum(1 for c in result.checks.values()
                      if c.get("status") == "warning")

        if critical > 0:
            result.status = HealthStatus.CRITICAL
            result.summary = f"{critical} 项严重问题, {warning} 项警告"
        elif warning > 0:
            result.status = HealthStatus.WARNING
            result.summary = f"{warning} 项警告"
        else:
            result.summary = "所有检查通过"

        return result

    def _check_gpu(self) -> dict:
        """GPU 健康检查"""
        issues = []
        try:
            # 检查 GPU 数量
            result = subprocess.run(
                ["nvidia-smi", "-L"], capture_output=True, text=True, timeout=10
            )
            gpu_count = len(result.stdout.strip().split("\n"))
            if gpu_count < 8:  # 假设 8 卡服务器
                issues.append(f"GPU 数量异常: {gpu_count}/8")

            # 检查 ECC 错误
            result = subprocess.run(
                ["nvidia-smi", "-q", "-d", "ECC"],
                capture_output=True, text=True, timeout=10
            )
            if "Uncorrectable" in result.stdout:
                if int(result.stdout.split("Uncorrectable")[1].split(":")[1].strip()) > 0:
                    issues.append("检测到不可纠正 ECC 错误")

            # 检查掉卡
            result = subprocess.run(
                ["nvidia-smi", "--query-gpu=index", "--format=csv,noheader"],
                capture_output=True, text=True, timeout=10
            )
            gpu_indices = [int(x.strip()) for x in result.stdout.strip().split("\n")]
            for i in range(8):
                if i not in gpu_indices:
                    issues.append(f"GPU {i} 掉卡")
        except Exception as e:
            return {"status": "critical", "issues": [f"GPU检查失败: {e}"]}

        return {
            "status": "critical" if issues else "healthy",
            "issues": issues,
        }

    def _check_memory(self) -> dict:
        """内存检查"""
        issues = []
        try:
            # 检查 dmesg 中的内存错误
            result = subprocess.run(
                ["dmesg", "--time-format=iso"],
                capture_output=True, text=True, timeout=10
            )
            if "EDAC" in result.stdout or "Machine Check" in result.stdout:
                issues.append("dmesg 中检测到内存错误 (EDAC/MCE)")
        except Exception:
            pass
        return {"status": "critical" if issues else "healthy", "issues": issues}

    def _check_disk(self) -> dict:
        """磁盘 SMART 检查"""
        issues = []
        try:
            result = subprocess.run(
                ["smartctl", "--scan"], capture_output=True, text=True, timeout=10
            )
            for line in result.stdout.strip().split("\n"):
                if "/dev/" in line:
                    dev = line.split()[0]
                    r = subprocess.run(
                        ["smartctl", "-H", dev],
                        capture_output=True, text=True, timeout=10
                    )
                    if "PASSED" not in r.stdout:
                        issues.append(f"磁盘 {dev} SMART 检查未通过")
        except Exception:
            pass
        return {"status": "warning" if issues else "healthy", "issues": issues}

    def _check_nic(self) -> dict:
        """网卡链路检查"""
        issues = []
        try:
            result = subprocess.run(
                ["ip", "-o", "link", "show"],
                capture_output=True, text=True, timeout=10
            )
            for line in result.stdout.strip().split("\n"):
                if "NO-CARRIER" in line and "eth" in line:
                    iface = line.split(":")[1].strip().split("@")[0]
                    issues.append(f"网卡 {iface} 链路断开")
        except Exception:
            pass
        return {"status": "warning" if issues else "healthy", "issues": issues}

    def _check_bmc_sensors(self) -> dict:
        """BMC 传感器检查"""
        issues = []
        try:
            result = subprocess.run(
                ["ipmitool", "sdr"],
                capture_output=True, text=True, timeout=10
            )
            for line in result.stdout.strip().split("\n"):
                parts = [p.strip() for p in line.split("|")]
                if len(parts) >= 4 and parts[3] != "ok" and parts[3] != "na":
                    issues.append(f"传感器异常: {parts[0]} = {parts[1]}{parts[2]} ({parts[3]})")
        except Exception:
            pass
        return {"status": "critical" if issues else "healthy", "issues": issues}

6.3 故障隔离策略

flowchart TB
    A[检测到硬件故障] --> B{故障严重度}
    B -->|GPU ECC/Xid| C[标记 GPU 为 unhealthy]
    B -->|GPU 掉卡| D[标记节点为 NotReady]
    B -->|PSU/风扇| E[BMC 告警<br/>评估是否紧急]
    B -->|NIC 故障| F[标记 NIC 为 down<br/>NCCL 回退到 Socket]

    C --> G[cordon + drain 节点]
    D --> G
    E -->|紧急| G
    F --> H[在调度层面排除该 NIC]

    G --> I[人工确认故障]
    I --> J[更换硬件部件]
    J --> K[硬件健康检查]
    K -->|通过| L[uncordon 节点<br/>重新上线]
    K -->|未通过| J
# K8s 层面隔离故障节点
kubectl cordon gpu-node-03         # 禁止调度
kubectl drain gpu-node-03 --ignore-daemonsets --delete-emptydir-data  # 驱逐工作负载

# 在节点层面标记 GPU 为不可用(GPU Operator + Node Feature Discovery)
kubectl label node gpu-node-03 nvidia.com/gpu.count=0
kubectl taint nodes gpu-node-03 hardware-issue=true:NoSchedule

# 检查修复后重新上线
kubectl uncordon gpu-node-03
kubectl label node gpu-node-03 nvidia.com/gpu.count=8
kubectl taint nodes gpu-node-03 hardware-issue=true:NoSchedule-

七、裸金属 OS 部署

7.1 PXE 网络启动

已有笔记 集群自动化部署方案 覆盖了 PXE 服务端配置,这里补充 GPU 裸金属特有的 OS 调优。

7.2 GPU 服务器 OS 内核调优

# /etc/sysctl.conf —— GPU 服务器专用调优

# 1. 内存管理
vm.swappiness = 1                    # 几乎禁用 swap,避免 GPU 训练数据被 swap
vm.overcommit_memory = 1             # 允许内存超额分配(训练框架常需要)
vm.max_map_count = 262144            # 大页表映射
vm.dirty_ratio = 10                  # 降低脏页比例,缩短 fsync 延迟
vm.dirty_background_ratio = 5

# 2. 网络调优(RDMA/RoCE 场景)
net.core.rmem_max = 2147483647       # 最大接收缓冲区 2GB
net.core.wmem_max = 2147483647       # 最大发送缓冲区 2GB
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_rmem = 4096 87380 2147483647
net.ipv4.tcp_wmem = 4096 65536 2147483647

# 3. 文件描述符
fs.file-max = 2097152
fs.nr_open = 2097152

# 4. hugepages(GPU 训练大内存映射)
# 预留 1000 个 1GB 大页
vm.nr_hugepages = 0                  # 使用透明大页替代
echo always > /sys/kernel/mm/transparent_hugepage/enabled

# 5. IRQ 亲和性
# 让 GPU 和 NIC 的中断分配到正确的 NUMA Node
# /etc/security/limits.conf —— 提高资源限制
* soft nofile 1048576
* hard nofile 1048576
* soft memlock unlimited
* hard memlock unlimited
* soft stack unlimited
* hard stack unlimited
# 内核启动参数 (GRUB)
# /etc/default/grub
GRUB_CMDLINE_LINUX="... intel_iommu=on iommu=pt \
  pci=realloc hugepagesz=1G hugepages=0 \
  transparent_hugepage=always \
  numa_balancing=disable \
  processor.max_cstate=1 \
  idle=poll \
  pcie_aspm=off"

# 说明:
# - intel_iommu=on iommu=pt: 启用 IOMMU(PCI Passthrough)
# - numa_balancing=disable: 关闭自动 NUMA 负载均衡(训练任务自行绑定)
# - processor.max_cstate=1: 限制 C-State(降低 GPU 通信延迟抖动)
# - idle=poll: CPU 空闲时轮询(极致延迟,代价是功耗)
# - pcie_aspm=off: 关闭 PCIe 节能(保持链路全速)

# 更新 GRUB
grub2-mkconfig -o /boot/grub2/grub.cfg

八、裸金属运维 Checklist

日常巡检 Checklist

检查项命令频率告警阈值
GPU 数量nvidia-smi -L | wc -l每小时!= 预期数量
GPU ECC 错误nvidia-smi -q -d ECC每5分钟uncorrectable > 0
GPU 温度nvidia-smi --query-gpu=temperature.gpu每30秒> 83°C
GPU 功耗nvidia-smi --query-gpu=power.draw每30秒> TDP
NVLink 状态nvidia-smi nvlink -s每5分钟任何 link down
BMC 传感器ipmitool sdr每5分钟非 ok 状态
SEL 事件ipmitool sel elist每小时新增 critical 事件
PSU 状态ipmitool sdr | grep -i psu每5分钟非冗余/故障
NIC 链路ip -o link show每30秒NO-CARRIER
磁盘 SMARTsmartctl -H /dev/sd*每天FAILED
dmesg 错误dmesg | grep -i error每5分钟新增硬件错误
节点功耗ipmitool dcmi power reading每5分钟> 额定功率 90%

上架前 Checklist

  • 物理安装:GPU 卡、内存条、网卡、电源模块
  • 线缆连接:电源线(双路)、网线/光模块、管理线
  • BMC 网络连通性:能 ping 通 BMC IP
  • BIOS 配置:Above 4G / SR-IOV / VT-d / NUMA / C-States
  • BMC 固件版本:符合基线
  • BIOS 固件版本:符合基线
  • GPU 固件版本:符合基线
  • OS 安装:PXE 启动 + 自动化安装
  • 内核参数:sysctl + GRUB 调优
  • GPU 驱动安装:版本一致性
  • nvidia-smi 验证:8卡可见,PCIe Gen5 x16
  • NVLink 验证:所有 NVLink 活跃
  • 网络验证:RDMA/RoCE 连通性测试
  • 散热验证:满载温度 < 83°C
  • 功耗验证:满载功耗 < 额定 90%
  • 健康检查脚本全部通过

关联知识

参考资源

  • Dell iDRAC9 User’s Guide
  • HPE iLO 5 User Guide
  • NVIDIA DataCenter GPU Driver Documentation
  • IPMI 2.0 Specification
  • DMTF Redfish API Specification
  • NVIDIA GPU Best Practices for Data Centers

学习时间

约 8-10 小时(含 BMC 实操 + 硬件诊断实践)

状态

  • 理解 BMC 远程管理体系(IPMI/Redfish)
  • 掌握 GPU 服务器关键 BIOS 配置
  • 理解固件类型与版本管理
  • 掌握功耗与散热管理
  • 能查看和分析 PCI 拓扑
  • 能执行硬件健康检查
  • 掌握故障隔离策略
  • 掌握 GPU 服务器 OS 内核调优
  • 实际操作 BMC 批量管理
  • 完成一次完整的硬件诊断流程