Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added additional system_info #246

Merged
merged 14 commits into from
Dec 20, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/publish-to-test-pypi.yml
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ jobs:
with:
miniconda-version: "latest"
activate-environment: tkml
python-version: ${{ matrix.python-version }}
python-version: "3.10"
- name: Install pypa/build
run: >-
python -m pip install build --user
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -104,6 +104,13 @@ All build results, such as `.onnx` files, are collected into a cache directory,

`turnkey` collects statistics about each model and build into the corresponding build directory in the cache. Use `turnkey report -h` to see how those statistics can be exported into a CSV file.

### System Information

System information for the current `turnkey` installation is collected and viewed with the `system-info` management tool:

```
> turnkey system-info
```

## Extensibility

1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@
"fasteners",
"GitPython>=3.1.40",
"psutil",
"wmi",
# Conditional dependencies for ONNXRuntime backends
"onnxruntime >=1.10.1;platform_system=='Linux' and extra != 'llm-oga-cuda'",
"onnxruntime-directml >=1.19.0;platform_system=='Windows' and extra != 'llm-oga-cuda'",
122 changes: 1 addition & 121 deletions src/turnkeyml/common/build.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import os
import logging
import sys
import shutil
import traceback
import platform
import subprocess
from typing import Dict, Union
import hashlib
import pkg_resources
import psutil
import yaml
import torch
import numpy as np
import turnkeyml.common.exceptions as exp


