import { arch, platform } from "node:os"; import { Effect } from "effect"; import type { AcceleratorInfo, DeviceVendor, TelemetryField } from "../contracts"; import { detectGpuMonitoringTool, getGpuInfo } from "../../system/platform/gpu"; import type { GpuInfo, RuntimeGpuMonitoringTool } from "../../models/types"; import { neverFails, type DeviceProbe } from "./probe"; const MB = 1024 * 1024; const TOOL_VENDORS: Readonly> = { "nvidia-smi": "nvidia", "amd-smi": "amd", "rocm-smi": "amd", "intel-sysfs": "intel", "apple-metal": "apple", }; const ACCELERATOR_BY_VENDOR: Readonly> = { nvidia: "cuda", amd: "rocm", intel: "xpu", apple: "metal", unknown: "cpu", }; /** Stable across reboots. Indices renumber when a card is added or removed, so they are * the last resort and are namespaced by vendor to stay unambiguous. */ const deviceIdFor = (gpu: GpuInfo, vendor: DeviceVendor): string => gpu.uuid ?? gpu.pci_bus_id ?? `${vendor}:${gpu.index}`; /** * `detectGpuMonitoringTool` only knows about the SMI binaries, so it answers null on * Apple Silicon even though `getGpuInfo` does return the SoC's GPU. Falling back to the * host is what keeps a Mac from being classified as a CPU-only box. */ const vendorFor = (tool: RuntimeGpuMonitoringTool | null): DeviceVendor => { if (tool) return TOOL_VENDORS[tool]; if (platform() === "darwin" && arch() === "arm64") return "apple"; return "unknown"; }; /** * The vendor probes already report, per GPU, which counters they could actually read * (`utilization_available`, `temperature_available`, …). Honouring those flags is what * lets the UI distinguish "this platform cannot tell you" from "it is currently zero" — * a 0 with the flag unset is the absence of a reading, not an idle GPU. * * The flags are optional in the contract; absent means "reported", which matches the * NVIDIA path where every counter is genuinely available. */ const available = (flag: boolean | undefined): boolean => flag !== false; const reading = (flag: boolean | undefined, value: number | undefined): number | null => available(flag) && typeof value === "number" && Number.isFinite(value) ? value : null; const toAccelerator = (gpu: GpuInfo, vendor: DeviceVendor): AcceleratorInfo => ({ id: deviceIdFor(gpu, vendor), index: gpu.index, vendor, name: gpu.name, accelerator: ACCELERATOR_BY_VENDOR[vendor], memoryTotalBytes: Math.max(0, gpu.memory_total_mb) * MB, memoryUsedBytes: available(gpu.memory_usage_available) ? Math.max(0, gpu.memory_used_mb) * MB : 0, // Apple Silicon and the Grace/GB10 parts share one pool with the CPU; never budget // their VRAM separately from host RAM. unifiedMemory: gpu.memory_shared === true, utilizationPct: reading(gpu.utilization_available, gpu.utilization_pct), temperatureC: reading(gpu.temperature_available, gpu.temp_c), powerWatts: reading(gpu.power_available, gpu.power_draw), powerLimitWatts: reading(gpu.power_available, gpu.power_limit), driver: null, }); /** Which fields at least one accelerator on this host can actually answer. */ const capabilitiesOf = (accelerators: readonly AcceleratorInfo[]): readonly TelemetryField[] => { if (accelerators.length === 0) return []; const capabilities: TelemetryField[] = ["memory"]; if (accelerators.some((entry) => entry.utilizationPct !== null)) capabilities.push("utilization"); if (accelerators.some((entry) => entry.temperatureC !== null)) capabilities.push("temperature"); if (accelerators.some((entry) => entry.powerWatts !== null)) capabilities.push("power"); return capabilities; }; /** * Every accelerator on this host, via whichever vendor tool is present. NVIDIA (incl. DGX * Spark), AMD, Intel and Apple Silicon all arrive through here — the vendor differences * live in the existing platform probes, and this only normalises their output. */ export const acceleratorProbe: DeviceProbe = { id: "accelerators", detect: () => true, run: () => neverFails( Effect.gen(function* () { const tool = yield* detectGpuMonitoringTool(); const gpus = yield* getGpuInfo(); const vendor = vendorFor(tool); const accelerators = gpus.map((gpu) => toAccelerator(gpu, vendor)); return { fragment: { accelerators }, capabilities: capabilitiesOf(accelerators) }; }), ), };