Files
TheRock/tests/extended_tests/utils/system/hardware.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

893 lines
33 KiB
Python

# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Hardware detection for CPU and GPU using system commands and ROCm tools."""
import subprocess
import re
import os
import json
import platform
import multiprocessing
from typing import List, Optional, Dict, Tuple
from dataclasses import dataclass, field
from ..logger import log
@dataclass
class CpuInfo:
"""CPU information dataclass with model, cores, cache, and clock details."""
model_name: str = "Unknown"
cores: int = 0
sockets: int = 1
ram_size_gb: int = 0
numa_nodes: int = 1
clock_speed_mhz: int = 0
l1_cache_kb: int = 0
l2_cache_kb: int = 0
l3_cache_kb: int = 0
def getCpuModelName(self) -> str:
"""Get CPU model name."""
return self.model_name
def getCpuCores(self) -> int:
"""Get CPU cores count."""
return self.cores
def getCpuSockets(self) -> int:
"""Get CPU sockets count."""
return self.sockets
def getCpuRamSize(self) -> int:
"""Get RAM size in GB."""
return self.ram_size_gb
def getCpuNumaNodes(self) -> int:
"""Get NUMA nodes count."""
return self.numa_nodes
def getCpuClockSpeed(self) -> int:
"""Get CPU clock speed in MHz."""
return self.clock_speed_mhz
def getCpuL1Cache(self) -> int:
"""Get L1 cache size in KB."""
return self.l1_cache_kb
def getCpuL2Cache(self) -> int:
"""Get L2 cache size in KB."""
return self.l2_cache_kb
def getCpuL3Cache(self) -> int:
"""Get L3 cache size in KB."""
return self.l3_cache_kb
def __str__(self):
return f"{self.model_name} ({self.cores} cores, {self.sockets} sockets, {self.ram_size_gb}GB RAM)"
@dataclass
class GpuInfo:
"""GPU information."""
device_id: str = ""
revision_id: str = ""
product_name: str = "Unknown"
vendor: str = "AMD"
vram_size_gb: int = 0
sys_clock_mhz: int = 0
mem_clock_mhz: int = 0
pci_address: str = ""
vbios: str = "Unknown"
partition_mode: str = "Unknown"
xgmi_type: str = "Unknown"
host_driver: str = "Unknown"
target_graphics_version: str = "Unknown" # e.g., 'gfx942', 'gfx1100'
firmwares: List[Dict[str, str]] = field(default_factory=list)
def __str__(self):
return f"{self.product_name} (Device ID: {self.device_id}, VRAM: {self.vram_size_gb}GB)"
def _get_rocm_tool_path(tool_name: str) -> str:
"""Get full path to ROCm tool (amd-smi) using THEROCK_BIN_DIR.
Args:
tool_name: Name of the tool ('rocm-smi' or 'amd-smi')
Returns:
str: Full path to tool if THEROCK_BIN_DIR is set, otherwise just tool name
"""
therock_bin_dir = os.getenv("THEROCK_BIN_DIR")
if therock_bin_dir:
return os.path.join(therock_bin_dir, tool_name)
return tool_name
class HardwareDetector:
"""Simple hardware detector."""
def __init__(self):
"""Initialize hardware detector."""
self.cpu_info = None
self.gpu_list = []
def detect_all(self):
"""Detect all hardware (CPU and GPU)."""
self.detect_cpu()
self.detect_gpu()
def detect_cpu(self) -> CpuInfo:
"""Detect CPU information (cross-platform).
Returns:
CpuInfo object
"""
if platform.system() == "Windows":
return self._detect_cpu_windows()
else:
return self._detect_cpu_linux()
def _detect_cpu_linux(self) -> CpuInfo:
"""Detect CPU information from /proc/cpuinfo and lscpu (Linux).
Returns:
CpuInfo object
"""
try:
# Read /proc/cpuinfo
with open("/proc/cpuinfo", "r") as f:
cpuinfo = f.read()
# Extract model name
model_match = re.search(r"model name\s*:\s*(.+)", cpuinfo)
model_name = model_match.group(1).strip() if model_match else "Unknown"
# Count cores (logical processors)
cores = len(re.findall(r"^processor\s*:", cpuinfo, re.MULTILINE))
# Count sockets
physical_ids = set(re.findall(r"physical id\s*:\s*(\d+)", cpuinfo))
sockets = len(physical_ids) if physical_ids else 1
# Get CPU MHz (clock speed)
clock_match = re.search(r"cpu MHz\s*:\s*([\d.]+)", cpuinfo)
clock_speed_mhz = int(float(clock_match.group(1))) if clock_match else 0
# Get cache sizes from first processor
l1_cache_kb = 0
l2_cache_kb = 0
l3_cache_kb = 0
numa_nodes = 1
# Try to get cache info from lscpu if available
try:
lscpu_output = subprocess.check_output(["lscpu", "-B"], text=True)
# L1d cache (data)
l1d_match = re.search(
r"L1d cache:\s*(\d+)\s*([KMG]?)", lscpu_output, re.IGNORECASE
)
if l1d_match:
size = int(l1d_match.group(1))
unit = l1d_match.group(2).upper()
if unit == "M":
size *= 1024
elif unit == "G":
size *= 1024 * 1024
l1_cache_kb = size
# L1i cache (instruction)
l1i_match = re.search(
r"L1i cache:\s*(\d+)\s*([KMG]?)", lscpu_output, re.IGNORECASE
)
if l1i_match:
size = int(l1i_match.group(1))
unit = l1i_match.group(2).upper()
if unit == "M":
size *= 1024
elif unit == "G":
size *= 1024 * 1024
l1_cache_kb += size
# L2 cache
l2_match = re.search(
r"L2 cache:\s*(\d+)\s*([KMG]?)", lscpu_output, re.IGNORECASE
)
if l2_match:
size = int(l2_match.group(1))
unit = l2_match.group(2).upper()
if unit == "M":
size *= 1024
elif unit == "G":
size *= 1024 * 1024
l2_cache_kb = size
# L3 cache
l3_match = re.search(
r"L3 cache:\s*(\d+)\s*([KMG]?)", lscpu_output, re.IGNORECASE
)
if l3_match:
size = int(l3_match.group(1))
unit = l3_match.group(2).upper()
if unit == "M":
size *= 1024
elif unit == "G":
size *= 1024 * 1024
l3_cache_kb = size
# Get NUMA nodes
numa_match = re.search(r"NUMA node\(s\):\s*(\d+)", lscpu_output)
numa_nodes = int(numa_match.group(1)) if numa_match else 1
except Exception as e:
# Fallback: try to get cache from /proc/cpuinfo
cache_match = re.search(r"cache size\s*:\s*(\d+)\s*KB", cpuinfo)
if cache_match:
# This is usually L2 or L3 cache
l2_cache_kb = int(cache_match.group(1))
# Get RAM size from /proc/meminfo
ram_size_gb = 0
try:
with open("/proc/meminfo", "r") as f:
meminfo = f.read()
mem_match = re.search(r"MemTotal:\s*(\d+)\s*kB", meminfo)
if mem_match:
# Convert KB to GB
ram_size_gb = int(mem_match.group(1)) // (1024 * 1024)
except Exception:
pass
self.cpu_info = CpuInfo(
model_name=model_name,
cores=cores,
sockets=sockets,
ram_size_gb=ram_size_gb,
numa_nodes=numa_nodes,
clock_speed_mhz=clock_speed_mhz,
l1_cache_kb=l1_cache_kb,
l2_cache_kb=l2_cache_kb,
l3_cache_kb=l3_cache_kb,
)
except Exception:
self.cpu_info = CpuInfo()
return self.cpu_info
def _detect_cpu_windows(self) -> CpuInfo:
"""Detect CPU information using Windows APIs and wmic (Windows).
Returns:
CpuInfo object
"""
try:
# Get CPU cores using multiprocessing
cores = multiprocessing.cpu_count()
# Get CPU info using wmic
model_name = "Unknown"
clock_speed_mhz = 0
sockets = 1
l1_cache_kb = 0
l2_cache_kb = 0
l3_cache_kb = 0
ram_size_gb = 0
try:
# Get CPU model name
cpu_output = subprocess.check_output(
["wmic", "cpu", "get", "Name"], text=True, stderr=subprocess.DEVNULL
).strip()
lines = [
line.strip() for line in cpu_output.split("\n") if line.strip()
]
if len(lines) > 1:
model_name = lines[1] # First line is header "Name"
# Get CPU clock speed (in MHz)
speed_output = subprocess.check_output(
["wmic", "cpu", "get", "MaxClockSpeed"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
lines = [
line.strip() for line in speed_output.split("\n") if line.strip()
]
if len(lines) > 1 and lines[1].isdigit():
clock_speed_mhz = int(lines[1])
# Get number of physical CPUs (sockets)
socket_output = subprocess.check_output(
["wmic", "cpu", "get", "NumberOfCores"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
lines = [
line.strip() for line in socket_output.split("\n") if line.strip()
]
if len(lines) > 1 and lines[1].isdigit():
physical_cores = int(lines[1])
# If we have hyperthreading, sockets = logical cores / (physical cores * 2)
# Otherwise sockets = logical cores / physical cores
if physical_cores > 0:
sockets = max(1, cores // physical_cores)
# Get L2 cache size
cache_output = subprocess.check_output(
["wmic", "cpu", "get", "L2CacheSize"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
lines = [
line.strip() for line in cache_output.split("\n") if line.strip()
]
if len(lines) > 1 and lines[1].isdigit():
l2_cache_kb = int(lines[1])
# Get L3 cache size
cache_output = subprocess.check_output(
["wmic", "cpu", "get", "L3CacheSize"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
lines = [
line.strip() for line in cache_output.split("\n") if line.strip()
]
if len(lines) > 1 and lines[1].isdigit():
l3_cache_kb = int(lines[1])
# Get total RAM size (in KB, convert to GB)
mem_output = subprocess.check_output(
["wmic", "computersystem", "get", "TotalPhysicalMemory"],
text=True,
stderr=subprocess.DEVNULL,
).strip()
lines = [
line.strip() for line in mem_output.split("\n") if line.strip()
]
if len(lines) > 1 and lines[1].isdigit():
ram_size_gb = int(lines[1]) // (1024 * 1024 * 1024)
except Exception:
# If wmic fails, use basic detection
pass
self.cpu_info = CpuInfo(
model_name=model_name,
cores=cores,
sockets=sockets,
ram_size_gb=ram_size_gb,
numa_nodes=1, # NUMA detection on Windows is complex, default to 1
clock_speed_mhz=clock_speed_mhz,
l1_cache_kb=l1_cache_kb,
l2_cache_kb=l2_cache_kb,
l3_cache_kb=l3_cache_kb,
)
except Exception:
# Fallback to minimal detection with at least the core count
try:
cores = multiprocessing.cpu_count()
self.cpu_info = CpuInfo(cores=cores, sockets=1)
except Exception:
self.cpu_info = CpuInfo()
return self.cpu_info
def detect_gpu(self) -> List[GpuInfo]:
"""Detect GPU information using ROCm tools and lspci.
Detection hierarchy:
1. amd-smi (primary) - Best for modern compute GPUs, SR-IOV, containers
2. rocminfo (secondary) - Works with older ROCm versions, runtime-based
3. lspci (fallback) - Basic detection, no ROCm required
Returns:
List of GpuInfo objects
"""
self.gpu_list = []
# PRIMARY METHOD: Try amd-smi first (modern, preferred)
log.debug("Trying primary GPU detection with amd-smi...")
if self._detect_gpu_with_amd_smi():
log.debug(f"Successfully detected {len(self.gpu_list)} GPU(s) with amd-smi")
return self.gpu_list
# SECONDARY METHOD: Try rocminfo (older ROCm versions, runtime-based)
log.debug("Primary detection found 0 GPUs, trying rocminfo method...")
if self._detect_gpu_with_rocminfo():
log.debug(
f"Successfully detected {len(self.gpu_list)} GPU(s) with rocminfo"
)
return self.gpu_list
# FALLBACK METHOD: Use lspci (works without ROCm)
log.debug("Secondary detection found 0 GPUs, trying fallback lspci method...")
self._detect_gpu_with_lspci()
if len(self.gpu_list) > 0:
log.debug(f"Successfully detected {len(self.gpu_list)} GPU(s) with lspci")
else:
log.debug("No GPUs detected by any method")
return self.gpu_list
def _detect_gpu_with_lspci(self) -> bool:
"""Detect GPUs using lspci (fallback method).
Returns:
True if GPUs detected, False otherwise
"""
try:
# Run lspci to find AMD GPUs
log.debug("Running lspci to detect AMD GPUs...")
result = subprocess.run(
["lspci", "-d", "1002:", "-nn"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
log.debug(f"lspci failed with return code {result.returncode}")
return False
log.debug(f"lspci output:\n{result.stdout}")
# Parse output
for line in result.stdout.splitlines():
# Only actual GPUs (VGA/Display controller)
if "VGA compatible controller" in line or "Display controller" in line:
# Extract PCI address (XX:XX.X)
pci_match = re.match(
r"^([0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])", line
)
pci_address = pci_match.group(1) if pci_match else ""
# Extract device ID from [1002:XXXX]
device_id_match = re.search(r"\[1002:([0-9a-fA-F]{4})\]", line)
device_id = device_id_match.group(1) if device_id_match else ""
# Extract product name
parts = line.split("]:")
if len(parts) >= 2:
product_part = parts[-1].strip()
product_name = re.sub(r"\s*\([^)]*\)\s*$", "", product_part)
product_name = re.sub(r"\s*\[[^\]]*\]\s*$", "", product_name)
else:
product_name = "AMD GPU"
# Get detailed info from lspci -v
revision_id = ""
vram_size_gb = 0
if pci_address:
try:
detail_result = subprocess.run(
["lspci", "-s", pci_address, "-vv"],
capture_output=True,
text=True,
timeout=5,
)
detail_output = detail_result.stdout
# Extract revision ID (multiple formats)
# Format 1: "rev 0a" or "rev a1"
rev_match = re.search(
r"\(rev\s+([0-9a-fA-F]{2})\)",
detail_output,
re.IGNORECASE,
)
if rev_match:
revision_id = rev_match.group(1)
else:
# Format 2: "Revision: 0a"
rev_match = re.search(
r"Revision:\s*([0-9a-fA-F]{2})",
detail_output,
re.IGNORECASE,
)
if rev_match:
revision_id = rev_match.group(1)
# Also try from the main lspci line (appears after device ID)
if not revision_id:
rev_match = re.search(
r"\[1002:[0-9a-fA-F]{4}\].*?\(rev\s+([0-9a-fA-F]{2})\)",
line,
re.IGNORECASE,
)
if rev_match:
revision_id = rev_match.group(1)
log.debug(f"GPU {pci_address}: revision_id={revision_id}")
# Extract VRAM from memory regions
# Look for large memory regions (typically VRAM)
mem_regions = re.findall(
r"Memory at [0-9a-f]+ \(.*?\) \[size=(\d+)([MGT])\]",
detail_output,
re.IGNORECASE,
)
if mem_regions:
max_size = 0
for size_str, unit in mem_regions:
size = int(size_str)
if unit.upper() == "G":
size_gb = size
elif unit.upper() == "M":
size_gb = size / 1024
elif unit.upper() == "T":
size_gb = size * 1024
else:
size_gb = 0
if size_gb > max_size:
max_size = size_gb
vram_size_gb = int(max_size)
log.debug(f"GPU {pci_address}: vram_size_gb={vram_size_gb}")
except Exception as e:
log.debug(
f"Error getting GPU details for {pci_address}: {e}"
)
gpu = GpuInfo(
device_id=device_id,
revision_id=revision_id,
product_name=product_name.strip(),
vendor="AMD",
vram_size_gb=vram_size_gb,
pci_address=pci_address,
)
self.gpu_list.append(gpu)
return len(self.gpu_list) > 0
except Exception as e:
log.debug(f"lspci detection failed: {e}")
return False
def _detect_gpu_with_rocminfo(self) -> bool:
"""Detect GPUs using rocminfo (secondary method, ROCm runtime-based).
Returns:
True if GPUs detected, False otherwise
"""
try:
# Try to find rocminfo
rocminfo_cmd = _get_rocm_tool_path("rocminfo")
log.debug(f"Running {rocminfo_cmd} to detect AMD GPUs...")
result = subprocess.run(
[rocminfo_cmd],
capture_output=True,
text=True,
timeout=15,
)
if result.returncode != 0:
log.debug(f"rocminfo failed with return code {result.returncode}")
return False
log.debug(f"rocminfo output length: {len(result.stdout)} characters")
# Parse rocminfo output
# rocminfo outputs sections for each agent (GPU)
# Format:
# Agent 1 *******
# Name: gfx942
# Marketing Name: AMD Instinct MI300X
# Vendor Name: AMD
# Device Type: GPU
# ...
current_agent = None
gpu_agents = []
for line in result.stdout.splitlines():
# Detect start of agent section
if line.strip().startswith("Agent ") and "*" in line:
# Save previous agent if it was a GPU
if current_agent and current_agent.get("device_type") == "GPU":
gpu_agents.append(current_agent)
# Start new agent
agent_match = re.match(r"Agent\s+(\d+)", line.strip())
if agent_match:
current_agent = {"agent_id": agent_match.group(1)}
# Parse agent properties
elif current_agent is not None and ":" in line:
key_value = line.split(":", 1)
if len(key_value) == 2:
key = key_value[0].strip()
value = key_value[1].strip()
# Map rocminfo fields to our properties
if key == "Name":
current_agent["gfx_version"] = value
elif key == "Marketing Name":
current_agent["marketing_name"] = value
elif key == "Vendor Name":
current_agent["vendor"] = value
elif key == "Device Type":
current_agent["device_type"] = value
elif key == "Uuid":
current_agent["uuid"] = value
elif key == "Location (Bus/Device/Function)":
# Format: "Location (Bus/Device/Function): 0000:1b:00.0"
# Convert to XX:XX.X format
location_match = re.search(
r"([0-9a-fA-F]{4}):([0-9a-fA-F]{2}):([0-9a-fA-F]{2})\.([0-9a-fA-F])",
value,
)
if location_match:
# Extract just the bus:device.function part (skip domain)
pci_addr = f"{location_match.group(2)}:{location_match.group(3)}.{location_match.group(4)}"
current_agent["pci_address"] = pci_addr
# Don't forget the last agent
if current_agent and current_agent.get("device_type") == "GPU":
gpu_agents.append(current_agent)
log.debug(f"Found {len(gpu_agents)} GPU agent(s) in rocminfo output")
# Convert to GpuInfo objects
for agent in gpu_agents:
gpu_info = GpuInfo(
vendor=agent.get("vendor", "AMD"),
product_name=agent.get("marketing_name", "AMD GPU"),
target_graphics_version=agent.get("gfx_version", "Unknown"),
pci_address=agent.get("pci_address", ""),
)
self.gpu_list.append(gpu_info)
return len(self.gpu_list) > 0
except FileNotFoundError:
log.debug("rocminfo command not found")
return False
except Exception as e:
log.debug(f"rocminfo detection error: {e}")
return False
def _detect_gpu_with_amd_smi(self) -> bool:
"""Detect GPUs using amd-smi (primary method for compute GPUs).
Returns:
True if GPUs detected, False otherwise
"""
try:
amd_smi_cmd = _get_rocm_tool_path("amd-smi")
log.debug(f"Running {amd_smi_cmd} static --json for GPU detection...")
result = subprocess.run(
[amd_smi_cmd, "static", "--json"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
log.debug(f"amd-smi not available (exit code {result.returncode})")
return False
log.debug(f"amd-smi static output: {result.stdout[:200]}...")
data = json.loads(result.stdout)
# Handle different amd-smi output formats
gpu_list_data = None
if isinstance(data, dict) and "gpu_data" in data:
# New format: {"gpu_data": [...]}
gpu_list_data = data["gpu_data"]
log.debug("Detected new amd-smi format (gpu_data wrapper)")
elif isinstance(data, list):
# Old format: [...]
gpu_list_data = data
log.debug("Detected old amd-smi format (direct list)")
else:
log.debug(f"Unexpected amd-smi output format: {type(data)}")
return False
if not isinstance(gpu_list_data, list) or len(gpu_list_data) == 0:
log.debug("No GPUs found in amd-smi output")
return False
# Parse GPU data and create GpuInfo objects
for gpu_data in gpu_list_data:
gpu_info = GpuInfo(vendor="AMD")
# Extract asic information
if "asic" in gpu_data:
asic = gpu_data["asic"]
if isinstance(asic, dict):
gpu_info.product_name = asic.get("market_name", "AMD GPU")
gpu_info.device_id = asic.get("device_id", "").replace("0x", "")
gpu_info.revision_id = asic.get("rev_id", "").replace("0x", "")
gpu_info.target_graphics_version = asic.get(
"target_graphics_version", "Unknown"
)
# Extract bus information
if "bus" in gpu_data:
bus = gpu_data["bus"]
if isinstance(bus, dict):
gpu_info.pci_address = bus.get("bdf", "").replace("0000:", "")
# Extract VRAM information
if "vram" in gpu_data:
vram = gpu_data["vram"]
if isinstance(vram, dict):
vram_mb = vram.get("total", 0)
gpu_info.vram_size_gb = vram_mb // 1024 if vram_mb > 0 else 0
# Extract VBIOS
if "vbios" in gpu_data:
gpu_info.vbios = str(gpu_data.get("vbios", "Unknown"))
# Extract partition mode
if "partition" in gpu_data:
gpu_info.partition_mode = str(gpu_data.get("partition", "Unknown"))
# Extract driver info
if "driver" in gpu_data:
gpu_info.host_driver = str(gpu_data.get("driver", "Unknown"))
elif "driver_version" in gpu_data:
gpu_info.host_driver = str(
gpu_data.get("driver_version", "Unknown")
)
self.gpu_list.append(gpu_info)
return len(self.gpu_list) > 0
except FileNotFoundError:
log.debug("amd-smi command not found")
return False
except json.JSONDecodeError as e:
log.debug(f"Failed to parse amd-smi JSON output: {e}")
return False
except Exception as e:
log.debug(f"amd-smi detection error: {e}")
return False
@staticmethod
def get_gpu_info_from_amd_smi() -> Tuple[int, Optional[str]]:
"""Get GPU count and target graphics version directly from amd-smi.
Returns:
Tuple[int, Optional[str]]: (gpu_count, target_graphics_version)
- gpu_count: Number of GPUs detected (0 if detection fails)
- target_graphics_version: GPU architecture (e.g., 'gfx942', 'gfx1100') or None
"""
try:
amd_smi_cmd = _get_rocm_tool_path("amd-smi")
log.debug(f"Trying {amd_smi_cmd} static --json for GPU count...")
result = subprocess.run(
[amd_smi_cmd, "static", "--json"],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode != 0:
log.debug(f"amd-smi not available (exit code {result.returncode})")
return 0, None
log.debug(f"amd-smi static output: {result.stdout[:200]}...")
data = json.loads(result.stdout)
# Handle different amd-smi output formats
gpu_list = None
if isinstance(data, dict) and "gpu_data" in data:
# New format: {"gpu_data": [...]}
gpu_list = data["gpu_data"]
log.debug("Detected new amd-smi format (gpu_data wrapper)")
elif isinstance(data, list):
# Old format: [...]
gpu_list = data
log.debug("Detected old amd-smi format (direct list)")
else:
log.debug(f"Unexpected amd-smi output format: {type(data)}")
return 0, None
if not isinstance(gpu_list, list):
log.debug("gpu_list is not a list")
return 0, None
gpu_count = len(gpu_list)
# Extract target_graphics_version from first GPU
target_graphics_version = None
if gpu_count > 0:
first_gpu = gpu_list[0]
# Try different possible field names for gfx version
# Common fields: asic.target_graphics_version, asic.market_name, asic
if "asic" in first_gpu:
asic_data = first_gpu["asic"]
if isinstance(asic_data, dict):
# Try target_graphics_version field
target_graphics_version = asic_data.get(
"target_graphics_version"
)
# Try market_name which might contain gfx info
if not target_graphics_version:
market_name = asic_data.get("market_name", "")
# Extract gfxXXXX pattern from market name
gfx_match = re.search(r"gfx\w+", market_name, re.IGNORECASE)
if gfx_match:
target_graphics_version = gfx_match.group(0).lower()
# Try top-level fields
if not target_graphics_version:
target_graphics_version = first_gpu.get("target_graphics_version")
if not target_graphics_version:
target_graphics_version = first_gpu.get("gfx_version")
log.debug(
f"Detected {gpu_count} GPU(s), target_graphics_version={target_graphics_version}"
)
return gpu_count, target_graphics_version
except FileNotFoundError:
log.debug("amd-smi command not found")
return 0, None
except json.JSONDecodeError as e:
log.debug(f"Failed to parse amd-smi JSON output: {e}")
return 0, None
except Exception as e:
log.debug(f"amd-smi detection error: {e}")
return 0, None
def get_cpu(self) -> Optional[CpuInfo]:
"""Get detected CPU information.
Returns:
CpuInfo: Detected CPU info or None
"""
return self.cpu_info
def get_is_cpu_initialized(self) -> bool:
"""Check if CPU detection completed.
Returns:
bool: True if CPU info available
"""
return self.cpu_info is not None
def get_is_gpu_initialized(self) -> bool:
"""Check if GPU detection completed.
Returns:
bool: True if GPU detection was attempted
"""
return True # Always true after detect_all() is called
def getGpu(self):
"""Get GPU handler (camelCase compatibility alias).
Returns:
Self for accessing .adapters attribute
"""
return self
@property
def adapters(self) -> List[GpuInfo]:
"""Get GPU adapters list (compatibility property).
Returns:
List[GpuInfo]: List of detected GPUs
"""
return self.gpu_list