UnionValidModelInstanceTypes = Union[
None,
str,
@@ -197,7 +192,7 @@ def get_shapes_and_dtypes(inputs: dict):

class Logger:
"""
Redirects stdout to to file (and console if needed)
Redirects stdout to file (and console if needed)
"""

def __init__(
@@ -266,118 +261,3 @@ def write(self, message):
def flush(self):
# needed for python 3 compatibility.
pass


def get_system_info():
os_type = platform.system()
info_dict = {}

# Get OS Version
try:
info_dict["OS Version"] = platform.platform()
except Exception as e: # pylint: disable=broad-except
info_dict["Error OS Version"] = str(e)

def get_wmic_info(command):
try:
output = subprocess.check_output(command, shell=True).decode()
return output.split("\n")[1].strip()
except Exception as e: # pylint: disable=broad-except
return str(e)

if os_type == "Windows":
if shutil.which("wmic") is not None:
info_dict["Processor"] = get_wmic_info("wmic cpu get name")
info_dict["OEM System"] = get_wmic_info("wmic computersystem get model")
mem_info_bytes = get_wmic_info(
"wmic computersystem get TotalPhysicalMemory"
)
try:
mem_info_gb = round(int(mem_info_bytes) / (1024**3), 2)
info_dict["Physical Memory"] = f"{mem_info_gb} GB"
except ValueError:
info_dict["Physical Memory"] = mem_info_bytes
else:
info_dict["Processor"] = "Install WMIC to get system info"
info_dict["OEM System"] = "Install WMIC to get system info"
info_dict["Physical Memory"] = "Install WMIC to get system info"

elif os_type == "Linux":
# WSL has to be handled differently compared to native Linux
if "microsoft" in str(platform.release()):
try:
oem_info = (
subprocess.check_output(
'powershell.exe -Command "wmic computersystem get model"',
shell=True,
)
.decode()
.strip()
)
oem_info = (
oem_info.replace("\r", "")
.replace("\n", "")
.split("Model")[-1]
.strip()
)
info_dict["OEM System"] = oem_info
except Exception as e: # pylint: disable=broad-except
info_dict["Error OEM System (WSL)"] = str(e)

else:
# Get OEM System Information
try:
oem_info = (
subprocess.check_output(
"sudo -n dmidecode -s system-product-name",
shell=True,
stderr=subprocess.DEVNULL,
)
.decode()
.strip()
.replace("\n", " ")
)
info_dict["OEM System"] = oem_info
except subprocess.CalledProcessError:
# This catches the case where sudo requires a password
info_dict["OEM System"] = "Unable to get oem info - password required"
except Exception as e: # pylint: disable=broad-except
info_dict["Error OEM System"] = str(e)

# Get CPU Information
try:
cpu_info = subprocess.check_output("lscpu", shell=True).decode()
for line in cpu_info.split("\n"):
if "Model name:" in line:
info_dict["Processor"] = line.split(":")[1].strip()
break
except Exception as e: # pylint: disable=broad-except
info_dict["Error Processor"] = str(e)

# Get Memory Information
try:
mem_info = (
subprocess.check_output("free -m", shell=True)
.decode()
.split("\n")[1]
.split()[1]
)
mem_info_gb = round(int(mem_info) / 1024, 2)
info_dict["Memory Info"] = f"{mem_info_gb} GB"
except Exception as e: # pylint: disable=broad-except
info_dict["Error Memory Info"] = str(e)

else:
info_dict["Error"] = "Unsupported OS"

# Get Python Packages
try:
installed_packages = pkg_resources.working_set
info_dict["Python Packages"] = [
f"{i.key}=={i.version}"
for i in installed_packages # pylint: disable=not-an-iterable
]
except Exception as e: # pylint: disable=broad-except
info_dict["Error Python Packages"] = str(e)

return info_dict
386 changes: 386 additions & 0 deletions src/turnkeyml/common/system_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
from abc import ABC
import importlib.metadata
import platform
import re
import subprocess


class SystemInfo(ABC):
"""Abstract base class for OS-dependent system information classes"""

def __init__(self):
pass

def get_dict(self):
"""
Retrieves all the system information into a dictionary
Returns:
dict: System information
"""
info_dict = {
"OS Version": self.get_os_version(),
"Python Packages": self.get_python_packages(),
}
return info_dict

@staticmethod
def get_os_version() -> str:
"""
Retrieves the OS version.
Returns:
str: OS Version
"""
try:
return platform.platform()
except Exception as e: # pylint: disable=broad-except
return f"ERROR - {e}"

@staticmethod
def get_python_packages() -> list:
"""
Retrieves the Python package versions.
Returns:
list: List of Python package versions in the form ["package-name==package-version", ...]
"""
# Get Python Packages
distributions = importlib.metadata.distributions()
return [
f"{dist.metadata['name']}=={dist.metadata['version']}"
for dist in distributions
]


class WindowsSystemInfo(SystemInfo):
"""Class used to access system information in Windows"""

def __init__(self):
super().__init__()
import wmi

self.connection = wmi.WMI()

def get_processor_name(self) -> str:
"""
Retrieves the name of the processor.
Returns:
str: Name of the processor.
"""
processors = self.connection.Win32_Processor()
if processors:
return (
f"{processors[0].Name.strip()} "
f"({processors[0].NumberOfCores} cores, "
f"{processors[0].NumberOfLogicalProcessors} logical processors)"
)
return "Processor information not found."

def get_system_model(self) -> str:
"""
Retrieves the model of the computer system.
Returns:
str: Model of the computer system.
"""
systems = self.connection.Win32_ComputerSystem()
if systems:
return systems[0].Model
return "System model information not found."

def get_physical_memory(self) -> str:
"""
Retrieves the physical memory of the computer system.
Returns:
str: Physical memory
"""
memory = self.connection.Win32_PhysicalMemory()
if memory:
total_capacity = sum([int(m.Capacity) for m in memory])
total_capacity_str = f"{total_capacity/(1024**3)} GB"
details_str = " + ".join(
[
f"{m.Manufacturer} {int(m.Capacity)/(1024**3)} GB {m.Speed} ns"
for m in memory
]
)
return total_capacity_str + " (" + details_str + ")"
return "Physical memory information not found."

def get_bios_version(self) -> str:
"""
Retrieves the BIOS Version of the computer system.
Returns:
str: BIOS Version
"""
bios = self.connection.Win32_BIOS()
if bios:
return bios[0].Name
return "BIOS Version not found."

def get_max_clock_speed(self) -> str:
"""
Retrieves the max clock speed of the CPU of the system.
Returns:
str: Max CPU clock speed
"""
processor = self.connection.Win32_Processor()
if processor:
return f"{processor[0].MaxClockSpeed} MHz"
return "Max CPU clock speed not found."

def get_driver_version(self, device_name) -> str:
"""
Retrieves the driver version for the specified device name.
Returns:
str: Driver version, or None if device driver not found
"""
drivers = self.connection.Win32_PnPSignedDriver(DeviceName=device_name)
if drivers:
return drivers[0].DriverVersion
return ""

@staticmethod
def get_npu_power_mode() -> str:
"""
Retrieves the NPU power mode.
Returns:
str: NPU power mode
"""
try:
out = subprocess.check_output(
[
r"C:\Windows\System32\AMD\xrt-smi.exe",
"examine",
"-r",
"platform",
],
stderr=subprocess.STDOUT,
).decode()
lines = out.splitlines()
modes = [line.split()[-1] for line in lines if "Mode" in line]
if len(modes) > 0:
return modes[0]
except FileNotFoundError:
# xrt-smi not present
pass
except subprocess.CalledProcessError:
pass
return "NPU power mode not found."

@staticmethod
def get_windows_power_setting() -> str:
"""
Retrieves the Windows power setting.
Returns:
str: Windows power setting.
"""
try:
out = subprocess.check_output(["powercfg", "/getactivescheme"]).decode()
return re.search(r"\((.*?)\)", out).group(1)
except subprocess.CalledProcessError:
pass
return "Windows power setting not found"

def get_dict(self) -> dict:
"""
Retrieves all the system information into a dictionary
Returns:
dict: System information
"""
info_dict = super().get_dict()
info_dict["Processor"] = self.get_processor_name()
info_dict["OEM System"] = self.get_system_model()
info_dict["Physical Memory"] = self.get_physical_memory()
info_dict["BIOS Version"] = self.get_bios_version()
info_dict["CPU Max Clock"] = self.get_max_clock_speed()
info_dict["Windows Power Setting"] = self.get_windows_power_setting()
if "AMD" in info_dict["Processor"]:
device_names = [
"NPU Compute Accelerator Device",
"AMD-OpenCL User Mode Driver",
]
driver_versions = {
device_name: self.get_driver_version(device_name)
for device_name in device_names
}
info_dict["Driver Versions"] = {
k: (v if len(v) else "DEVICE NOT FOUND")
for k, v in driver_versions.items()
}
info_dict["NPU Power Mode"] = self.get_npu_power_mode()
return info_dict


class WSLSystemInfo(SystemInfo):
"""Class used to access system information in WSL"""

@staticmethod
def get_system_model() -> str:
"""
Retrieves the model of the computer system.
Returns:
str: Model of the computer system.
"""
try:
oem_info = (
subprocess.check_output(
'powershell.exe -Command "wmic computersystem get model"',
shell=True,
)
.decode()
.strip()
)
oem_info = (
oem_info.replace("\r", "").replace("\n", "").split("Model")[-1].strip()
)
return oem_info
except Exception as e: # pylint: disable=broad-except
return f"ERROR - {e}"

def get_dict(self) -> dict:
"""
Retrieves all the system information into a dictionary
Returns:
dict: System information
"""
info_dict = super().get_dict()
info_dict["OEM System"] = self.get_system_model()
return info_dict


class LinuxSystemInfo(SystemInfo):
"""Class used to access system information in Linux"""

@staticmethod
def get_processor_name() -> str:
"""
Retrieves the name of the processor.
Returns:
str: Name of the processor.
"""
# Get CPU Information
try:
cpu_info = subprocess.check_output("lscpu", shell=True).decode()
for line in cpu_info.split("\n"):
if "Model name:" in line:
return line.split(":")[1].strip()
except Exception as e: # pylint: disable=broad-except
return f"ERROR - {e}"

@staticmethod
def get_system_model() -> str:
"""
Retrieves the model of the computer system.
Returns:
str: Model of the computer system.
"""
# Get OEM System Information
try:
oem_info = (
subprocess.check_output(
"sudo -n dmidecode -s system-product-name",
shell=True,
stderr=subprocess.DEVNULL,
)
.decode()
.strip()
.replace("\n", " ")
)
return oem_info
except subprocess.CalledProcessError:
# This catches the case where sudo requires a password
return "Unable to get oem info - password required"
except Exception as e: # pylint: disable=broad-except
return f"ERROR - {e}"

@staticmethod
def get_physical_memory() -> str:
"""
Retrieves the physical memory of the computer system.
Returns:
str: Physical memory
"""
try:
mem_info = (
subprocess.check_output("free -m", shell=True)
.decode()
.split("\n")[1]
.split()[1]
)
mem_info_gb = round(int(mem_info) / 1024, 2)
return f"{mem_info_gb} GB"
except Exception as e: # pylint: disable=broad-except
return f"ERROR - {e}"

def get_dict(self) -> dict:
"""
Retrieves all the system information into a dictionary
Returns:
dict: System information
"""
info_dict = super().get_dict()
info_dict["Processor"] = self.get_processor_name()
info_dict["OEM System"] = self.get_system_model()
info_dict["Physical Memory"] = self.get_physical_memory()
return info_dict


class UnsupportedOSSystemInfo(SystemInfo):
"""Class used to access system information in unsupported operating systems"""

def get_dict(self):
"""
Retrieves all the system information into a dictionary
Returns:
dict: System information
"""
info_dict = super().get_dict()
info_dict["Error"] = "UNSUPPORTED OS"
return info_dict


def get_system_info() -> SystemInfo:
"""
Creates the appropriate SystemInfo object based on the operating system.
Returns:
A subclass of SystemInfo for the current operating system.
"""
os_type = platform.system()
if os_type == "Windows":
return WindowsSystemInfo()
elif os_type == "Linux":
# WSL has to be handled differently compared to native Linux
if "microsoft" in str(platform.release()):
return WSLSystemInfo()
else:
return LinuxSystemInfo()
else:
return UnsupportedOSSystemInfo()


def get_system_info_dict() -> dict:
"""
Puts the system information into a dictionary.
Returns:
dict: Dictionary containing the system information.
"""
return get_system_info().get_dict()
3 changes: 2 additions & 1 deletion src/turnkeyml/llm/cli.py
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@
import turnkeyml.common.filesystem as fs
import turnkeyml.cli.cli as cli
from turnkeyml.sequence import Sequence
from turnkeyml.tools.management_tools import Cache, Version
from turnkeyml.tools.management_tools import Cache, Version, SystemInfo
from turnkeyml.tools.report import Report
from turnkeyml.state import State

@@ -40,6 +40,7 @@ def main():
Report,
Cache,
Version,
SystemInfo,
]

# Import onnxruntime-genai recipes
3 changes: 2 additions & 1 deletion src/turnkeyml/sequence/sequence.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
import turnkeyml.common.printing as printing
import turnkeyml.common.exceptions as exp
import turnkeyml.common.build as build
from turnkeyml.common.system_info import get_system_info_dict
import turnkeyml.common.filesystem as fs
import turnkeyml.common.status as status
from turnkeyml.tools.tool import Tool
@@ -133,7 +134,7 @@ def launch(
)

# Save the system information used for this build
system_info = build.get_system_info()
system_info = get_system_info_dict()
state.save_stat(
fs.Keys.SYSTEM_INFO,
system_info,
1 change: 1 addition & 0 deletions src/turnkeyml/sequence/tool_plugins.py
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ def get_supported_tools():
mgmt.Version,
mgmt.Cache,
mgmt.ModelsLocation,
mgmt.SystemInfo,
report.Report,
Discover,
export.ExportPytorchModel,
35 changes: 35 additions & 0 deletions src/turnkeyml/tools/management_tools.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
import turnkeyml.common.printing as printing
from turnkeyml.tools.tool import ToolParser
from turnkeyml.version import __version__ as turnkey_version
from turnkeyml.common.system_info import get_system_info_dict


class ManagementTool(abc.ABC):
@@ -265,3 +266,37 @@ def run(self, _, quiet: bool = False):
print(fs.MODELS_DIR)
else:
printing.log_info(f"The models directory is: {fs.MODELS_DIR}")


class SystemInfo(ManagementTool):
"""
Prints system information for the turnkeyml installation.
"""

unique_name = "system-info"

@staticmethod
def parser(add_help: bool = True) -> argparse.ArgumentParser:
parser = __class__.helpful_parser(
short_description="Print system information",
add_help=add_help,
)

return parser

@staticmethod
def pretty_print(my_dict: dict, level=0):
for k, v in my_dict.items():
if isinstance(v, dict):
print(" " * level + f"{k}:")
SystemInfo.pretty_print(v, level + 1)
elif isinstance(v, list):
print(" " * level + f"{k}:")
for item in v:
print(" " * (level + 1) + f"{item}")
else:
print(" " * level + f"{k}: {v}")

def run(self, _):
system_info_dict = get_system_info_dict()
self.pretty_print(system_info_dict)