| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- #!/usr/bin/env node
- const { execSync } = require('child_process');
- const fs = require('fs');
- const path = require('path');
- const DEFAULT_CONFIG_PATH = '/etc/amd-gpu-temp-control.json';
- const DEFAULT_MIN_TEMP = 70;
- const DEFAULT_MAX_TEMP = 80;
- const DEFAULT_MIN_TDP_PERCENT = 10;
- const DEFAULT_MAX_TDP_PERCENT = 100;
- const DEFAULT_TDP_STEP_PERCENT = 10;
- const DEFAULT_CHECK_INTERVAL_MS = 2000;
- const DEFAULT_CMD_TIMEOUT_MS = 5000;
- const DEFAULT_UPP_PATH = 'upp';
- function loadConfig(configPath) {
- if (!fs.existsSync(configPath)) {
- return null;
- }
- const raw = fs.readFileSync(configPath, 'utf-8');
- return JSON.parse(raw);
- }
- function mergeConfig(config) {
- return {
- minTemp: config.minTemp ?? DEFAULT_MIN_TEMP,
- maxTemp: config.maxTemp ?? DEFAULT_MAX_TEMP,
- minTdpPercent: config.minTdpPercent ?? DEFAULT_MIN_TDP_PERCENT,
- maxTdpPercent: config.maxTdpPercent ?? DEFAULT_MAX_TDP_PERCENT,
- tdpStepPercent: config.tdpStepPercent ?? DEFAULT_TDP_STEP_PERCENT,
- checkIntervalMs: config.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS,
- cmdTimeoutMs: config.cmdTimeoutMs ?? DEFAULT_CMD_TIMEOUT_MS,
- uppPath: config.uppPath ?? DEFAULT_UPP_PATH,
- };
- }
- function getCardIndices() {
- const entries = fs.readdirSync('/sys/class/drm');
- const indices = [];
- for (const name of entries) {
- const match = name.match(/^card(\d+)$/);
- if (match) {
- indices.push(parseInt(match[1], 10));
- }
- }
- return indices.length > 0 ? indices : [0];
- }
- function findHwmonDirs(cardNumber) {
- const hwmonDirs = [];
- const baseDir = `/sys/class/drm/card${cardNumber}/device/hwmon`;
- if (fs.existsSync(baseDir)) {
- for (const entry of fs.readdirSync(baseDir)) {
- if (entry.startsWith('hwmon')) {
- hwmonDirs.push(path.join(baseDir, entry));
- }
- }
- }
- const altBase = `/sys/class/drm/card${cardNumber}/device`;
- if (fs.existsSync(altBase)) {
- for (const entry of fs.readdirSync(altBase)) {
- if (entry.startsWith('hwmon')) {
- const dirPath = path.join(altBase, entry);
- if (!hwmonDirs.includes(dirPath)) {
- hwmonDirs.push(dirPath);
- }
- }
- }
- }
- return hwmonDirs;
- }
- function getTemperature(cardNumber) {
- const hwmonDirs = findHwmonDirs(cardNumber);
- if (hwmonDirs.length === 0) return null;
- let maxTemp = null;
- for (const hwmonDir of hwmonDirs) {
- try {
- const entries = fs.readdirSync(hwmonDir);
- for (const entry of entries) {
- const match = entry.match(/^temp\d+_input$/);
- if (match) {
- const raw = fs.readFileSync(path.join(hwmonDir, entry), 'utf-8').trim();
- const value = parseInt(raw, 10);
- if (!isNaN(value)) {
- const tempC = value / 1000;
- if (maxTemp === null || tempC > maxTemp) {
- maxTemp = tempC;
- }
- }
- }
- }
- } catch {
- }
- }
- return maxTemp;
- }
- function getDeviceName(cardNumber) {
- try {
- const namePath = `/sys/class/drm/card${cardNumber}/device/name`;
- if (fs.existsSync(namePath)) {
- return fs.readFileSync(namePath, 'utf-8').trim();
- }
- } catch { /* ignore */ }
- try {
- const slotPath = `/sys/class/drm/card${cardNumber}/device/uevent`;
- if (fs.existsSync(slotPath)) {
- const lines = fs.readFileSync(slotPath, 'utf-8').trim().split('\n');
- for (const line of lines) {
- const match = line.match(/^PCI_SLOT_NAME=(.*)$/);
- if (match) {
- const slot = match[1].trim();
- try {
- const out = execSync(`lspci -s ${slot} 2>/dev/null`, { encoding: 'utf-8' }).trim();
- if (out) return out.split(/\s{3,}/).pop().trim();
- } catch { /* ignore */ }
- }
- }
- }
- } catch { /* ignore */ }
- return `card${cardNumber}`;
- }
- function getCardPath(cardNumber) {
- return `/sys/class/drm/card${cardNumber}/device/pp_table`;
- }
- function setTdpForCard(cardNumber, power, config) {
- const cardPath = getCardPath(cardNumber);
- const cmd = `${config.uppPath} -p ${cardPath} set --write ` +
- `SmallPowerLimit1=${power} ` +
- `SmallPowerLimit2=${power} ` +
- `BoostPowerLimit=${power} ` +
- `smcPPTable/SocketPowerLimitAc0=${power} ` +
- `smcPPTable/SocketPowerLimitDc=${power}`;
- execSync(cmd, { encoding: 'utf-8', stdio: 'pipe', timeout: config.cmdTimeoutMs });
- }
- function parseArgs(argv) {
- let configPath = null;
- for (let i = 2; i < argv.length; i++) {
- switch (argv[i]) {
- case '--config':
- case '-c':
- configPath = argv[++i];
- break;
- case '--help':
- case '-h':
- console.log('Usage: node amd-gpu-temp-control.js [options]');
- console.log('');
- console.log('Options:');
- console.log(' --config, -c <path> Config file path (default: /etc/amd-gpu-temp-control.json)');
- console.log(' --help, -h Show this help message');
- process.exit(0);
- break;
- }
- }
- return configPath || DEFAULT_CONFIG_PATH;
- }
- function calculateTargetTdpPercent(temp, minTemp, maxTemp, minTdpPercent, maxTdpPercent) {
- if (temp <= minTemp) return maxTdpPercent;
- if (temp >= maxTemp) return minTdpPercent;
- const ratio = (temp - minTemp) / (maxTemp - minTemp);
- return Math.round(maxTdpPercent - ratio * (maxTdpPercent - minTdpPercent));
- }
- function wattsFromPercent(percent, maxTdp) {
- return Math.round((percent / 100) * maxTdp);
- }
- function log(message) {
- const timestamp = new Date().toISOString();
- console.log(`[${timestamp}] ${message}`);
- }
- async function main() {
- const configPath = parseArgs(process.argv);
- const rawConfig = loadConfig(configPath);
- const config = mergeConfig(rawConfig || {});
- const cardIndices = getCardIndices();
- log('AMD GPU Temperature Control');
- log(`Temperature range: ${config.minTemp}°C - ${config.maxTemp}°C`);
- log(`TDP range: ${config.minTdpPercent}% - ${config.maxTdpPercent}%`);
- log(`TDP step: ${config.tdpStepPercent}%`);
- log(`Check interval: ${config.checkIntervalMs}ms`);
- log(`Upp path: ${config.uppPath}`);
- log(`Config: ${configPath}`);
- log(`Monitoring ${cardIndices.length} GPU(s)`);
- log('Press Ctrl+C to stop');
- log('');
- const gpuStates = cardIndices.map(cardNumber => ({
- cardNumber,
- deviceName: getDeviceName(cardNumber),
- currentTdpPercent: null,
- }));
- if (gpuStates.length === 0) {
- log('No GPUs found');
- process.exit(1);
- }
- let iteration = 0;
- const interval = setInterval(() => {
- iteration++;
- for (const gpuState of gpuStates) {
- try {
- const temp = getTemperature(gpuState.cardNumber);
- if (temp === null) {
- log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: Cannot read temperature`);
- continue;
- }
- const inRange = temp >= config.minTemp && temp <= config.maxTemp;
- if (!inRange) {
- const targetTdpPercent = calculateTargetTdpPercent(
- temp,
- config.minTemp,
- config.maxTemp,
- config.minTdpPercent,
- config.maxTdpPercent
- );
- let shouldApply = false;
- if (gpuState.currentTdpPercent === null) {
- gpuState.currentTdpPercent = targetTdpPercent;
- shouldApply = true;
- } else if (targetTdpPercent !== gpuState.currentTdpPercent) {
- const diff = targetTdpPercent - gpuState.currentTdpPercent;
- if (Math.abs(diff) >= config.tdpStepPercent) {
- gpuState.currentTdpPercent += Math.sign(diff) * config.tdpStepPercent;
- } else {
- gpuState.currentTdpPercent = targetTdpPercent;
- }
- shouldApply = true;
- }
- if (shouldApply) {
- const clampedPercent = Math.max(config.minTdpPercent, Math.min(config.maxTdpPercent, gpuState.currentTdpPercent));
- const power = wattsFromPercent(clampedPercent, 250);
- setTdpForCard(gpuState.cardNumber, power, config);
- log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: ${temp.toFixed(1)}°C | TDP: ${clampedPercent}% (${power}W)`);
- }
- } else if (gpuState.currentTdpPercent !== null && iteration % 10 === 0) {
- const currentWatts = wattsFromPercent(gpuState.currentTdpPercent, 250);
- log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: ${temp.toFixed(1)}°C | TDP: ${gpuState.currentTdpPercent}% (${currentWatts}W) (stable)`);
- }
- } catch (err) {
- log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: Error reading temperature - ${err.message}`);
- }
- }
- }, config.checkIntervalMs);
- process.on('SIGINT', () => {
- clearInterval(interval);
- log('');
- log('Stopped.');
- process.exit(0);
- });
- }
- main();
|