那台跑 LLM 的台式机(MSI PRO Z790-A MAX WIFI、RTX 4070 Ti SUPER、四条 G.Skill RGB 内存、四把带圆形 LCD 屏的风扇)放在卧室里,一开机就是一屋子彩虹。装的是 Ubuntu 24.04,没有厂商软件。这篇记录怎么在 Linux 下把每一个发光的东西关掉,以及顺手做的按需运行方案。
摸清硬件
cat /sys/class/dmi/id/board_name # PRO Z790-A MAX WIFI (MS-7E07)lsusblsusb 里和灯有关的:
1462:7e07 Micro Star International MYSTIC LIGHT0416:7372 Winbond Electronics Corp. TL_Series_ControllerV0.6204fc:7393 Sunplus Technology Co., Ltd TL_LCDV0.1 (×4,各自挂在一个 1a40:0801 Terminus hub 后面,菊花链)内存条走 SMBus(i2c_i801 已加载,/dev/i2c-* 存在),型号要 root 才能读:
sudo dmidecode -t memory | grep -E "Manufacturer|Part Number"# G Skill Intl F5-6400J3239G16G ×4风扇那四个 TL_LCD 设备我一开始按字符串当成了 Thermalright TL 系列,查了两个 Thermalright 的 Linux 项目都不认这些 ID。后来在 FanControl.LianLi 的 issue 和 OpenRGB 的 issue 里找到了对应:0416:7372 和 04fc:7393 是联力 UNI FAN TL / TL LCD 的 hub 控制器和 LCD 屏,“TL_Series” 是联力的命名。这个误判浪费了一轮调研。
OpenRGB:内存和主板
OpenRGB 1.0rc3.1 的 AppImage 不需要安装,但 hidraw 和 i2c 设备默认只有 root 能读:
wget -O OpenRGB.AppImage https://codeberg.org/OpenRGB/OpenRGB/releases/download/release_candidate_1.0rc3.1/OpenRGB_1.0rc3.1_x86_64_5e81e26.AppImagechmod +x OpenRGB.AppImage./OpenRGB.AppImage --appimage-extract # 解出 squashfs-root/,systemd 里直接跑 AppRunsudo modprobe i2c-devsudo ./squashfs-root/AppRun --list-devices识别结果:
0: ENE DRAM I2C 0x70 Modes: Direct Off Static Breathing ... [Rainbow]1: ENE DRAM I2C 0x712: ENE DRAM I2C 0x723: ENE DRAM I2C 0x734: MSI PRO Z790-A MAX WIFI (MS-7E07) MSI Mystic Light Device (185-byte) Modes: Direct Static Breathing ... ['Rainbow wave'] ... Zones: JRGB1 JRAINBOW1 JRAINBOW2 JRAINBOW3四条 G.Skill 是 ENE 控制器,有原生 Off 模式,设一次就存在控制器里,关机重启都保持。主板 Mystic Light 没有 Off,只能用静态黑。四个 zone 就是主板上的灯针,风扇的 ARGB 灯环接在 JRAINBOW 上,所以主板黑掉风扇灯环也跟着黑。
内存一条命令就好:
sudo ./squashfs-root/AppRun -d 0 -m off -d 1 -m off -d 2 -m off -d 3 -m off主板有个坑,折腾了三次才定位。直接下 Static 黑:
sudo ./squashfs-root/AppRun -d 4 -m static -c 000000# 回读还是 ['Rainbow wave']先切一次 Direct 再下 Static 就生效:
sudo ./squashfs-root/AppRun -d 4 -m direct -c 000000 # 回读 [Direct]sudo ./squashfs-root/AppRun -d 4 -m static -c 000000 # 回读 [Static]这块 185-byte 协议的 MSI 控制器似乎需要先被切到 Direct 才接受后续模式写入。我第一次手动试的时候正好是先试了 Direct,写进 systemd 服务时漏了这一步,服务从来没真正对主板生效过,直到第一次挂起恢复灯全回来才暴露。
TIP主板灯的另一条路是 BIOS:MSI 官方 FAQ 的路径是
Settings → Advanced → Power Management Setup → Onboard Function LED Control → Off。但它是否连 JRAINBOW 针一起关我没验证,OpenRGB 这条路能覆盖接在针上的风扇,所以没走 BIOS。
联力 TL LCD:按协议直接发 HID 报文
OpenRGB、liquidctl、两个 Thermalright 项目都不认 04fc:7393。真正有用的是 byrdltd/whyLIAN,一个 Rust 写的联力设备 Linux 驱动,crates/lianli-devices/src/tl_lcd.rs 里是逆向出来的协议:
//! Protocol uses HID Output Reports (Report ID 0x02, 512 bytes).//! 11-byte header: [reportId, cmd, dataSize(4 BE), packetNum(3 BE), payloadLen(2 BE)]const CMD_LCD_CONTROL: u8 = 64; LcdSetting = 5,
fn set_brightness(&self, brightness: u8) -> Result<()> { let mut payload = [0u8; 11]; payload[0] = LcdControlMode::LcdSetting as u8; payload[4] = brightness.min(100); payload[5] = 30; // fps payload[6] = self.rotation as u8; self.send_command_with_response(CMD_LCD_CONTROL, &payload)?;不想为四块屏编一个 Rust 项目,照协议写了个纯 hidraw 的 Python 脚本,零依赖:
def build(cmd, payload, total=None, pkt_num=0): total = len(payload) if total is None else total hdr = struct.pack(">BBIxxxH", 0x02, cmd, total, len(payload)) hdr = hdr[:6] + pkt_num.to_bytes(3, "big") + hdr[9:] return (hdr + payload).ljust(512, b"\0")
for dev in lcd_nodes(): # 扫 /sys/class/hidraw/*/device/uevent 里 HID_ID=0003:000004FC:00007393 fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK) payload = bytearray(11); payload[0] = 5; payload[4] = brightness; payload[5] = 30 os.write(fd, build(64, bytes(payload))) resp = os.read(fd, 512) # select 等 2s发亮度 0,四块屏都应答:
/dev/hidraw10 handshake resp: 023c0000000000000000000100000000/dev/hidraw10 set brightness=0 resp: 02400000000b000000000b0501000000(hidraw9/11/12 同)响应第二字节 0x40 回显命令号,payload 里 05 01 是模式 5 加状态 1。目视确认四块屏全黑。
顺便说两条走不通的路:uhubctl 报这块主板的 USB hub 不支持按口断电;把 LCD 设备 USB autosuspend 挂起(power/control 写 auto)能让内核把设备挂起,但没等到验证效果就被协议方案取代了。
做成开机自启并扛住挂起恢复
[Unit]Description=Turn off RGB LEDs via OpenRGBAfter=multi-user.target systemd-udev-settle.service
[Service]Type=oneshotExecStartPre=/sbin/modprobe i2c-devExecStart=/home/user/openrgb/squashfs-root/AppRun --noautoconnect -d 0 -m off -d 1 -m off -d 2 -m off -d 3 -m offExecStart=/home/user/openrgb/squashfs-root/AppRun --noautoconnect -d 4 -m direct -c 000000ExecStart=/home/user/openrgb/squashfs-root/AppRun --noautoconnect -d 4 -m static -c 000000ExecStart=-/usr/bin/python3 /home/user/openrgb/tl_lcd_brightness.py 0
[Install]WantedBy=multi-user.target设备序号是 OpenRGB 按 I2C 先、HID 后排的,换硬件要重新 --list-devices。
第一次挂起恢复后主板灯全回来了。内核日志里 usb 1-2: reset full-speed USB device 说明 MSI 控制器在恢复时被复位,固件回到默认 Rainbow wave,hidraw 节点号也从 0 变成了 13。一个”恢复后 5 秒重跑服务”的 hook 跑早了。最后用了两层保障。
第一层 udev:控制器的 hidraw 设备每次出现(开机、恢复、USB 复位)都拉起服务,不依赖时序:
ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1462", ATTRS{idProduct}=="7e07", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rgb-off.service"ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="04fc", ATTRS{idProduct}=="7393", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rgb-off.service"第二层 suspend.target 的 hook 兜底:
[Unit]After=suspend.target hibernate.target hybrid-sleep.target[Service]Type=oneshotExecStart=/bin/sleep 8ExecStart=/bin/systemctl start rgb-off.serviceExecStart=/bin/sleep 10ExecStart=/bin/systemctl restart rgb-off.service[Install]WantedBy=suspend.target hibernate.target hybrid-sleep.target再挂起恢复一轮,30 秒后回读:内存 Off、主板 Static、LCD 应答正常。
按需运行:S3 挂起 + Wake-on-LAN + 显存保留
这台机器唯一的用途是跑 llama-server,一天真正在推理的时间很短。实测空闲功耗 GPU 18 W 加 CPU 包 1.7 W,加上主板、内存、四块 LCD 风扇、电源损耗,整机估计 55-70 W。按 Con Ed 布鲁克林住宅综合电价约 32¢/kWh,常开一年 150-190 美元。推理本身反而不值钱:每千 token 1.17 Wh,一百万 token 0.37 美元。
问题在于挂起会丢显存。llama-server 占着 15.7GB 显存,普通 S3 恢复后 CUDA 上下文全废,服务得重启,重新加载要几十秒。NVIDIA 驱动有个开关能在挂起前把显存整个存到磁盘(或内存页缓存)、恢复后原样放回:
grep -E "PreserveVideoMemoryAllocations|TemporaryFilePath" /proc/driver/nvidia/params# PreserveVideoMemoryAllocations: 1# TemporaryFilePath: "/var"NVIDIA 文档把这个特性标为实验性、默认关闭,需要在 /etc/modprobe.d/ 里加 options nvidia NVreg_PreserveVideoMemoryAllocations=1 NVreg_TemporaryFilePath=/var。Ubuntu 24.04 的 nvidia 驱动包(nvidia-graphics-drivers-kms.conf)已经写好了,nvidia-suspend.service 和 nvidia-resume.service 也已启用,不用重启。存显存的路径要有 16GB 以上空间且不能是 tmpfs。
其余配置:
cat /sys/power/mem_sleep # s2idle [deep] → 支持真正的 S3sudo nmcli connection modify netplan-enp7s0 802-3-ethernet.wake-on-lan magic # WoL 持久化sudo ethtool -s enp7s0 wol gecho enabled | sudo tee /sys/class/net/enp7s0/device/power/wakeupgsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing # 别让 GNOME 抢着挂起Mac 上的唤醒脚本,发魔术包然后等服务健康:
python3 - "$MAC" <<'PY'import socket, sysmac = bytes.fromhex(sys.argv[1].replace(":", ""))pkt = b"\xff" * 6 + mac * 16s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)s.sendto(pkt, ("192.168.1.255", 9))PYuntil curl -sf -m 2 "http://192.168.1.x:8080/health" | grep -q ok; do sleep 1; done实测一轮:systemctl suspend 后 25 秒 ping 不通,发魔术包,17 秒后 8080 端口健康,llama-server 进程 PID 不变,显存 15.7GB 原样,接着生成 decode 75 tok/s。BIOS 一个字没改,网卡本来就支持 pumbg。
空闲自动挂起用一个 5 分钟的 systemd timer,判断四个条件连续 30 分钟都满足:llama-server /metrics 里的 token 计数没变、requests_processing 为 0、who 没有登录会话、GPU 利用率低于 20%:
metrics=$(wget -qO- --timeout=3 http://127.0.0.1:8080/metrics)tokens=$(printf '%s\n' "$metrics" | awk '/^llamacpp:(prompt_tokens_total|tokens_predicted_total) /{s+=$2} END{print s+0}')inflight=$(printf '%s\n' "$metrics" | awk '/^llamacpp:requests_processing /{print $2}')sessions=$(who | wc -l)gpu_busy=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits)# 任一有活动就刷新 last_active,写到 /run 下的状态文件;空闲 ≥30min 则 systemctl suspendSSH 连着算活跃,不会在用的时候睡。本机登录了 GNOME 桌面的话 who 会一直有会话,自动挂起永远不触发,这是已知限制。
挂起状态整机约 3 W,一年不到 10 美元。
经验总结
- USB 字符串里的 “TL” 是联力,不是 Thermalright。
0416:7372+04fc:7393这对 ID 直接搜 Lian Li。 - OpenRGB 对 MSI 185-byte 控制器要先 Direct 再 Static,否则写入被忽略,而且回读会告诉你。
- 联力 TL LCD 关屏 = LcdSetting 命令亮度 0,协议在 whyLIAN 的
tl_lcd.rs,用 hidraw 十几行 Python 就能发。 - S3 恢复会复位 USB 灯控制器,靠 sleep 几秒的 hook 不可靠,udev 按设备出现触发才稳。
- NVIDIA
PreserveVideoMemoryAllocations让 CUDA 进程跨挂起存活,Ubuntu 驱动包已默认配好;配上 WoL,一台 LLM 服务器可以 20 秒内从 3 W 唤醒到可用。 - 远程
pkill -f "关键字"会匹配到自己的 ssh 会话把自己杀掉(本次踩了三回),用pgrep -f "^命令"锚定行首。