353 lines · 11.8 KB · TypeScript
RawDownload
// CRITICALimport { existsSync } from "node:fs";import { resolve } from "node:path";import type { GpuInfo } from "../types/models";import { resolveBinary, runCommand } from "./command/command-utilities"; type AmdSmiValue = { value?: number; unit?: string } | "N/A" | null; type AmdSmiMetricGpu = {  gpu?: number;  mem_usage?: {    total_vram?: AmdSmiValue;    used_vram?: AmdSmiValue;    free_vram?: AmdSmiValue;  };  usage?: {    gfx_activity?: AmdSmiValue;  };  temperature?: {    hotspot?: AmdSmiValue;    mem?: AmdSmiValue;    edge?: AmdSmiValue;  };  power?: {    socket_power?: AmdSmiValue;  };}; type AmdSmiStaticGpu = {  gpu?: number;  asic?: {    market_name?: string;    target_graphics_version?: string;  };}; type RocmSmiParsed = {  index: number;  name: string;  memory_total_bytes: number | null;  memory_used_bytes: number | null;  utilization_pct: number | null;  temp_c: number | null;  power_draw_w: number | null;  power_limit_w: number | null;}; const resolveAmdSmiBinary = (): string | null => {  const configured = process.env["AMD_SMI_PATH"] || "amd-smi";  const resolved = resolveBinary(configured);  if (resolved) return resolved;  if (configured.includes("/")) {    const abs = resolve(configured);    return existsSync(abs) ? abs : null;  }  return null;}; const resolveRocmSmiBinary = (): string | null => {  const configured = process.env["ROCM_SMI_PATH"] || "rocm-smi";  const resolved = resolveBinary(configured);  if (resolved) return resolved;  if (configured.includes("/")) {    const abs = resolve(configured);    return existsSync(abs) ? abs : null;  }  return null;}; const isRecord = (value: unknown): value is Record<string, unknown> =>  Boolean(value) && typeof value === "object" && !Array.isArray(value); const coerceNumber = (value: unknown): number | null => {  if (typeof value === "number" && Number.isFinite(value)) {    return value;  }  return null;}; const readAmdSmiValueMb = (value: AmdSmiValue | undefined): number | null => {  if (!value || value === "N/A") {    return null;  }  if (!isRecord(value)) {    return null;  }  const unit = typeof value["unit"] === "string" ? value["unit"] : "";  const raw = coerceNumber(value["value"]);  if (raw === null) {    return null;  }  if (!unit || unit.toLowerCase() === "mb") {    return raw;  }  if (unit.toLowerCase() === "gb") {    return raw * 1024;  }  return raw;}; const readAmdSmiValueNumber = (value: AmdSmiValue | undefined): number | null => {  if (!value || value === "N/A") return null;  if (!isRecord(value)) return null;  return coerceNumber(value["value"]);}; /** * Parse amd-smi metric output JSON. * @param jsonText - JSON string from `amd-smi metric --json`. * @returns Parsed metric entries. */export const parseAmdSmiMetricJson = (jsonText: string): AmdSmiMetricGpu[] => {  try {    const parsed = JSON.parse(jsonText) as unknown;    if (!isRecord(parsed)) return [];    const gpuData = parsed["gpu_data"];    if (!Array.isArray(gpuData)) return [];    return gpuData.filter((entry) => isRecord(entry)) as AmdSmiMetricGpu[];  } catch {    return [];  }}; /** * Parse amd-smi static output JSON. * @param jsonText - JSON string from `amd-smi static --json`. * @returns Parsed static entries. */export const parseAmdSmiStaticJson = (jsonText: string): AmdSmiStaticGpu[] => {  try {    const parsed = JSON.parse(jsonText) as unknown;    if (!isRecord(parsed)) return [];    const gpuData = parsed["gpu_data"];    if (!Array.isArray(gpuData)) return [];    return gpuData.filter((entry) => isRecord(entry)) as AmdSmiStaticGpu[];  } catch {    return [];  }}; const parseRocmSmiValue = (raw: string): { value: number; unit: string } | null => {  const cleaned = raw.trim();  if (!cleaned) return null;  const match = cleaned.match(/^([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z%]+)?$/);  if (!match) return null;  const value = Number(match[1]);  if (!Number.isFinite(value)) return null;  return { value, unit: (match[2] ?? "").trim() };}; const rocmSmiToBytes = (parsed: { value: number; unit: string } | null): number | null => {  if (!parsed) return null;  const unit = parsed.unit.toLowerCase();  if (!unit || unit === "b") return Math.round(parsed.value);  if (unit === "kb" || unit === "kib") return Math.round(parsed.value * 1024);  if (unit === "mb" || unit === "mib") return Math.round(parsed.value * 1024 ** 2);  if (unit === "gb" || unit === "gib") return Math.round(parsed.value * 1024 ** 3);  if (unit === "tb" || unit === "tib") return Math.round(parsed.value * 1024 ** 4);  return null;}; /** * Parse roc-smi output text into a normalized structure. * @param text - Combined stdout/stderr from `rocm-smi`. * @returns Parsed GPU records. */export const parseRocmSmiText = (text: string): RocmSmiParsed[] => {  const byIndex = new Map<number, Partial<RocmSmiParsed>>();  const lines = text.split("\n").map((line) => line.trim()).filter(Boolean);   for (const line of lines) {    const match = line.match(/GPU\[(\d+)\]\s*:\s*([^:]+?)\s*:\s*(.*)$/i);    if (!match) continue;    const index = Number(match[1]);    if (!Number.isFinite(index)) continue;    const label = (match[2] ?? "").trim().toLowerCase();    const valueText = (match[3] ?? "").trim();     const existing = byIndex.get(index) ?? { index, name: "AMD GPU" };    const entry = { ...existing, index, name: existing.name ?? "AMD GPU" };     if (label.includes("card model")) {      if (valueText) entry.name = valueText;    } else if (label.includes("card series") && entry.name === "AMD GPU") {      if (valueText) entry.name = valueText;    } else if (label.includes("total vram")) {      entry.memory_total_bytes = rocmSmiToBytes(parseRocmSmiValue(valueText));    } else if (label.includes("used vram")) {      entry.memory_used_bytes = rocmSmiToBytes(parseRocmSmiValue(valueText));    } else if (label.includes("gpu use")) {      const parsed = parseRocmSmiValue(valueText.replace("%", "").trim());      entry.utilization_pct = parsed ? parsed.value : null;    } else if (label.includes("temperature") && label.includes("(c)")) {      const parsed = parseRocmSmiValue(valueText.replace(/c$/i, "").trim());      entry.temp_c = parsed ? parsed.value : null;    } else if (label.includes("average") && label.includes("power") && label.includes("(w)")) {      const parsed = parseRocmSmiValue(valueText.replace(/w$/i, "").trim());      entry.power_draw_w = parsed ? parsed.value : null;    } else if ((label.includes("power cap") || label.includes("max")) && label.includes("(w)")) {      const parsed = parseRocmSmiValue(valueText.replace(/w$/i, "").trim());      entry.power_limit_w = parsed ? parsed.value : null;    }     byIndex.set(index, entry);  }   return Array.from(byIndex.values())    .map((value) => {      const base: RocmSmiParsed = {        index: typeof value.index === "number" ? value.index : -1,        name: value.name ?? "AMD GPU",        memory_total_bytes: value.memory_total_bytes ?? null,        memory_used_bytes: value.memory_used_bytes ?? null,        utilization_pct: value.utilization_pct ?? null,        temp_c: value.temp_c ?? null,        power_draw_w: value.power_draw_w ?? null,        power_limit_w: value.power_limit_w ?? null,      };      if (base.index < 0) return null;      return base;    })    .filter((value): value is RocmSmiParsed => Boolean(value))    .sort((a, b) => a.index - b.index);}; /** * Query GPU info from amd-smi JSON output. * @returns GPU info list. */export const getGpuInfoFromAmdSmi = (): GpuInfo[] => {  try {    const amdSmi = resolveAmdSmiBinary();    if (!amdSmi) return [];     const metricResult = runCommand(amdSmi, ["metric", "--json", "-g", "all"], 5_000);    if (metricResult.status !== 0 || !metricResult.stdout) return [];     const staticResult = runCommand(amdSmi, ["static", "--json", "-g", "all"], 5_000);    if (staticResult.status !== 0 || !staticResult.stdout) return [];     const metrics = parseAmdSmiMetricJson(metricResult.stdout);    const statics = parseAmdSmiStaticJson(staticResult.stdout);    const staticByGpu = new Map<number, AmdSmiStaticGpu>();    for (const entry of statics) {      const gpuIndex = typeof entry.gpu === "number" ? entry.gpu : null;      if (gpuIndex === null) continue;      staticByGpu.set(gpuIndex, entry);    }     return metrics      .map((metric) => {        const index = typeof metric.gpu === "number" ? metric.gpu : null;        if (index === null) return null;        const staticEntry = staticByGpu.get(index) ?? null;         const name = staticEntry?.asic?.market_name ?? "AMD GPU";         const totalMb = readAmdSmiValueMb(metric.mem_usage?.total_vram) ?? 0;        const usedMb = readAmdSmiValueMb(metric.mem_usage?.used_vram) ?? 0;        const freeMb =          readAmdSmiValueMb(metric.mem_usage?.free_vram) ?? Math.max(0, totalMb - usedMb);         const toBytes = (mb: number): number => Math.max(0, Math.round(mb * 1024 * 1024));         const utilization = Math.max(0, Math.round(readAmdSmiValueNumber(metric.usage?.gfx_activity) ?? 0));        const temperature =          Math.max(            0,            Math.round(              readAmdSmiValueNumber(metric.temperature?.hotspot) ??                readAmdSmiValueNumber(metric.temperature?.edge) ??                0,            ),          );        const powerDraw = Math.max(0, Number(readAmdSmiValueNumber(metric.power?.socket_power) ?? 0));         return {          index,          name,          memory_total: toBytes(totalMb),          memory_total_mb: Math.max(0, Math.round(totalMb)),          memory_used: toBytes(usedMb),          memory_used_mb: Math.max(0, Math.round(usedMb)),          memory_free: toBytes(freeMb),          memory_free_mb: Math.max(0, Math.round(freeMb)),          utilization,          utilization_pct: utilization,          temperature,          temp_c: temperature,          power_draw: powerDraw,          power_limit: 0,        } satisfies GpuInfo;      })      .filter((entry): entry is GpuInfo => Boolean(entry));  } catch {    return [];  }}; /** * Query GPU info from rocm-smi output text. * @returns GPU info list. */export const getGpuInfoFromRocmSmi = (): GpuInfo[] => {  try {    const rocmSmi = resolveRocmSmiBinary();    if (!rocmSmi) return [];     // Prefer explicit flags for stable output; fall back to default output if flags are unsupported.    const args = ["--showproductname", "--showmeminfo", "vram", "--showuse", "--showtemp", "--showpower"];    let result = runCommand(rocmSmi, args, 5_000);    if (result.status !== 0) {      result = runCommand(rocmSmi, [], 5_000);    }     const combined = [result.stdout, result.stderr].filter(Boolean).join("\n");    if (!combined.trim()) return [];     const parsed = parseRocmSmiText(combined);    if (parsed.length === 0) return [];     const toMb = (bytes: number): number => Math.max(0, Math.round(bytes / 1024 ** 2));    return parsed.map((gpu) => {      const totalBytes = gpu.memory_total_bytes ?? 0;      const usedBytes = gpu.memory_used_bytes ?? 0;      const freeBytes = Math.max(0, totalBytes - usedBytes);      const utilization = Math.max(0, Math.round(gpu.utilization_pct ?? 0));      const temperatureC = Math.max(0, Math.round(gpu.temp_c ?? 0));      const powerDraw = Math.max(0, Number(gpu.power_draw_w ?? 0));      const powerLimit = Math.max(0, Number(gpu.power_limit_w ?? 0));       return {        index: gpu.index,        name: gpu.name || "AMD GPU",        memory_total: totalBytes,        memory_total_mb: toMb(totalBytes),        memory_used: usedBytes,        memory_used_mb: toMb(usedBytes),        memory_free: freeBytes,        memory_free_mb: toMb(freeBytes),        utilization,        utilization_pct: utilization,        temperature: temperatureC,        temp_c: temperatureC,        power_draw: powerDraw,        power_limit: powerLimit,      } satisfies GpuInfo;    });  } catch {    return [];  }};