amd-gpu-temp-control.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. #!/usr/bin/env node
  2. const { execSync } = require('child_process');
  3. const fs = require('fs');
  4. const path = require('path');
  5. const DEFAULT_CONFIG_PATH = '/etc/amd-gpu-temp-control.json';
  6. const DEFAULT_MIN_TEMP = 70;
  7. const DEFAULT_MAX_TEMP = 80;
  8. const DEFAULT_MIN_TDP_PERCENT = 10;
  9. const DEFAULT_MAX_TDP_PERCENT = 100;
  10. const DEFAULT_TDP_STEP_PERCENT = 10;
  11. const DEFAULT_CHECK_INTERVAL_MS = 2000;
  12. const DEFAULT_UPP_PATH = 'upp';
  13. function loadConfig(configPath) {
  14. if (!fs.existsSync(configPath)) {
  15. return null;
  16. }
  17. const raw = fs.readFileSync(configPath, 'utf-8');
  18. return JSON.parse(raw);
  19. }
  20. function mergeConfig(config) {
  21. return {
  22. minTemp: config.minTemp ?? DEFAULT_MIN_TEMP,
  23. maxTemp: config.maxTemp ?? DEFAULT_MAX_TEMP,
  24. minTdpPercent: config.minTdpPercent ?? DEFAULT_MIN_TDP_PERCENT,
  25. maxTdpPercent: config.maxTdpPercent ?? DEFAULT_MAX_TDP_PERCENT,
  26. tdpStepPercent: config.tdpStepPercent ?? DEFAULT_TDP_STEP_PERCENT,
  27. checkIntervalMs: config.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS,
  28. uppPath: config.uppPath ?? DEFAULT_UPP_PATH,
  29. };
  30. }
  31. function getCardIndices() {
  32. const entries = fs.readdirSync('/sys/class/drm');
  33. const indices = [];
  34. for (const name of entries) {
  35. const match = name.match(/^card(\d+)$/);
  36. if (match) {
  37. indices.push(parseInt(match[1], 10));
  38. }
  39. }
  40. return indices.length > 0 ? indices : [0];
  41. }
  42. function findHwmonDirs(cardNumber) {
  43. const hwmonDirs = [];
  44. const baseDir = `/sys/class/drm/card${cardNumber}/device/hwmon`;
  45. if (fs.existsSync(baseDir)) {
  46. for (const entry of fs.readdirSync(baseDir)) {
  47. if (entry.startsWith('hwmon')) {
  48. hwmonDirs.push(path.join(baseDir, entry));
  49. }
  50. }
  51. }
  52. const altBase = `/sys/class/drm/card${cardNumber}/device`;
  53. if (fs.existsSync(altBase)) {
  54. for (const entry of fs.readdirSync(altBase)) {
  55. if (entry.startsWith('hwmon')) {
  56. const dirPath = path.join(altBase, entry);
  57. if (!hwmonDirs.includes(dirPath)) {
  58. hwmonDirs.push(dirPath);
  59. }
  60. }
  61. }
  62. }
  63. return hwmonDirs;
  64. }
  65. function getTemperature(cardNumber) {
  66. const hwmonDirs = findHwmonDirs(cardNumber);
  67. if (hwmonDirs.length === 0) return null;
  68. let maxTemp = null;
  69. for (const hwmonDir of hwmonDirs) {
  70. try {
  71. const entries = fs.readdirSync(hwmonDir);
  72. for (const entry of entries) {
  73. const match = entry.match(/^temp\d+_input$/);
  74. if (match) {
  75. const raw = fs.readFileSync(path.join(hwmonDir, entry), 'utf-8').trim();
  76. const value = parseInt(raw, 10);
  77. if (!isNaN(value)) {
  78. const tempC = value / 1000;
  79. if (maxTemp === null || tempC > maxTemp) {
  80. maxTemp = tempC;
  81. }
  82. }
  83. }
  84. }
  85. } catch {
  86. }
  87. }
  88. return maxTemp;
  89. }
  90. function getDeviceName(cardNumber) {
  91. try {
  92. const namePath = `/sys/class/drm/card${cardNumber}/device/name`;
  93. if (fs.existsSync(namePath)) {
  94. return fs.readFileSync(namePath, 'utf-8').trim();
  95. }
  96. } catch { /* ignore */ }
  97. try {
  98. const slotPath = `/sys/class/drm/card${cardNumber}/device/uevent`;
  99. if (fs.existsSync(slotPath)) {
  100. const lines = fs.readFileSync(slotPath, 'utf-8').trim().split('\n');
  101. for (const line of lines) {
  102. const match = line.match(/^PCI_SLOT_NAME=(.*)$/);
  103. if (match) {
  104. const slot = match[1].trim();
  105. try {
  106. const out = execSync(`lspci -s ${slot} 2>/dev/null`, { encoding: 'utf-8' }).trim();
  107. if (out) return out.split(/\s{3,}/).pop().trim();
  108. } catch { /* ignore */ }
  109. }
  110. }
  111. }
  112. } catch { /* ignore */ }
  113. return `card${cardNumber}`;
  114. }
  115. function getCardPath(cardNumber) {
  116. return `/sys/class/drm/card${cardNumber}/device/pp_table`;
  117. }
  118. function setTdpForCard(cardNumber, power, config) {
  119. const cardPath = getCardPath(cardNumber);
  120. const cmd = `${config.uppPath} -p ${cardPath} set --write ` +
  121. `SmallPowerLimit1=${power} ` +
  122. `SmallPowerLimit2=${power} ` +
  123. `BoostPowerLimit=${power} ` +
  124. `smcPPTable/SocketPowerLimitAc0=${power} ` +
  125. `smcPPTable/SocketPowerLimitDc=${power}`;
  126. execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' });
  127. }
  128. function getMaxTdp(cardNumber, config) {
  129. try {
  130. const cardPath = getCardPath(cardNumber);
  131. const out = execSync(
  132. `${config.uppPath} -p ${cardPath} get SmallPowerLimit1`,
  133. { encoding: 'utf-8', stdio: 'pipe' }
  134. ).trim();
  135. const match = out.match(/(\d+(?:\.\d+)?)/);
  136. if (match) return parseFloat(match[1]);
  137. } catch { /* ignore */ }
  138. return 120;
  139. }
  140. function parseArgs(argv) {
  141. let configPath = null;
  142. for (let i = 2; i < argv.length; i++) {
  143. switch (argv[i]) {
  144. case '--config':
  145. case '-c':
  146. configPath = argv[++i];
  147. break;
  148. case '--help':
  149. case '-h':
  150. console.log('Usage: node amd-gpu-temp-control.js [options]');
  151. console.log('');
  152. console.log('Options:');
  153. console.log(' --config, -c <path> Config file path (default: /etc/amd-gpu-temp-control.json)');
  154. console.log(' --help, -h Show this help message');
  155. process.exit(0);
  156. break;
  157. }
  158. }
  159. return configPath || DEFAULT_CONFIG_PATH;
  160. }
  161. function calculateTargetTdpPercent(temp, minTemp, maxTemp, minTdpPercent, maxTdpPercent) {
  162. if (temp <= minTemp) return maxTdpPercent;
  163. if (temp >= maxTemp) return minTdpPercent;
  164. const ratio = (temp - minTemp) / (maxTemp - minTemp);
  165. return Math.round(maxTdpPercent - ratio * (maxTdpPercent - minTdpPercent));
  166. }
  167. function wattsFromPercent(percent, maxTdp) {
  168. return Math.round((percent / 100) * maxTdp);
  169. }
  170. function log(message) {
  171. const timestamp = new Date().toISOString();
  172. console.log(`[${timestamp}] ${message}`);
  173. }
  174. async function main() {
  175. const configPath = parseArgs(process.argv);
  176. const rawConfig = loadConfig(configPath);
  177. const config = mergeConfig(rawConfig || {});
  178. const cardIndices = getCardIndices();
  179. log('AMD GPU Temperature Control');
  180. log(`Temperature range: ${config.minTemp}°C - ${config.maxTemp}°C`);
  181. log(`TDP range: ${config.minTdpPercent}% - ${config.maxTdpPercent}%`);
  182. log(`TDP step: ${config.tdpStepPercent}%`);
  183. log(`Check interval: ${config.checkIntervalMs}ms`);
  184. log(`Upp path: ${config.uppPath}`);
  185. log(`Config: ${configPath}`);
  186. log(`Monitoring ${cardIndices.length} GPU(s)`);
  187. log('Press Ctrl+C to stop');
  188. log('');
  189. const gpuStates = cardIndices.map(cardNumber => ({
  190. cardNumber,
  191. deviceName: getDeviceName(cardNumber),
  192. maxTdp: getMaxTdp(cardNumber, config),
  193. currentTdpPercent: null,
  194. }));
  195. for (const state of gpuStates) {
  196. log(`GPU ${state.cardNumber} [${state.deviceName}]: max TDP ${state.maxTdp}W`);
  197. }
  198. if (gpuStates.length === 0) {
  199. log('No GPUs found');
  200. process.exit(1);
  201. }
  202. let iteration = 0;
  203. const interval = setInterval(() => {
  204. iteration++;
  205. for (const gpuState of gpuStates) {
  206. try {
  207. const temp = getTemperature(gpuState.cardNumber);
  208. if (temp === null) {
  209. log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: Cannot read temperature`);
  210. continue;
  211. }
  212. const inRange = temp >= config.minTemp && temp <= config.maxTemp;
  213. if (!inRange) {
  214. const targetTdpPercent = calculateTargetTdpPercent(
  215. temp,
  216. config.minTemp,
  217. config.maxTemp,
  218. config.minTdpPercent,
  219. config.maxTdpPercent
  220. );
  221. let shouldApply = false;
  222. if (gpuState.currentTdpPercent === null) {
  223. gpuState.currentTdpPercent = targetTdpPercent;
  224. shouldApply = true;
  225. } else if (targetTdpPercent !== gpuState.currentTdpPercent) {
  226. const diff = targetTdpPercent - gpuState.currentTdpPercent;
  227. if (Math.abs(diff) >= config.tdpStepPercent) {
  228. gpuState.currentTdpPercent += Math.sign(diff) * config.tdpStepPercent;
  229. } else {
  230. gpuState.currentTdpPercent = targetTdpPercent;
  231. }
  232. shouldApply = true;
  233. }
  234. if (shouldApply) {
  235. const clampedPercent = Math.max(config.minTdpPercent, Math.min(config.maxTdpPercent, gpuState.currentTdpPercent));
  236. const power = wattsFromPercent(clampedPercent, gpuState.maxTdp);
  237. setTdpForCard(gpuState.cardNumber, power, config);
  238. log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: ${temp.toFixed(1)}°C | TDP: ${clampedPercent}% (${power}W)`);
  239. }
  240. } else if (gpuState.currentTdpPercent !== null && iteration % 10 === 0) {
  241. const currentWatts = wattsFromPercent(gpuState.currentTdpPercent, gpuState.maxTdp);
  242. log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: ${temp.toFixed(1)}°C | TDP: ${gpuState.currentTdpPercent}% (${currentWatts}W) (stable)`);
  243. }
  244. } catch (err) {
  245. log(`GPU ${gpuState.cardNumber} [${gpuState.deviceName}]: Error reading temperature - ${err.message}`);
  246. }
  247. }
  248. }, config.checkIntervalMs);
  249. process.on('SIGINT', () => {
  250. clearInterval(interval);
  251. log('');
  252. log('Stopped.');
  253. process.exit(0);
  254. });
  255. }
  256. main();