import type { GpuInfo } from "../../models/types";import { Effect } from "effect";import { runCommandAsyncEffect } from "../../../core/command";import { resolveAmdSmiBinary, resolveRocmSmiBinary } from "./smi-tools"; 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; edge?: AmdSmiValue; }; power?: { socket_power?: AmdSmiValue; };}; type AmdSmiStaticGpu = { gpu?: number; asic?: { market_name?: 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 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"].toLowerCase() : ""; const rawValue = coerceNumber(value["value"]); if (rawValue === null) { return null; } if (!unit || unit === "mb" || unit === "mib") { return rawValue; } if (unit === "gb" || unit === "gib") { return rawValue * 1024; } return rawValue;}; const readAmdSmiValueNumber = (value: AmdSmiValue | undefined): number | null => { if (!value || value === "N/A") return null; if (!isRecord(value)) return null; return coerceNumber(value["value"]);}; 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 []; }}; 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;}; const enrichUnitFromLabel = ( parsed: { value: number; unit: string } | null, label: string,): { value: number; unit: string } | null => { if (!parsed) return null; if (parsed.unit) return parsed; const match = label.match(/\((kib|mib|gib|tib|kb|mb|gb|tb|b)\)/i); if (!match) return parsed; return { ...parsed, unit: match[1] ?? "" };}; 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( enrichUnitFromLabel(parseRocmSmiValue(valueText), label), ); } else if (label.includes("used vram")) { entry.memory_used_bytes = rocmSmiToBytes( enrichUnitFromLabel(parseRocmSmiValue(valueText), label), ); } 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((entry) => { const parsed: RocmSmiParsed = { index: typeof entry.index === "number" ? entry.index : -1, name: entry.name ?? "AMD GPU", memory_total_bytes: entry.memory_total_bytes ?? null, memory_used_bytes: entry.memory_used_bytes ?? null, utilization_pct: entry.utilization_pct ?? null, temp_c: entry.temp_c ?? null, power_draw_w: entry.power_draw_w ?? null, power_limit_w: entry.power_limit_w ?? null, }; return parsed.index >= 0 ? parsed : null; }) .filter((value): value is RocmSmiParsed => Boolean(value)) .sort((a, b) => a.index - b.index);}; export const getGpuInfoFromAmdSmi = (): Effect.Effect<GpuInfo[]> => Effect.gen(function* () { const amdSmi = resolveAmdSmiBinary(); if (!amdSmi) return []; const metricResult = yield* runCommandAsyncEffect(amdSmi, ["metric", "--json", "-g", "all"], { timeoutMs: 5_000, }); if (metricResult.status !== 0 || !metricResult.stdout) return []; const staticResult = yield* runCommandAsyncEffect(amdSmi, ["static", "--json", "-g", "all"], { timeoutMs: 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 index = typeof entry.gpu === "number" ? entry.gpu : null; if (index !== null) { staticByGpu.set(index, 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 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_mb: Math.max(0, Math.round(totalMb)), memory_used_mb: Math.max(0, Math.round(usedMb)), memory_free_mb: Math.max(0, Math.round(freeMb)), utilization_pct: utilization, temp_c: temperature, power_draw: powerDraw, power_limit: 0, } satisfies GpuInfo; }) .filter((entry): entry is GpuInfo => Boolean(entry)); }); export const getGpuInfoFromRocmSmi = (): Effect.Effect<GpuInfo[]> => Effect.gen(function* () { const rocmSmi = resolveRocmSmiBinary(); if (!rocmSmi) return []; const args = [ "--showproductname", "--showmeminfo", "vram", "--showuse", "--showtemp", "--showpower", ]; let result = yield* runCommandAsyncEffect(rocmSmi, args, { timeoutMs: 5_000 }); if (result.status !== 0) { result = yield* runCommandAsyncEffect(rocmSmi, [], { timeoutMs: 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_mb: toMb(totalBytes), memory_used_mb: toMb(usedBytes), memory_free_mb: toMb(freeBytes), utilization_pct: utilization, temp_c: temperatureC, power_draw: powerDraw, power_limit: powerLimit, } satisfies GpuInfo; }); });
import type { GpuInfo } from "../../models/types";
import { Effect } from "effect";
import { runCommandAsyncEffect } from "../../../core/command";
import { resolveAmdSmiBinary, resolveRocmSmiBinary } from "./smi-tools";
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;
edge?: AmdSmiValue;
};
power?: {
socket_power?: AmdSmiValue;
};
};
type AmdSmiStaticGpu = {
gpu?: number;
asic?: {
market_name?: 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 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"].toLowerCase() : "";
const rawValue = coerceNumber(value["value"]);
if (rawValue === null) {
return null;
}
if (!unit || unit === "mb" || unit === "mib") {
return rawValue;
}
if (unit === "gb" || unit === "gib") {
return rawValue * 1024;
}
return rawValue;
};
const readAmdSmiValueNumber = (value: AmdSmiValue | undefined): number | null => {
if (!value || value === "N/A") return null;
if (!isRecord(value)) return null;
return coerceNumber(value["value"]);
};
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 [];
}
};
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;
};
const enrichUnitFromLabel = (
parsed: { value: number; unit: string } | null,
label: string,
): { value: number; unit: string } | null => {
if (!parsed) return null;
if (parsed.unit) return parsed;
const match = label.match(/\((kib|mib|gib|tib|kb|mb|gb|tb|b)\)/i);
if (!match) return parsed;
return { ...parsed, unit: match[1] ?? "" };
};
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(
enrichUnitFromLabel(parseRocmSmiValue(valueText), label),
);
} else if (label.includes("used vram")) {
entry.memory_used_bytes = rocmSmiToBytes(
enrichUnitFromLabel(parseRocmSmiValue(valueText), label),
);
} 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((entry) => {
const parsed: RocmSmiParsed = {
index: typeof entry.index === "number" ? entry.index : -1,
name: entry.name ?? "AMD GPU",
memory_total_bytes: entry.memory_total_bytes ?? null,
memory_used_bytes: entry.memory_used_bytes ?? null,
utilization_pct: entry.utilization_pct ?? null,
temp_c: entry.temp_c ?? null,
power_draw_w: entry.power_draw_w ?? null,
power_limit_w: entry.power_limit_w ?? null,
};
return parsed.index >= 0 ? parsed : null;
})
.filter((value): value is RocmSmiParsed => Boolean(value))
.sort((a, b) => a.index - b.index);
};
export const getGpuInfoFromAmdSmi = (): Effect.Effect<GpuInfo[]> =>
Effect.gen(function* () {
const amdSmi = resolveAmdSmiBinary();
if (!amdSmi) return [];
const metricResult = yield* runCommandAsyncEffect(amdSmi, ["metric", "--json", "-g", "all"], {
timeoutMs: 5_000,
});
if (metricResult.status !== 0 || !metricResult.stdout) return [];
const staticResult = yield* runCommandAsyncEffect(amdSmi, ["static", "--json", "-g", "all"], {
timeoutMs: 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 index = typeof entry.gpu === "number" ? entry.gpu : null;
if (index !== null) {
staticByGpu.set(index, 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 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_mb: Math.max(0, Math.round(totalMb)),
memory_used_mb: Math.max(0, Math.round(usedMb)),
memory_free_mb: Math.max(0, Math.round(freeMb)),
utilization_pct: utilization,
temp_c: temperature,
power_draw: powerDraw,
power_limit: 0,
} satisfies GpuInfo;
})
.filter((entry): entry is GpuInfo => Boolean(entry));
});
export const getGpuInfoFromRocmSmi = (): Effect.Effect<GpuInfo[]> =>
Effect.gen(function* () {
const rocmSmi = resolveRocmSmiBinary();
if (!rocmSmi) return [];
const args = [
"--showproductname",
"--showmeminfo",
"vram",
"--showuse",
"--showtemp",
"--showpower",
];
let result = yield* runCommandAsyncEffect(rocmSmi, args, { timeoutMs: 5_000 });
if (result.status !== 0) {
result = yield* runCommandAsyncEffect(rocmSmi, [], { timeoutMs: 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_mb: toMb(totalBytes),
memory_used_mb: toMb(usedBytes),
memory_free_mb: toMb(freeBytes),
utilization_pct: utilization,
temp_c: temperatureC,
power_draw: powerDraw,
power_limit: powerLimit,
} satisfies GpuInfo;
});
});