mainboard 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mainboard/__init__.py +113 -0
- mainboard/__main__.py +2 -0
- mainboard/assets/banner.svg +54 -0
- mainboard/cli.py +24 -0
- mainboard/enums.py +78 -0
- mainboard/gpu.py +178 -0
- mainboard/host.py +133 -0
- mainboard/machine.py +127 -0
- mainboard/models/__init__.py +58 -0
- mainboard/models/base.py +26 -0
- mainboard/models/board.py +58 -0
- mainboard/models/clock.py +20 -0
- mainboard/models/clock_info.py +14 -0
- mainboard/models/compiler_info.py +48 -0
- mainboard/models/compute_capability.py +20 -0
- mainboard/models/cuda_python_info.py +11 -0
- mainboard/models/dimm_card.py +34 -0
- mainboard/models/disk.py +102 -0
- mainboard/models/drive_info.py +72 -0
- mainboard/models/energy_interval.py +34 -0
- mainboard/models/energy_reading.py +19 -0
- mainboard/models/environment.py +72 -0
- mainboard/models/gpu_snapshot.py +28 -0
- mainboard/models/host_disk.py +36 -0
- mainboard/models/host_memory.py +99 -0
- mainboard/models/machine_snapshot.py +68 -0
- mainboard/models/mem_info.py +35 -0
- mainboard/models/memory_card.py +122 -0
- mainboard/models/memory_usage.py +46 -0
- mainboard/models/partition_info.py +86 -0
- mainboard/models/pcie_info.py +14 -0
- mainboard/models/process_info.py +14 -0
- mainboard/models/system_compilers.py +73 -0
- mainboard/models/thermal_state.py +35 -0
- mainboard/models/thermal_tracker.py +36 -0
- mainboard/models/throttle_reason.py +30 -0
- mainboard/models/unit_snapshot.py +38 -0
- mainboard/models/utilization.py +14 -0
- mainboard/npu.py +23 -0
- mainboard/providers/__init__.py +23 -0
- mainboard/providers/amd.py +23 -0
- mainboard/providers/apple.py +176 -0
- mainboard/providers/intel.py +41 -0
- mainboard/providers/nvidia.py +304 -0
- mainboard/providers/qualcomm.py +41 -0
- mainboard/py.typed +0 -0
- mainboard/shell/__init__.py +15 -0
- mainboard/shell/run.py +18 -0
- mainboard/shell/sysctl.py +17 -0
- mainboard/shell/sysfs.py +29 -0
- mainboard/shell/system_profiler.py +25 -0
- mainboard/unit.py +138 -0
- mainboard/visual.py +289 -0
- mainboard-0.0.1.dist-info/METADATA +100 -0
- mainboard-0.0.1.dist-info/RECORD +59 -0
- mainboard-0.0.1.dist-info/WHEEL +5 -0
- mainboard-0.0.1.dist-info/entry_points.txt +2 -0
- mainboard-0.0.1.dist-info/licenses/LICENSE +202 -0
- mainboard-0.0.1.dist-info/top_level.txt +1 -0
mainboard/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .enums import (
|
|
4
|
+
CmakeBuildType,
|
|
5
|
+
CompilerKind,
|
|
6
|
+
CudaPythonVariant,
|
|
7
|
+
DiskKind,
|
|
8
|
+
Scheduler,
|
|
9
|
+
SystemFamily,
|
|
10
|
+
UnitKind,
|
|
11
|
+
Vendor,
|
|
12
|
+
)
|
|
13
|
+
from .gpu import GPU
|
|
14
|
+
from .host import Host
|
|
15
|
+
from .machine import Machine
|
|
16
|
+
from .models import (
|
|
17
|
+
Board,
|
|
18
|
+
Clock,
|
|
19
|
+
ClockInfo,
|
|
20
|
+
CompilerInfo,
|
|
21
|
+
ComputeCapability,
|
|
22
|
+
CpuSnapshot,
|
|
23
|
+
CudaPythonInfo,
|
|
24
|
+
DriveInfo,
|
|
25
|
+
EnergyInterval,
|
|
26
|
+
EnergyReading,
|
|
27
|
+
Environment,
|
|
28
|
+
GPUSnapshot,
|
|
29
|
+
HostDisk,
|
|
30
|
+
HostMemory,
|
|
31
|
+
MachineSnapshot,
|
|
32
|
+
MemInfo,
|
|
33
|
+
MemoryCard,
|
|
34
|
+
MemoryUsage,
|
|
35
|
+
PartitionInfo,
|
|
36
|
+
PcieInfo,
|
|
37
|
+
ProcessInfo,
|
|
38
|
+
SystemCompilers,
|
|
39
|
+
ThermalState,
|
|
40
|
+
ThermalTracker,
|
|
41
|
+
ThrottleReason,
|
|
42
|
+
UnitSnapshot,
|
|
43
|
+
Utilization,
|
|
44
|
+
)
|
|
45
|
+
from .npu import NPU
|
|
46
|
+
from .providers import (
|
|
47
|
+
AMDGPU,
|
|
48
|
+
AppleGPU,
|
|
49
|
+
AppleNPU,
|
|
50
|
+
IntelGPU,
|
|
51
|
+
IntelNPU,
|
|
52
|
+
NvidiaGPU,
|
|
53
|
+
QualcommGPU,
|
|
54
|
+
QualcommNPU,
|
|
55
|
+
)
|
|
56
|
+
from .unit import CPU, Unit
|
|
57
|
+
from .visual import MachineView
|
|
58
|
+
|
|
59
|
+
NAME = __name__ # the package folder is the single source of the name
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"NAME",
|
|
63
|
+
"CPU",
|
|
64
|
+
"GPU",
|
|
65
|
+
"NPU",
|
|
66
|
+
"AMDGPU",
|
|
67
|
+
"AppleGPU",
|
|
68
|
+
"AppleNPU",
|
|
69
|
+
"Board",
|
|
70
|
+
"Clock",
|
|
71
|
+
"ClockInfo",
|
|
72
|
+
"CmakeBuildType",
|
|
73
|
+
"CompilerInfo",
|
|
74
|
+
"CompilerKind",
|
|
75
|
+
"ComputeCapability",
|
|
76
|
+
"CpuSnapshot",
|
|
77
|
+
"CudaPythonInfo",
|
|
78
|
+
"CudaPythonVariant",
|
|
79
|
+
"DiskKind",
|
|
80
|
+
"DriveInfo",
|
|
81
|
+
"EnergyInterval",
|
|
82
|
+
"EnergyReading",
|
|
83
|
+
"Environment",
|
|
84
|
+
"GPUSnapshot",
|
|
85
|
+
"Host",
|
|
86
|
+
"HostDisk",
|
|
87
|
+
"HostMemory",
|
|
88
|
+
"IntelGPU",
|
|
89
|
+
"IntelNPU",
|
|
90
|
+
"Machine",
|
|
91
|
+
"MachineSnapshot",
|
|
92
|
+
"MachineView",
|
|
93
|
+
"MemInfo",
|
|
94
|
+
"MemoryCard",
|
|
95
|
+
"MemoryUsage",
|
|
96
|
+
"NvidiaGPU",
|
|
97
|
+
"PartitionInfo",
|
|
98
|
+
"PcieInfo",
|
|
99
|
+
"ProcessInfo",
|
|
100
|
+
"QualcommGPU",
|
|
101
|
+
"QualcommNPU",
|
|
102
|
+
"Scheduler",
|
|
103
|
+
"SystemCompilers",
|
|
104
|
+
"SystemFamily",
|
|
105
|
+
"ThermalState",
|
|
106
|
+
"ThermalTracker",
|
|
107
|
+
"ThrottleReason",
|
|
108
|
+
"Unit",
|
|
109
|
+
"UnitKind",
|
|
110
|
+
"UnitSnapshot",
|
|
111
|
+
"Utilization",
|
|
112
|
+
"Vendor",
|
|
113
|
+
]
|
mainboard/__main__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="420" viewBox="0 0 1280 420" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">Maquina</title>
|
|
3
|
+
<desc id="desc">CPU, GPU, and NPU hardware topology for Python.</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
|
|
6
|
+
<stop offset="0" stop-color="#020617"/>
|
|
7
|
+
<stop offset="0.46" stop-color="#0f172a"/>
|
|
8
|
+
<stop offset="1" stop-color="#1e3a8a"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
<linearGradient id="unit" x1="0" x2="1" y1="0" y2="1">
|
|
11
|
+
<stop offset="0" stop-color="#38bdf8"/>
|
|
12
|
+
<stop offset="1" stop-color="#2563eb"/>
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
|
15
|
+
<feGaussianBlur stdDeviation="10" result="blur"/>
|
|
16
|
+
<feMerge>
|
|
17
|
+
<feMergeNode in="blur"/>
|
|
18
|
+
<feMergeNode in="SourceGraphic"/>
|
|
19
|
+
</feMerge>
|
|
20
|
+
</filter>
|
|
21
|
+
</defs>
|
|
22
|
+
|
|
23
|
+
<rect width="1280" height="420" rx="42" fill="url(#bg)"/>
|
|
24
|
+
<circle cx="1030" cy="70" r="210" fill="#2563eb" opacity="0.18"/>
|
|
25
|
+
<circle cx="230" cy="380" r="260" fill="#38bdf8" opacity="0.10"/>
|
|
26
|
+
<path d="M0 310 C170 245 270 370 430 310 C610 242 725 160 920 220 C1070 268 1120 160 1280 185 L1280 420 L0 420 Z" fill="#0ea5e9" opacity="0.08"/>
|
|
27
|
+
|
|
28
|
+
<g transform="translate(760 92)" filter="url(#glow)">
|
|
29
|
+
<rect x="0" y="0" width="360" height="220" rx="28" fill="#020617" stroke="#38bdf8" stroke-opacity="0.42" stroke-width="2"/>
|
|
30
|
+
<rect x="38" y="44" width="82" height="58" rx="16" fill="url(#unit)"/>
|
|
31
|
+
<rect x="139" y="44" width="82" height="58" rx="16" fill="url(#unit)" opacity="0.82"/>
|
|
32
|
+
<rect x="240" y="44" width="82" height="58" rx="16" fill="url(#unit)" opacity="0.66"/>
|
|
33
|
+
<rect x="38" y="128" width="284" height="34" rx="17" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.32"/>
|
|
34
|
+
<path d="M72 198 H288" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.55"/>
|
|
35
|
+
<path d="M106 18 V-18 M180 18 V-18 M254 18 V-18 M106 238 V202 M180 238 V202 M254 238 V202" stroke="#38bdf8" stroke-width="8" stroke-linecap="round" opacity="0.34"/>
|
|
36
|
+
<text x="79" y="81" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="22" font-weight="800" fill="#e0f2fe">CPU</text>
|
|
37
|
+
<text x="180" y="81" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="22" font-weight="800" fill="#e0f2fe">GPU</text>
|
|
38
|
+
<text x="281" y="81" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="22" font-weight="800" fill="#e0f2fe">NPU</text>
|
|
39
|
+
</g>
|
|
40
|
+
|
|
41
|
+
<g transform="translate(96 92)">
|
|
42
|
+
<text x="0" y="0" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="86" font-weight="900" fill="#f8fafc" letter-spacing="-3">Maquina</text>
|
|
43
|
+
<text x="3" y="58" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="28" font-weight="600" fill="#bfdbfe">Hardware topology for heterogeneous Python systems.</text>
|
|
44
|
+
<text x="3" y="112" font-family="JetBrains Mono, ui-monospace, SFMono-Regular, Menlo, monospace" font-size="22" fill="#93c5fd">from maquina import Machine</text>
|
|
45
|
+
<g transform="translate(3 154)">
|
|
46
|
+
<rect x="0" y="0" width="88" height="38" rx="19" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.35"/>
|
|
47
|
+
<rect x="104" y="0" width="88" height="38" rx="19" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.35"/>
|
|
48
|
+
<rect x="208" y="0" width="88" height="38" rx="19" fill="#0f172a" stroke="#60a5fa" stroke-opacity="0.35"/>
|
|
49
|
+
<text x="44" y="25" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="800" fill="#dbeafe">CPU</text>
|
|
50
|
+
<text x="148" y="25" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="800" fill="#dbeafe">GPU</text>
|
|
51
|
+
<text x="252" y="25" text-anchor="middle" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="16" font-weight="800" fill="#dbeafe">NPU</text>
|
|
52
|
+
</g>
|
|
53
|
+
</g>
|
|
54
|
+
</svg>
|
mainboard/cli.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cyclopts import App
|
|
4
|
+
|
|
5
|
+
from . import NAME
|
|
6
|
+
from .machine import Machine
|
|
7
|
+
from .visual import MachineView
|
|
8
|
+
|
|
9
|
+
app = App(name=NAME, help="Inspect CPU, GPU, and NPU hardware topology.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.default
|
|
13
|
+
def show(color: bool = True) -> None:
|
|
14
|
+
"""Render a Rich terminal view of the current machine."""
|
|
15
|
+
MachineView(Machine()).print(color=color)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main() -> None:
|
|
19
|
+
"""Run the Mainboard command-line interface."""
|
|
20
|
+
app()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
mainboard/enums.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CompilerKind(StrEnum):
|
|
7
|
+
"""Normalized host compiler kind."""
|
|
8
|
+
|
|
9
|
+
CLANG_GRACE = "clang-grace"
|
|
10
|
+
GCC = "gcc"
|
|
11
|
+
CLANG = "clang"
|
|
12
|
+
NVCC = "nvcc"
|
|
13
|
+
UNKNOWN = "unknown"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SystemFamily(StrEnum):
|
|
17
|
+
"""Machine-family grouping used to guide build choices."""
|
|
18
|
+
|
|
19
|
+
GRACE_HOPPER = "grace-hopper"
|
|
20
|
+
GRACE_BLACKWELL = "grace-blackwell"
|
|
21
|
+
HOPPER = "hopper"
|
|
22
|
+
BLACKWELL = "blackwell"
|
|
23
|
+
GENERIC = "generic"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CudaPythonVariant(StrEnum):
|
|
27
|
+
"""CUDA Python stack family required on the current system."""
|
|
28
|
+
|
|
29
|
+
CU12 = "cu12"
|
|
30
|
+
CU13 = "cu13"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CmakeBuildType(StrEnum):
|
|
34
|
+
"""CMake build type string."""
|
|
35
|
+
|
|
36
|
+
RELEASE = "Release"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DiskKind(StrEnum):
|
|
40
|
+
"""Drive technology for a mounted disk or partition."""
|
|
41
|
+
|
|
42
|
+
NVME = "nvme"
|
|
43
|
+
SSD = "ssd"
|
|
44
|
+
HDD = "hdd"
|
|
45
|
+
RAMDISK = "ramdisk"
|
|
46
|
+
UNKNOWN = "unknown"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UnitKind(StrEnum):
|
|
50
|
+
"""Schedulable execution-resource category."""
|
|
51
|
+
|
|
52
|
+
CPU = "cpu"
|
|
53
|
+
GPU = "gpu"
|
|
54
|
+
NPU = "npu"
|
|
55
|
+
DSP = "dsp"
|
|
56
|
+
MEDIA = "media"
|
|
57
|
+
UNKNOWN = "unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Vendor(StrEnum):
|
|
61
|
+
"""Normalized hardware vendor."""
|
|
62
|
+
|
|
63
|
+
ARM = "arm"
|
|
64
|
+
APPLE = "apple"
|
|
65
|
+
NVIDIA = "nvidia"
|
|
66
|
+
QUALCOMM = "qualcomm"
|
|
67
|
+
AMD = "amd"
|
|
68
|
+
INTEL = "intel"
|
|
69
|
+
UNKNOWN = "unknown"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Scheduler(StrEnum):
|
|
73
|
+
"""Job scheduler detected on the host's PATH."""
|
|
74
|
+
|
|
75
|
+
SLURM = "slurm"
|
|
76
|
+
PBS = "pbs"
|
|
77
|
+
PUEUE = "pueue"
|
|
78
|
+
NONE = "none"
|
mainboard/gpu.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .enums import UnitKind, Vendor
|
|
9
|
+
from .models.clock import Clock
|
|
10
|
+
from .models.clock_info import ClockInfo
|
|
11
|
+
from .models.energy_reading import EnergyReading
|
|
12
|
+
from .models.gpu_snapshot import GPUSnapshot
|
|
13
|
+
from .models.mem_info import MemInfo
|
|
14
|
+
from .models.memory_usage import MemoryUsage
|
|
15
|
+
from .models.pcie_info import PcieInfo
|
|
16
|
+
from .models.thermal_state import ThermalState
|
|
17
|
+
from .models.utilization import Utilization
|
|
18
|
+
from .unit import Unit
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .models.process_info import ProcessInfo
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GPU(Unit):
|
|
25
|
+
"""GPU with telemetry and legacy profiling sensor accessors."""
|
|
26
|
+
|
|
27
|
+
providers: ClassVar[tuple[type[GPU], ...]] = ()
|
|
28
|
+
|
|
29
|
+
index: int = 0
|
|
30
|
+
kind: ClassVar[UnitKind] = UnitKind.GPU
|
|
31
|
+
vendor: Vendor = Field(default=Vendor.UNKNOWN)
|
|
32
|
+
backend: str = "none"
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def register_providers(cls, *providers: type[GPU]) -> None:
|
|
36
|
+
"""Register concrete providers used by `GPU.all`."""
|
|
37
|
+
cls.providers = providers
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def all(cls) -> tuple[GPU, ...]:
|
|
41
|
+
"""Return GPUs visible to supported providers."""
|
|
42
|
+
return tuple(gpu for provider in cls.providers for gpu in provider.all())
|
|
43
|
+
|
|
44
|
+
@cached_property
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
"""Human-readable GPU name."""
|
|
47
|
+
return "unknown"
|
|
48
|
+
|
|
49
|
+
@cached_property
|
|
50
|
+
def uuid(self) -> str:
|
|
51
|
+
"""Stable GPU identifier when the provider exposes one."""
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
@cached_property
|
|
55
|
+
def architecture(self) -> str:
|
|
56
|
+
"""Human-readable architecture or generation name."""
|
|
57
|
+
return "unknown"
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def total_memory_bytes(self) -> int:
|
|
61
|
+
"""Total memory managed by this accelerator."""
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
@cached_property
|
|
65
|
+
def peak_bandwidth_gbs(self) -> float:
|
|
66
|
+
"""Theoretical peak memory bandwidth in GB/s when known."""
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
@cached_property
|
|
70
|
+
def driver_version(self) -> tuple[int, int] | None:
|
|
71
|
+
"""Driver or runtime version as `(major, minor)` when known."""
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def mem_info(self) -> MemInfo:
|
|
76
|
+
"""Current accelerator memory state."""
|
|
77
|
+
return MemInfo(total_bytes=self.total_memory_bytes)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def memory_readings(self) -> tuple[MemoryUsage, ...]:
|
|
81
|
+
"""Memory regions visible to this GPU."""
|
|
82
|
+
mem = self.mem_info
|
|
83
|
+
return (
|
|
84
|
+
MemoryUsage(
|
|
85
|
+
scope="device",
|
|
86
|
+
total_bytes=mem.total_bytes,
|
|
87
|
+
used_bytes=mem.used_bytes,
|
|
88
|
+
free_bytes=mem.free_bytes,
|
|
89
|
+
source=self.backend,
|
|
90
|
+
supported=mem.total_bytes > 0,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def clocks(self) -> ClockInfo:
|
|
96
|
+
"""Current compute and memory clocks."""
|
|
97
|
+
return ClockInfo()
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def clock_readings(self) -> tuple[Clock, ...]:
|
|
101
|
+
"""Clock readings grouped by hardware domain."""
|
|
102
|
+
clocks = self.clocks
|
|
103
|
+
return (
|
|
104
|
+
Clock(
|
|
105
|
+
domain="gpu_compute",
|
|
106
|
+
current_mhz=clocks.sm_mhz or None,
|
|
107
|
+
source=self.backend,
|
|
108
|
+
supported=clocks.sm_mhz > 0,
|
|
109
|
+
),
|
|
110
|
+
Clock(
|
|
111
|
+
domain="memory",
|
|
112
|
+
current_mhz=clocks.memory_mhz or None,
|
|
113
|
+
source=self.backend,
|
|
114
|
+
supported=clocks.memory_mhz > 0,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def utilization(self) -> Utilization:
|
|
120
|
+
"""Current compute and memory-controller utilization."""
|
|
121
|
+
return Utilization()
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def thermal(self) -> ThermalState:
|
|
125
|
+
"""Current thermal state."""
|
|
126
|
+
return ThermalState()
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def energy(self) -> EnergyReading:
|
|
130
|
+
"""Current power and cumulative energy reading."""
|
|
131
|
+
return EnergyReading()
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def pcie(self) -> PcieInfo:
|
|
135
|
+
"""Current host interconnect throughput."""
|
|
136
|
+
return PcieInfo()
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def fan_speed_pct(self) -> int:
|
|
140
|
+
"""Fan speed percentage, or zero when unavailable."""
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def temperature_c(self) -> int:
|
|
145
|
+
"""Current GPU temperature in Celsius."""
|
|
146
|
+
return self.thermal.temperature_c
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def gpu_util_pct(self) -> int:
|
|
150
|
+
"""Current compute utilization percentage."""
|
|
151
|
+
return self.utilization.gpu_pct
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def processes(self) -> list[ProcessInfo]:
|
|
155
|
+
"""Processes using this accelerator."""
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
def snapshot(self, name: str = "") -> GPUSnapshot:
|
|
159
|
+
"""Point-in-time reading of all common sensor properties.
|
|
160
|
+
|
|
161
|
+
name: profiling region label to embed in the snapshot.
|
|
162
|
+
"""
|
|
163
|
+
return GPUSnapshot(
|
|
164
|
+
name=name,
|
|
165
|
+
unit_name=self.name,
|
|
166
|
+
kind=self.kind,
|
|
167
|
+
vendor=self.vendor,
|
|
168
|
+
memory=self.memory_readings,
|
|
169
|
+
clocks=self.clock_readings,
|
|
170
|
+
utilization=self.utilization,
|
|
171
|
+
energy=self.energy,
|
|
172
|
+
thermal=self.thermal,
|
|
173
|
+
gpu_memory=self.mem_info,
|
|
174
|
+
gpu_clocks=self.clocks,
|
|
175
|
+
pcie=self.pcie,
|
|
176
|
+
fan_speed_pct=self.fan_speed_pct,
|
|
177
|
+
processes=self.processes,
|
|
178
|
+
)
|
mainboard/host.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
import re
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
|
|
8
|
+
import psutil
|
|
9
|
+
|
|
10
|
+
from . import shell
|
|
11
|
+
from .enums import Vendor
|
|
12
|
+
from .models.host_disk import HostDisk
|
|
13
|
+
from .models.host_memory import HostMemory
|
|
14
|
+
|
|
15
|
+
_CPU_MODEL_RE = re.compile(r"^(?:model name|hardware)\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE)
|
|
16
|
+
_CPU_IMPLEMENTER_RE = re.compile(r"^CPU implementer\s*:\s*(.+)$", re.MULTILINE)
|
|
17
|
+
_CPU_PART_RE = re.compile(r"^CPU part\s*:\s*(.+)$", re.MULTILINE)
|
|
18
|
+
_ARM_IMPLEMENTERS = {
|
|
19
|
+
"0x41": "Arm",
|
|
20
|
+
"0x4e": "NVIDIA",
|
|
21
|
+
"0x51": "Qualcomm",
|
|
22
|
+
"0x61": "Apple",
|
|
23
|
+
"0xc0": "Ampere",
|
|
24
|
+
}
|
|
25
|
+
_VENDOR_BY_IMPLEMENTER = {
|
|
26
|
+
"0x41": Vendor.ARM,
|
|
27
|
+
"0x4e": Vendor.NVIDIA,
|
|
28
|
+
"0x51": Vendor.QUALCOMM,
|
|
29
|
+
"0x61": Vendor.APPLE,
|
|
30
|
+
}
|
|
31
|
+
_ARM_PARTS = {
|
|
32
|
+
("0x41", "0xd85"): "Cortex-X925",
|
|
33
|
+
("0x41", "0xd87"): "Cortex-A725",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Host:
|
|
38
|
+
"""Host CPU, memory, and disk identity for the current machine."""
|
|
39
|
+
|
|
40
|
+
@cached_property
|
|
41
|
+
def arch(self) -> str:
|
|
42
|
+
"""CPU architecture string, e.g. `x86_64` or `aarch64`."""
|
|
43
|
+
return platform.machine().lower()
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def cpu(self) -> str:
|
|
47
|
+
"""CPU model name from `platform` or `/proc/cpuinfo`."""
|
|
48
|
+
if platform.system() == "Darwin" and (brand := shell.sysctl("machdep.cpu.brand_string")):
|
|
49
|
+
return brand
|
|
50
|
+
text = self.cpuinfo_text
|
|
51
|
+
if not text:
|
|
52
|
+
return platform.processor() or "unknown"
|
|
53
|
+
if m := _CPU_MODEL_RE.search(text):
|
|
54
|
+
return m.group(1).strip()
|
|
55
|
+
if name := self.arm_cpu_name:
|
|
56
|
+
return name
|
|
57
|
+
return platform.processor() or "unknown"
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def cpu_vendor(self) -> Vendor:
|
|
61
|
+
"""CPU core vendor inferred from OS identity records."""
|
|
62
|
+
if platform.system() == "Darwin":
|
|
63
|
+
return Vendor.APPLE
|
|
64
|
+
for record in self.cpuinfo_records:
|
|
65
|
+
implementer = record.get("CPU implementer", "").lower()
|
|
66
|
+
if vendor := _VENDOR_BY_IMPLEMENTER.get(implementer):
|
|
67
|
+
return vendor
|
|
68
|
+
return Vendor.UNKNOWN
|
|
69
|
+
|
|
70
|
+
@cached_property
|
|
71
|
+
def cpuinfo_text(self) -> str:
|
|
72
|
+
"""Raw Linux `/proc/cpuinfo`, or an empty string when unavailable."""
|
|
73
|
+
return shell.read("/proc/cpuinfo")
|
|
74
|
+
|
|
75
|
+
@cached_property
|
|
76
|
+
def cpuinfo_records(self) -> tuple[dict[str, str], ...]:
|
|
77
|
+
"""Parsed Linux `/proc/cpuinfo` records."""
|
|
78
|
+
return tuple(
|
|
79
|
+
{
|
|
80
|
+
key.strip(): value.strip()
|
|
81
|
+
for line in block.splitlines()
|
|
82
|
+
if ":" in line
|
|
83
|
+
for key, value in [line.split(":", 1)]
|
|
84
|
+
}
|
|
85
|
+
for block in self.cpuinfo_text.strip().split("\n\n")
|
|
86
|
+
if block.strip()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@cached_property
|
|
90
|
+
def arm_cpu_name(self) -> str:
|
|
91
|
+
"""Human-readable ARM CPU core mix from MIDR implementer and part IDs."""
|
|
92
|
+
pairs = [
|
|
93
|
+
(record.get("CPU implementer", "").lower(), record.get("CPU part", "").lower())
|
|
94
|
+
for record in self.cpuinfo_records
|
|
95
|
+
if record.get("CPU implementer") and record.get("CPU part")
|
|
96
|
+
]
|
|
97
|
+
counts = Counter(pairs)
|
|
98
|
+
names = []
|
|
99
|
+
for implementer, part in dict.fromkeys(pairs):
|
|
100
|
+
vendor = _ARM_IMPLEMENTERS.get(implementer, f"implementer {implementer}")
|
|
101
|
+
model = _ARM_PARTS.get((implementer, part), f"part {part}")
|
|
102
|
+
count = counts[(implementer, part)]
|
|
103
|
+
names.append(f"{count}x {vendor} {model}" if count > 1 else f"{vendor} {model}")
|
|
104
|
+
return " + ".join(names)
|
|
105
|
+
|
|
106
|
+
@cached_property
|
|
107
|
+
def logical_cpus(self) -> int:
|
|
108
|
+
"""Number of logical CPU threads (including hyperthreading)."""
|
|
109
|
+
return psutil.cpu_count(logical=True) or 0
|
|
110
|
+
|
|
111
|
+
@cached_property
|
|
112
|
+
def physical_cpus(self) -> int:
|
|
113
|
+
"""Number of physical CPU cores."""
|
|
114
|
+
return psutil.cpu_count(logical=False) or 0
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def cpu_freq_mhz(self) -> float | None:
|
|
118
|
+
"""Current CPU frequency in MHz; `None` if the platform cannot report it."""
|
|
119
|
+
try:
|
|
120
|
+
freq = psutil.cpu_freq()
|
|
121
|
+
except (AttributeError, NotImplementedError, OSError):
|
|
122
|
+
return None
|
|
123
|
+
return freq.current if freq else None
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def memory(self) -> HostMemory:
|
|
127
|
+
"""Live system memory snapshot; lazily samples psutil on first field access."""
|
|
128
|
+
return HostMemory()
|
|
129
|
+
|
|
130
|
+
@cached_property
|
|
131
|
+
def disk(self) -> HostDisk:
|
|
132
|
+
"""All physical drives with their mounted partitions."""
|
|
133
|
+
return HostDisk()
|