1. 风扇“全速狂转”背后的真实原因:Linux下温控链路断在哪
1.1 GPU发热谁来管?Ubuntu默认状态下没人管得明白
先说一个很多人第一次遇到的场景:把显卡装进一台跑Ubuntu的机器,开机后一切正常,但只要跑个AI推理、编译一次大模型,或者渲染个3D画面,GPU温度几分钟内冲上70度、80度,显卡自带的散热风扇直接满转。此时你的机箱里就像有一台小型吹风机,隔壁工位都能听到。更烦的是,温度降下来之后,风扇转速根本不会跟着降,一直保持着那种“我很热”的大功率咆哮。
这事的根源在于,在Windows下显卡风扇的温控策略通常由驱动和厂商工具负责,比如NVIDIA的GeForce Experience、AMD的Adrenalin,它们会维护一条温度和转速之间的平滑曲线。而Ubuntu这类Linux发行版,驱动虽然是开源的(AMD/NVIDIA的闭源驱动也差不多),但默认并没有一个“帮你把风扇曲线调好”的用户态服务。风扇控制、温度读取、策略决策、PWM输出这几个环节,系统默认是分开的。换句话说,Linux不是不能管风扇,而是默认谁都没上岗。
很多教程会告诉你用nvidia-smi看温度、用nvidia-settings设置风扇转速,但很少有人讲清楚:**这些命令只能控制显卡自带的那个“原装风扇”,并且主板PWM风扇接口和显卡风扇控制芯片完全是两套体系。**如果你的机箱风扇压根没插在显卡上,而是插在主板的CHA_FAN或SYS_FAN接口,那么就算你把GPUFanControlState设成1,机箱风扇也不会动一下。所以第一步必须先搞清楚,你要控的是“哪个风扇”,它连在“谁的接口”上。
1.2 三种典型硬件拓扑,对应三种完全不同的控制路径
结合我实际拆过的机器,常见的GPU风扇温控场景基本就这三种:
场景A:涡轮/开放式显卡自带风扇,由显卡板载控制芯片驱动。这是最省心的一种,因为温度传感器和风扇都在显卡上,控制回路是闭环的。NVIDIA卡通过NVML接口,AMD卡通过amdgpu驱动的hwmon节点,都可以直接读写风扇转速。你要做的只是提供一个“策略”,把温度映射成转速,剩下的PWM信号生成由显卡控制芯片完成。
场景B:GPU是裸Die/被动散热,机箱风扇直连主板PWM接口。多见于服务器、老工作站、深度学习机或者矿卡改装的场景。GPU上没有主动散热风扇,或者原装风扇坏了被拆掉,全靠机箱风扇对着吹。此时风扇的供电和PWM调速线都插在主板上,GPU温度和风扇转速之间没有任何物理联系,必须靠软件把两者“嫁接”起来:读GPU温度,算目标转速,写入主板的hwmon PWM节点。
场景C:显卡风扇被改装成独立供电,用独立PWM控制器(比如外置调速器)驱动。这种情况一般出现在水冷改装或者自制散热平台上。风扇调速器有自己的控制逻辑,甚至有的支持外部温度探头,和Ubuntu系统完全没有软件接口。如果调速器没有自动曲线,你甚至需要外接一个单片机或PWM信号发生器。不过这种属于极客玩法,绝大多数人还是用A和B。
你先对照一下自己的机器属于哪种。如果你的需求是“GPU一热,机箱风扇自动加速”,那你大概率在场景B,这也是本文最想解决的情况。但无论哪种场景,搞清楚硬件链路之前,盲目装脚本都是白费功夫,因为你会连“该写哪个文件”都找不到。
2. 硬件连接与PWM调速原理:搞清楚风扇到底“听谁的话”
2.1 4Pin风扇的PWM调速,到底是怎么实现的
绝大多数主板上用于CPU和机箱风扇的接口都是4Pin的,引脚定义从左到右分别是:地线(GND)、12V供电、转速反馈(Tach/Sense)、PWM控制信号。12V供电始终在,风扇转不转、转多快,由PWM引脚上的方波信号决定。这个方波的占空比(高电平时间占一个周期的比例)越高,风扇电机获得的等效驱动电压就越高,转速也就越快。
有一点值得多提一句:PWM信号并不直接控制12V供电的通断,而是通过风扇内部的一个小驱动MOS管来调节电机线圈的平均电压。所以4Pin风扇本质上支持两种调速方式,一是主板输出不同占空比的PWM信号,二是主板直接调节12V输出端的电压(这就是很多3Pin风扇的调速方式,电压越低转得越慢)。在Linux的hwmon体系里,这两者都会抽象成pwmN节点,但物理实现不同,后者通常不支持连续可调,所以碰到3Pin风扇调速体验会差很多。
PWM占空比在Linux hwmon里通常用0到255的整数表示,0表示完全停转,255表示满速。但注意,并不是所有风扇在占空比很低时都能启动,很多风扇的启动电压在5V左右,占空比低于某个阈值(比如20%)时电机根本转不起来。所以写控制脚本时,最低转速不要直接设成0,一般要留一个最小占空比,比如30左右,对应大概1200转。我自己调过好几块不同品牌的主板,实测NCT6775F、ITE IT8720F、华硕主板自带的AURA控制芯片,它们的PWM输出范围虽然都是0-255,但同一只风扇在同样的占空比下转速并不完全一致,所以后面改脚本时一定要以实际观测到的fanN_input转速为准,而不是死记占空比。
2.2 主板PWM接口与显卡风扇接口,控制权在谁手上
主板PWM接口的控制逻辑通常集成在Super I/O芯片里(Nuvoton NCT系列、ITE IT系列、ASPEED BMC等),Linux通过hwmon子系统把这些芯片注册成/sys/class/hwmon/hwmon*/下的一个个节点。每个接口对应一组文件:pwmN(写入目标占空比)、pwmN_enable(控制模式,0为手动,1为自动,2为手动忽略温度等)、fanN_input(当前转速读数)。当你把pwmN_enable设成1时,主板BIOS里的自动温控策略接管,你写pwmN也不会生效;设成0或2时,你才能直接写pwmN控制转速。
显卡这边则是另一套。NVIDIA显卡的风扇控制芯片挂在显卡PCB上,部分高端卡通过NVML暴露GPUFanControlState和GPUTargetFanSpeed两个接口。AMD显卡则通过amdgpu驱动暴露在/sys/class/drm/card0/device/hwmon/hwmon*/fan1_input和pwm1节点上。注意,NVIDIA卡的hwmon节点通常只暴露温度,不暴露PWM控制,这也是很多人困惑的地方:明明在/sys/class/hwmon下看到了NVIDIA的温度传感器,却没有pwm文件可写。原因就是NVIDIA选择用NVML来处理风扇控制,而不是标准hwmon。
所以当你看到某篇博客说“写hwmon节点直接控风扇”,先确认他控的是主板风扇还是显卡风扇,两者路径完全不同。如果你要控的是主板风扇,显卡这边再折腾也白搭。
2.3 先做一次硬件侦察:确认你的风扇挂在哪
在动手写脚本之前,我建议你先花几分钟把机器的传感器拓扑摸一遍。Ubuntu下顺序执行这么几步:
- 查看当前hwmon设备列表:
ls -l /sys/class/hwmon/每条软链接都指向一个驱动实例,从名字基本能看出是谁,比如nct6775就是Nuvoton的Super I/O,amdgpu是AMD显卡。
- 查看每个hwmon下的设备名:
for h in /sys/class/hwmon/hwmon*; do echo "=== $h ===" cat $h/name 2>/dev/null ls $h | grep -E "pwm|fan" done这一步会让你快速知道哪一块芯片带PWM输出,哪一块只有温度传感器。
- 如果你用的是NVIDIA卡,直接用
nvidia-smi -q -d TEMPERATURE看GPU温度;如果是AMD卡,直接cat /sys/class/drm/card0/device/hwmon/hwmon*/temp1_input(单位是毫摄氏度)。
做完这三步,你脑子里应该有一张“温度从哪来、PWM去哪写”的路线图了。如果你的主板芯片压根没被识别(比如某些新主板用瑞萨或联想的定制EC芯片),你可能需要先确认内核版本并手动加载对应驱动,这一步比写控制脚本更麻烦,下文会专门讲。
3. Ubuntu下获取GPU温度:四套方案,一套兜底
3.1 NVIDIA卡:nvidia-smi、NVML和nvidia-settings的边界
NVIDIA卡在Linux下读取GPU温度最常用的命令是nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader。这个温度是GPU核心边缘的温度传感器读数,单位是摄氏度,和Windows下看到的GPU温度基本一致。如果要用程序循环读,建议用nvidia-smi -q -d TEMPERATURE解析,或者直接用Python的pynvml库。
pynvml是NVIDIA官方NVML库的Python绑定,安装是pip install nvidia-ml-py,用起来非常方便:
import pynvml pynvml.nvmlInit() handle = pynvml.nvmlDeviceGetHandleByIndex(0) temp = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU) print(f"GPU温度: {temp} °C")很多人会混淆nvidia-settings和nvidia-smi的分工。简单说,nvidia-smi负责查询状态、设置功耗和性能模式,nvidia-settings才是控制风扇转速的工具,部分桌面卡的GPUFanControlState设置就是通过它实现的。在Ubuntu下要先:
sudo apt install nvidia-settings nvidia-settings -a [gpu:0]/GPUFanControlState=1 -a [fan-0]/GPUTargetFanSpeed=50GPUTargetFanSpeed的取值范围是0到100,代表目标转速百分比。注意这只是NVIDIA卡自带风扇的控制方式,和主板PWM风扇无关。而且很多涡轮卡、OEM卡并不开放这个控制接口,命令敲下去会提示Attribute 'GPUFanControlState' ... could not be assigned,遇到这种情况别硬刚,直接用后文的主板hwmon方案。
3.2 AMD卡:amdgpu驱动的hwmon节点,温度随手可读
AMD显卡在Linux下用的是amdgpu内核驱动,驱动挂载后会暴露一组hwmon节点。路径形如:
/sys/class/drm/card0/device/hwmon/hwmon5/temp1_input /sys/class/drm/card0/device/hwmon/hwmon5/fan1_input /sys/class/drm/card0/device/hwmon/hwmon5/pwm1temp1_input返回的是毫摄氏度,读取之后要除以1000。pwm1是风扇PWM占空比,读写范围通常也是0到255。比较新的内核版本里,amdgpu驱动对风扇控制的策略也可以通过pwm1_enable切换,0代表手动控制、1代表自动、2代表根据温度手动映射。如果你的卡是RX 6000/7000系列,pwm1_enable默认是1,也就是自动模式,此时你写pwm1不一定生效,必须先把pwm1_enable改成0或2。
AMD还有一个小工具叫rocm-smi,它是ROCm栈的一部分,也可以查温度、功耗、风扇转速:
rocm-smi --showtemp --showfan但rocm-smi对风扇控制的支持比较有限,多数版本只读不写。所以我个人建议,AMD用户直接操作hwmon节点就好,可控性最强。这里有个比NVIDIA好的地方:AMD的hwmon节点是标准Linux接口,不需要额外装闭源库,写脚本、跑服务都很透明。
3.3 Intel核显/Arc独显:别忘了一众“非主流”GPU
很多人一提GPU就只想到NVIDIA和AMD,但别忘了Intel核显和Intel Arc独显在Ubuntu下同样可能成为发热大户,尤其是在用集成显卡跑视频转码或者渲染的时候。Intel显卡的温度通过i915/xe驱动的hwmon节点暴露,路径一般是:
/sys/class/drm/card0/device/hwmon/hwmon*/temp1_inputArc独显还提供pwm1节点用于控制风扇,不过如果你的Arc显卡是被动散热版,就没有风扇节点。另外Intel核显没有自己的风扇,只能靠CPU风扇或机箱风扇散热,所以它更依赖主板的PWM温控策略。如果你的机器是核显跑深度学习推理、视频编码,需要让主板风扇跟着核显温度走,那做法和场景B完全一样,只是温度源换成Intel的hwmon节点。
3.4 兜底方案:lm-sensors与nvtop,先做到“看得见”
不管你是什么显卡,先装一套lm-sensors总没错:
sudo apt install lm-sensors sudo sensors-detect --auto sensorssensors-detect会自动扫描主板上的Sensor芯片并加载对应的内核模块,比如nct6775、coretemp。跑完sudo sensors就能看到CPU温度、主板温度、风扇转速。虽然sensors的输出不会直接给出GPU温度,但它能帮你确认主板的hwmon节点是否全部注册,省去很多自己翻dmesg的功夫。
nvtop是另一个方便的工具,它类似于htop但专门显示GPU状态,支持NVIDIA、AMD、Intel。安装之后可以直接在终端里看到多张GPU的温度、显存使用、功耗、风扇转速,非常直观。我调试温控脚本时经常开着nvtop和sensors两个窗口,一边看着温度变化一边看脚本输出的PWM占空比,排查问题比全靠猜快得多。
4. 让PWM风扇跟着GPU温度走:三种可落地的方案对比
4.1 方案A:GPU自带风扇,用NVIDIA/AMD官方接口控制
如果目标是控制显卡自己的风扇,思路很简单:循环读温度,算目标转速,写风扇控制接口。NVIDIA卡可以用pynvml或者nvidia-settings,AMD卡直接写hwmon的pwm1。我自己给一张GTX 1660S写过一个最简单的温度映射脚本,逻辑就是温度小于55度转速设30,55到75度线性升到70,超过75度直接拉满。跑了一个月,游戏和AI训练场景下噪音和温度平衡得不错。
但这里要特别提醒:**部分显卡的硬件风扇控制电路并不接受低占空比设定。**比如某些显卡在GPUTargetFanSpeed=0时风扇确实停转,但下次加载驱动的瞬间会有一个转速脉冲;还有些显卡在GPUTargetFanSpeed设为20以下时,风扇虽然没停但出现明显的电机咝咝声,因为PWM频率低于人耳可听范围的上限,甚至能听到线圈啸叫。所以如果你的风扇在低速区间有明显异响,建议把最低限速调高一点,30或者40,换来的是安静得多。NVIDIA官方驱动里没有暴露PWM频率的调节接口,遇到啸叫只能靠提高最低占空比绕过共振点。
4.2 方案B:主板PWM风扇,通过hwmon节点直接控
这是场景B的核心方案,也是本文的重头戏。控制流程如下:
- 读取GPU温度(可选的温度源:NVIDIA的
nvidia-smi、AMD/Intel的hwmon节点); - 根据你设定的温度-转速映射函数,计算目标PWM占空比;
- 找到主板风扇对应的
pwmN节点,把pwmN_enable设成手动模式; - 把目标占空比写入
pwmN,循环执行。
第一步的温度源和第三步的PWM节点没有任何物理联系,所以脚本里要显式地做一个“点对点”绑定。举个例子,我家里的Windows机器用的是NVIDIA显卡,机箱风扇插在主板的SYS_FAN2接口,对应的hwmon路径是:
/sys/class/hwmon/hwmon3/pwm2我在Ubuntu里的做法就是开一个systemd服务,每两秒钟跑一次Python脚本:读nvidia-smi的GPU温度,然后写pwm2。脚本本身很简单,难的是怎么让这个服务稳定、优雅地跑起来,这部分第四节细说。
这里有个关键坑:主板BIOS默认把SYS_FAN2设成了自动温控模式,也就是pwm2_enable=1。在这个模式下你直接往pwm2写入数字大概率没反应。你必须先执行:
echo 0 | sudo tee /sys/class/hwmon/hwmon3/pwm2_enablepwm2_enable=0代表手动模式,之后才能用echo 180 | sudo tee /sys/class/hwmon/hwmon3/pwm2来控制。有的主板支持值为2,意思是“写速度百分比”的模式,但具体行为因驱动而异,我建议统一用0加0-255占空比的方式,最不容易踩雷。
4.3 方案C:更精细的PID控制,适合温度波动大的场景
固定映射曲线在多数情况下够用,但有个缺点:温度升高时风扇转速反应滞后明显。比如跑一个短时间内负载交替的任务,温度每隔几秒就在50度和75度之间跳,映射关系如果写得激进,风扇就会一直做全速跑-降速-再加速的往复运动,噪音忽大忽小,听着很难受。
解决这个问题有两种思路。一是给映射加“迟滞”(hysteresis),也就是上升沿用一条曲线,下降沿用另一条偏移曲线,避免在阈值附近反复横跳。二就是PID控制。PID的P项根据当前温度与目标温度的偏差输出,I项消除长期偏差,D项抑制温度斜率带来的过冲。实际上风扇控制是典型的“输入=目标转速或者目标温度、输出=PWM占空比”问题,写一个简单的增量式PID并不难,效果也立竿见影。
我个人的调参经验是:先只调P,把Kp从0.5开始往上试,目标是温度稳定时风扇转速不剧烈抖动;然后加D,Kd控制在0.5到1.5之间,用来抑制温度突变;I项尽量小,因为风扇本身不需要消除静差到零,反而容易积分饱和导致转速一直偏高。PID参数不是越小越好,要结合你机箱的风量和GPU散热器的热容去试。这个调参过程有点像煮饭时控制火候:火太大锅底糊,火太小饭夹生,只有摸清锅的脾气才能找到合适的曲线。
4.4 三套方案怎么选:一张表格看懂
| 需求场景 | 推荐方案 | 控制对象 | 实现难度 | 前期工作量 |
|---|---|---|---|---|
| 显卡原装风扇,N卡 | 方案A(nvidia-settings/pynvml) | GPU风扇 | 低 | 装nvidia-settings,写简单循环 |
| 显卡原装风扇,A卡 | 方案A(amdgpu hwmon / rocm-smi) | GPU风扇 | 低 | 确认hwmon路径,写简单循环 |
| 机箱风扇吹GPU,主板PWM | 方案B(hwmon PWM写入) | 主板风扇 | 中 | 确认主板hwmon节点,写systemd服务 |
| GPU温度波动剧烈 | 方案C(PID控制) | 任意PWM风扇 | 中高 | 写PID算法,整定参数 |
| 多GPU或多风扇 | 方案B/C,但要逐个绑定 | 多个PWM节点 | 高 | 设计合理的配置格式 |
5. 实测脚本:一个从温度到PWM的完整控制器
5.1 脚本设计思路与关键参数
下面给出一套我在Ubuntu 22.04上实跑过的方案,场景为“NVIDIA显卡温度驱动主板SYS_FAN PWM风扇”。设计原则是:能用标准工具尽量用标准工具,脚本要有日志、有错误恢复、不该写坏系统文件。整个控制器分三块:温度读取、映射计算、PWM写入。
映射策略我选的是“线性插值+死区”。温度低于45度时占空比固定在40(约1100转,保证基本风量);45到70度之间线性从40升到180;超过70度直接满速255。加入死区的目的是避免温度在45度附近来回穿越导致风扇转速小幅度频繁变化。你可以根据自己的显卡热设计功率和机箱风道调整这三个参数,但逻辑保持一致。
完整Python脚本如下:
#!/usr/bin/env python3 import subprocess import time import os import sys import logging # ---------- 配置区 ---------- PWM_PATH = "/sys/class/hwmon/hwmon3/pwm2" PWM_ENABLE_PATH = "/sys/class/hwmon/hwmon3/pwm2_enable" FAN_INPUT_PATH = "/sys/class/hwmon/hwmon3/fan2_input" # 可选,用于日志 TEMP_CMD = ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"] TEMP_MIN = 45 TEMP_MAX = 70 PWM_MIN = 40 PWM_MAX = 180 PWM_FULL = 255 DUTY_STEP_MAX = 20 # 单次调整最大步进,避免转速突变 SLEEP_INTERVAL = 2.0 LOG_FILE = "/var/log/gpu-fan-controller.log" # ---------- 配置区结束 ---------- logging.basicConfig( filename=LOG_FILE, level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) def read_gpu_temp(): try: out = subprocess.check_output(TEMP_CMD, universal_newlines=True).strip() return int(out.split("\n")[0]) except Exception as e: logging.error(f"读取GPU温度失败: {e}") return None def read_current_pwm(): try: with open(PWM_PATH, "r") as f: return int(f.read().strip()) except Exception: return -1 def write_file(path, value): try: with open(path, "w") as f: f.write(str(value)) return True except Exception as e: logging.error(f"写入 {path} 失败: {e}") return False def map_temp_to_pwm(temp): if temp <= TEMP_MIN: return PWM_MIN if temp >= TEMP_MAX: return PWM_FULL ratio = (temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN) return int(PWM_MIN + (PWM_MAX - PWM_MIN) * ratio) def init_pwm(): # 切手动模式 if os.path.exists(PWM_ENABLE_PATH): write_file(PWM_ENABLE_PATH, 0) def main(): init_pwm() current_pwm = read_current_pwm() if current_pwm < 0: current_pwm = PWM_MIN write_file(PWM_PATH, current_pwm) logging.info("GPU风扇控制器启动") while True: temp = read_gpu_temp() if temp is None: time.sleep(SLEEP_INTERVAL) continue target_pwm = map_temp_to_pwm(temp) # 限制单次步进,防止转速突变 if abs(target_pwm - current_pwm) > DUTY_STEP_MAX: target_pwm = current_pwm + DUTY_STEP_MAX if target_pwm > current_pwm else current_pwm - DUTY_STEP_MAX if target_pwm != current_pwm: if write_file(PWM_PATH, target_pwm): log_msg = f"温度: {temp}C, 目标PWM: {target_pwm}" if os.path.exists(FAN_INPUT_PATH): try: with open(FAN_INPUT_PATH, "r") as f: rpm = int(f.read().strip()) log_msg += f", 当前转速: {rpm} RPM" except Exception: pass logging.info(log_msg) current_pwm = target_pwm time.sleep(SLEEP_INTERVAL) if __name__ == "__main__": main()这里有几点设计细节值得解释。第一,DUTY_STEP_MAX限制单次调整不超过20个占空比单位,对应2秒的采样周期,也就是说风扇转速从最低到最高大约需要20多秒,这样既有响应速度又不会让噪音“阶梯式”猛冲。第二,如果某一次读温度失败,脚本不会强行把PWM清零或者拉满,而是保持上一状态继续循环,这样至少在传感器短暂异常时不会做出危险动作。第三,所有写入都做了异常捕获,即使权限不够或者节点不存在,脚本也只是记录日志而不是直接崩溃。
5.2 如何绑定到systemd让脚本开机自启
写好的脚本放在/usr/local/bin/gpu-fan-controller.py,先加执行权限并测试运行几秒钟:
chmod +x /usr/local/bin/gpu-fan-controller.py /usr/local/bin/gpu-fan-controller.py 2>&1 & sleep 5 kill %1确认日志文件有输出、风扇转速有明显变化后,再做成服务。创建/etc/systemd/system/gpu-fan-controller.service:
[Unit] Description=GPU Temperature Controlled Fan Service After=multi-user.target [Service] Type=simple ExecStart=/usr/bin/python3 /usr/local/bin/gpu-fan-controller.py Restart=always RestartSec=5 StandardOutput=null StandardError=journal [Install] WantedBy=multi-user.target然后:
sudo systemctl daemon-reload sudo systemctl enable gpu-fan-controller.service sudo systemctl start gpu-fan-controller.service注意这里After=multi-user.target的意思是等系统所有后台服务起来之后再说,避免在hwmon节点还没注册时就启动脚本。如果你是AMD卡或者Intel卡,只需要把TEMP_CMD换成读取hwmon温度文件的命令,PWM路径改成实际路径,其他逻辑完全一样。
5.3 脚本实测效果和日志怎么看
我用这套脚本在那台机器连续跑了三个月,效果符合预期:待机时GPU温度在42到48度之间徘徊,风扇稳定在1200转左右,几乎听不见;跑pytorch训练时温度冲到80度,风扇转速在几十秒内从1200转平滑爬到满速,机箱风噪明显变大,但温度最终稳定在82度,没有触发任何过热保护。
日志文件/var/log/gpu-fan-controller.log里每一行都记录了温度、目标PWM和实际转速,找规律特别方便。有一次我发现晚上跑负载时风扇转速总是来回摆动,温度却一直稳定在70度附近,查日志发现是环境温度下降到一定程度,GPU散热器变冷了,但脚本的目标PWM还在因为70度这个临界点上下浮动。后来把TEMP_MAX从70改成72,给了一点缓冲区间,摆动就消失了。
6. 踩坑记录:一份完整的排查链路,照着走能省一晚上
6.1 PWM文件写不进去?先查权限和驱动加载
最常见的问题就是脚本写pwm2时报Permission denied。首先确认你是不是用root运行的,hwmon节点默认权限都是root才能写,普通用户不行。但你不可能一直开个root终端跑脚本吧?所以正确做法是把当前用户加入root权限组,或者更优雅一点,直接让systemd服务以root身份运行。我上面的service文件里没指定User,默认就是root,所以不会遇到权限问题。
如果root也写不进去,那就要查主板芯片驱动有没有加载了。先用dmesg | grep -i hwmon或者lsmod | grep -E "nct6775|it87|w83627"确认驱动在不在。我碰到过一次很诡异的情况:主板是华硕B550M,sensor芯片是NCT6798D,但Ubuntu 22.04的内核只加载了nct6775的一部分功能,导致hwmon下虽然有pwm1但没有pwm2。后来发现是BIOS里把第二个风扇接口设成了“水冷泵模式”,这种模式下的PWM输出不受hwmon控制,直接在BIOS里改回“DC/PWM自动识别”模式才解决。
6.2 pwmN_enable设为1还是0?不同驱动行为可能完全不一样
前面提到pwmN_enable不同的值代表不同的控制模式,但不同驱动、不同主板对pwmN_enable=2的解释并不一致。NCT6775驱动里,0是手动模式,1是自动模式,2是“写目标转速百分比”的特殊模式。ITE IT87驱动则可能反过来。所以最稳妥的写法是:
echo 0 | sudo tee /sys/class/hwmon/hwmon*/pwm2_enable写0一定是手动PWM控制。如果你非要确认自动模式是否被关闭,可以读一下:
cat /sys/class/hwmon/hwmon*/pwm2_enable返回0才说明现在是手动模式。有些发行版(比如较新的Fedora)默认启用了pwm-fan驱动,它会接管一部分PWM节点,此时你的hwmon路径可能不再直接对应BIOS里的接口,检查和配置方式要相应调整。
6.3 显卡风扇一直满转?先分清“系统不控”和“硬件故障”
很多人在做温控脚本之前会遇到显卡风扇一直满转的问题,以为是主板控制问题。其实在Linux下,NVIDIA显卡默认就是这个状态:桌面卡的风扇控制策略仅在X server启动并加载驱动参数后生效,服务器版驱动或者没有桌面的最小系统,显卡风扇就是满转的。这是驱动默认行为,不是硬件故障,不用慌。
如果你用的是NVIDIA Tesla或Quadro专业卡,可能根本没有风扇控制接口,因为这些卡设计成在服务器里由机箱风墙散热,GPU自己不带风扇。如果这种卡装在普通PC上,风扇自然就一直不转或者满转,解决方案只有一个:加机箱风扇,然后按场景B的方式把机箱风扇和GPU温度绑定起来。
6.4 轮询温度的命令写太频繁,也会引发别的问题
写脚本的时候最好把nvidia-smi的调用频率控制在1秒以上。虽然nvidia-smi本身不会因为频繁调用而损坏硬件,但每次调用都会通过NVML的库初始化和查询,开销其实不小;在多GPU机器上,如果你同时跑多个监控脚本,CPU占用率会明显上涨。我自己在双卡Titan RTX机器上实测,每0.5秒查询一次nvidia-smi --query-gpu=temperature.gpu,大约会占用一个整核的5%左右,看起来不多,但如果你同时在跑深度学习训练,这点开销可能让它变成训练脚本的一个小干扰源。
更好的做法是直接用pynvml在一个进程里维护NVML上下文,循环查询不重复初始化,或者用nvidia-smi dmon这类批量监控工具。不过对小规模部署来说,2秒间隔的nvidia-smi子进程调用完全够用,CPU占用不到1%,这也是我上面脚本默认间隔取2秒的原因。
6.5 从BIOS层排查:有些坑只能在BIOS里解决
最后提一个容易被忽略的坑:主板BIOS里有一个“Fan Stop Level”或“Start PWM”的设置,它决定了PWM占空比在什么值以下时风扇完全停转。如果你的脚本把目标PWM写在35左右,但风扇却完全不转,有可能不是脚本问题,而是BIOS把这个阈值设得过高了。遇到这种情况,检查BIOS里的风扇设置,把起始占空比调低,或者直接把风扇设为“Disable stop”模式,让驱动器始终对PWM信号做出响应。
另外,部分服务器主板(比如超微的X10/X11系列)有一个FAN MODE控制器,它会根据主板温度自动覆盖系统对PWM的写入。这种情况下,你就算把pwm1_enable切到手动,BMC固件也会周期性把PWM值改回去。要解决必须进入IPMI管理界面,把风扇模式从“Optimal/Standard”改成“Full Speed”或“Custom”,再用ipmitool raw来设置风扇占空比。这条路比较硬核,普通家用主板一般不会遇到。
7. 调参经验与长期运行须知
7.1 曲线怎么定:别让风扇跟着“瞬时温度”跳舞
温度和转速的映射最忌讳的是“瞬时值驱动”。GPU在跑负载时温度会有几十秒甚至几分钟的爬升,负载一停温度又会断崖式下降。如果映射曲线太陡,风扇就会在低转速和高转速之间来回冲刺,既吵又容易让电机轴承磨损。我建议把采样周期加长到3到5秒,同时在映射函数里加一个EMA(指数滑动平均),比如当前目标PWM = 0.7 × 上一次目标PWM + 0.3 × 新算出的目标PWM。这样温度瞬间跳变时,风扇转速只会平滑地跟随,不会猛地加速。
另外一个容易被忽视的参数是风扇的“响应斜率”。很多风扇从800转爬到满速,物理上需要两三秒;如果你脚本里限制单次步进20,转速爬升就会拉长到十几秒,这其实是好事,因为温度本身是慢变量,风扇响应太快反而会导致系统振荡。只要不出现散热跟不上温度继续上涨的情况,缓一点没关系。
7.2 静音和性能的平衡:死区、最大转速上限和手动开关
温控风扇的终极目标不是“温度越低越好”,而是在噪音、功耗和温度三个维度之间找到可接受的平衡点。很多人上来就把温度目标定在60度,结果风扇全程都在高速运转,吵得根本无法静心工作。我自己的经验是,NVIDIA卡核心在85度以内都是安全区间,AMD卡在90度以内也问题不大,没必要为了数字好看牺牲静音。
所以你可以考虑这样设计策略:设置PWM_FULL阈值在75或80度,超过这个温度才拉满;而待机温度区间尽量压低转速。如果你做了水冷或者机箱风道特别优秀,甚至可以给最大转速设置一个上限,比如180(对应约70%转速),让它在重负载时也不会太吵,温度稍微高两三度完全可以接受。
7.3 脚本的安全退出机制和异常恢复
控制脚本跑的时间长了,最怕系统恰好需要恢复默认风扇策略,比如你要刷BIOS或者做硬件诊断。为此可以在脚本里增加一个简单的“退出即恢复自动模式”机制,捕获SIGTERM信号,退出前把pwmN_enable写回1。具体实现不复杂:
import signal def cleanup(signum, frame): write_file(PWM_ENABLE_PATH, 1) # 恢复自动模式 logging.info("收到退出信号,恢复自动风扇控制") sys.exit(0) signal.signal(signal.SIGTERM, cleanup)这样你systemctl stop gpu-fan-controller的瞬间,风扇就会交还给BIOS自动管理,不会出现脚本一停、风扇就完全不动的情况。这个细节在长期运行的服务器上非常重要,因为一旦哪天你忘了脚本还在跑,直接拔掉某根传感器线,恢复自动模式能帮你兜底。
7.4 多GPU机器的扩展思路
如果你有一台6卡、8卡的深度学习工作站,每张卡的负载可能差异很大,温度也各不相同。此时温控策略要区分“局部”和“全局”。我的做法是:取所有GPU温度里的最大值作为控制源,机箱风扇跟随这个最高温度走。因为风道散热是共享的,只要最热的卡被压住,其他卡自然也不会过热。如果你想让每个风扇分别对应特定卡,需要把PWM节点和温度源建立多对多映射,脚本架构要改成配置驱动的形式,比如写一个JSON配置文件,每个风扇节点定义一个温度源和映射曲线。复杂度虽高,但换来的是更高的定制性,适合水冷和多路显卡场景。
这个内容后续还可以扩展的方向是接入Prometheus + Grafana做温度监控,或者直接把温控逻辑挪到BMC里去实现,不过那是另一个话题了。至少对大多数个人用户来说,上面这套“Ubuntu + 温度查询 + hwmon PWM写入 + 线性映射 + systemd服务”的组合,已经能把GPU发热和风扇转速之间的关系理顺。最后再分享一个小技巧:调试阶段别一上来就设成开机自启,先在终端前台跑,手动用stress或者跑个gpu-burn制造负载,观察温度变化时风扇有没有及时响应。确认曲线符合预期后,再部署为系统服务,稳得多。