机器:DGX-Spark(Grace ARM + Blackwell GPU,121 GiB 统一内存),跑 115 GB 的 DeepSeek-V4-Flash-0731 GGUF,推理框架 llama.cpp。
TL;DR
一个配错的自启动服务在开机过程中就把 115 GB 模型加载进统一内存。大约 30 秒后机器彻底没法用:SSH 超时、图形界面冻结、连物理终端都只能回显你敲的字符,新进程根本 fork 不出来。
修复只需要一次重启加两条命令,在开机后 30 秒的窗口里敲进去。搞清楚状况之后,整个救援大约花了一分钟。
根本原因(一句话):机器上有两个 llama-ds4-0731.service 单元(一个用户级、一个系统级),而且都 enabled。开机时两个服务同时 mmap 同一个 115 GB 模型,在 sshd 应答之前就把机器噎死了。还有第三个自启动,lmstudio-server.service,挂在我们的用户下通过 linger 静默启动,每次开机都钉住大约 27 GiB 的 GPU/统一内存。这就是为什么把两个 llama 服务都禁用之后,一次"干净"的重启仍然没给手动 profile-A 验证留出足够内存,机器又死了一次(见 §4.3)。
真正管用的救援不是内核文档里那套 SysRq 魔法键,而是普通的杀进程加 systemctl disable。往下看为什么,以及为什么只有这个办法能可靠收尾。
1. 怎么判断机器是"卡死"而不是"死了"
症状很容易被误读成"机器坏了",其实不是:
| 信号 | 实际含义 |
|---|---|
| ping 通,但 SSH 报 Connection timed out | 网络栈活着;sshd 无法 fork 出会话来接你 |
| SSH 报 Connection refused | ssh.socket 的端口也被改过;另一个问题,但同一族根因 |
| 图形界面在登录中途冻结 | GNOME/X 也 fork 不了 |
| 物理 tty 能敲几条命令然后停住 | bash 还在缓冲输入,但 fork() 失败,典型的 OOM / 分配上限行为 |
经验法则:ping 通但没人能接受连接,说明内核还活着,只是在讨内存。 先别急着拔电源,重启对你有利,因为它会重新打开救援窗口(见 §2)。
2. 黄金 30 秒窗口,以及重新打开它的那次重启
这一步大多数教程要么跳过要么讲错。统一内存机器上,自启动服务不是瞬间占满内存的:加载 115 GB 模型需要时间(几十秒的文件 I/O 加页表建立)。所以开机后的一小会儿,在自启动服务把内存全部抢走之前,有一个大约 30 秒的窗口,fork() 还管用。能在这个窗口里敲进一条命令,你就赢了。
问题在于:模型加载一完成,窗口砰地关上,什么都 fork 不了。shell、SSH 会话、物理 tty 上的大多数命令,全都不行。所以已经卡死时,正确的动作是:
第 1 步:重启
机器已经卡死、敲什么都不执行的时候,直接断电重启。这不丢人,干净的重启正是重新打开救援窗口的办法。GUI 冻结时可能要用复位键或硬关机;tty 不执行命令时可以试 Ctrl+Alt+Del。别想着在一台 fork 不动的机器上耍聪明,回到黄金窗口才是正事。
(内核的 magic SysRq 键,Alt+SysRq + f 触发 OOM 杀进程,或者 REISUB 序列,有时确实能在不重启的情况下换回一个 shell。它们是万不得已时的后备,比如没有远程电源的远端机器。这次救援没用上它们;知道它们存在有用,指望它们就错了。)
第 2 步:拿到 shell 立刻终止加载
机器一起来的瞬间,哪怕是图形登录界面、哪怕是已经开着的终端,把这一行敲进去,回车,要快:
sudo pkill -9 -f llama; sudo systemctl disable --now llama-ds4-0731
这一行做两件事:
pkill -9 -f llama强杀正在进行的 llama-server / 模型加载,立刻释放内存(赶在加载完成、机器噎死之前)。systemctl disable --now llama-ds4-0731停掉服务,同时把它从自启动列表里移除,下次开机不会再卡。
只要能在加载完成前把这行敲进去,机器就回来了,不用打任何架。全部诀窍就这么多。
第 3 步:确认救援成功
free -h
uptime
used 应该远低于 total,1 分钟负载 10 秒内回落到个位数。看到就是成了。
兜底:如果窗口每次都赶不上
如果重启后机器卡得太快、来不及敲救援命令(少见,说明自启动比 multi-user.target 还早,或者你的机器窗口更短),这时候才用控制台兜底:
- 文本 tty 而不是 GUI:
Ctrl + Alt + F3。GUI 本身吃内存还容易跟着你一起卡;F3 上的裸login:提示符不启动桌面会话,能给你的命令多留点内存。 - 内核 OOM 杀手: 按住
Alt + SysRq再按f。让内核立刻杀掉点什么来回收内存,往往能让fork()重新工作,然后你就能跑第 2 步的救援命令了。 - 最后一招干净重启:
Alt + SysRq然后r-e-i-s-u-b(回收键盘、结束进程、同步、只读重挂、卸载、重启)。
这些是降落伞。主救援方案是重启加杀进程加 disable 那一行。别从降落伞里学救援。
3. 为什么"我把服务禁用了"还是不够安全
机器上有两份服务文件:
/etc/systemd/system/llama-ds4-0731.service(系统级,WantedBy=multi-user.target,不带 draft 模型)/home/xLab/.config/systemd/user/llama-ds4-0731.service(用户级,WantedBy=default.target,带 speculative decoding 用的 draft 模型)
两份都 enabled。系统级那份先启动(multi-user.target 时),用户级紧接着启动(因为开了 loginctl enable-linger)。两次模型加载,同一块内存池。
只禁一个是没用的,两个层级都要确认关掉:
systemctl is-enabled llama-ds4-0731
systemctl --user is-enabled llama-ds4-0731
# 两个都应该输出: disabled
如果你的用户服务确实需要在开机时自启(无头生产环境),还需要开 linger,否则没有登录用户去跑用户管理器:
sudo loginctl enable-linger <user>
loginctl show-user <user> | grep Linger # Linger=yes
但 linger 是把双刃剑:它会把你忘了已经 enable 的每一个 --user 服务都自启起来,包括第二份模型加载。要审计。
4. 第三个我没写过的自启动,以及名字对不上的坑
修机器的时候,我从一篇通用的"禁用 Ollama 自启动"教程里抄命令。一半命令返回空,我差点就下了"没问题"的结论。其实问题大了。教训分两部分。
4.1 别人的自启动也可能在吃机器
systemctl list-units --state=failed 暴露了第三个自启动,不是我写的,跟我的 llama 配置毫无关系:
● lms-load.service loaded failed failed Load lms qwen model after boot delay
同一台机器上的另一个用户配了个 systemd 服务,开机后自动把 Qwen 模型加载进 LM Studio。它自己在失败,但它仍然是另一个在抢同一块 121 GiB 内存池的大模型自启动。在内存紧张的共享机器上,每一个大模型自启动都是所有人的问题。 要审计整台机器,不能只看自己的服务:
systemctl list-units --state=failed
systemctl list-unit-files | grep -iE 'llama|ds4|deepseek|lms|ollama|qwen|model'
systemctl --user list-unit-files | grep -iE 'llama|ds4|deepseek|lms|ollama|qwen|model'
4.2 grep 没输出不等于"没有匹配"
教程让我跑 systemctl list-unit-files --state=enabled | grep -i ollama。返回空。看起来像"Ollama 自启动没开",我差点就停在这了。真相不一样:
- 这台机器上真正的服务名是
llama-ds4-0731,不是ollama。grep ollama当然没有东西可匹配,但这完全不能说明没有别的大模型服务被 enable。 - grep 没匹配时退出码是 1、什么都不打印。"你要找的东西不存在"和"你搜错了名字"看起来完全一样,都是静默。
systemctl cat <怀疑的名字>更严格:单元不存在时明确打印No files found for <name>.,这个报错可以信。存在的话,直接吐出单元文件。
给所有想把别人教程(Ollama、vLLM、LM Studio、llama.cpp……)套到自己机器上的人:
- 在相信一次干净的 grep 之前,先确认名字真的存在。 跑
systemctl cat <name>,明确的 "No files found" 才是信号,空的 grep 不是。 - 把搜索范围放宽。 用
grep -iE 'llama|ds4|deepseek|lms|ollama|qwen|model|gguf'同时扫systemctl list-unit-files和systemctl --user list-unit-files,因为你不知道别人把自启动命名成了什么。 - failed 状态的单元也要查。
--state=enabled会把已经失败的东西过滤掉,而恰恰是最可能出问题的那一个。我就是从systemctl list-units --state=failed里找到 Qwen 自启动的。
另外提醒看老教程的 Ubuntu 用户:24.04 默认已经没有 /etc/rc.local 了;裸的 crontab -l 返回 no crontab for <user> 也是正常空状态。这两种"静默"都不代表问题解决了。
4.3 一个在运行而不是 failed 的自启动,照样能搞死你
4.1 里那个 lms-load 单元容易发现,因为它最后是 failed。难办的是另一种:自启动活得好好的,active (running),永远不会出现在 --state=failed 里,说"是别人的"也不太对,但它就是每次开机都吃掉一块共享内存池的模型服务。
这台机器上的实例:两个 llama-ds4-0731 单元都禁用后,我们重启,free -h 显示 used 30 GiB,没有模型在跑,我们以为就是桌面占的。不是。真实的分账是这样的:
PID 3354 xLab node /home/xLab/.lmstudio/.internal/utils/node
nvidia-smi --query-compute-apps=pid,used_memory --format=csv
3354, 26899 MiB ← 显卡上钉着约 27 GiB
这是我们自己用户(xLab,经 linger 自启)下的 lmstudio-server.service,它的 node worker 在"0% 利用率"下仍然占着 26.9 GiB GPU 内存。在 Grace-Blackwell 统一内存机器上,GPU 内存就是系统内存:它出现在 free 的 used 列里,但在普通的 Linux 进程视图里哪里都看不到:
- 不在进程 RSS 里(那个 node 的 RSS 只有约 1.1 GiB,不是 27)。
- 不在
/proc/meminfo的AnonPages/Shmem/Slab里(加起来约 1.9 GiB,剩下的"used 30 GiB"根本对不上账)。 - 不在 cgroup 的
memory.current里(lmstudio 的 cgroup 报了约 3.9 GiB,GPU 那 27 GiB 在这本账外面)。 - 只在
nvidia-smi --query-compute-apps=pid,used_memory里。(GB10 上聚合的Memory-Usage行显示 "Not Supported",按 PID 查的那行才管用。)
这就是所有统一内存设备的陷阱:free 会骗你,让你不知道是什么在吃机器。 当你拿着 used ≈ 30 GiB 去对进程列表、而所有进程 RSS 加起来只有约 2 GiB 时,消失的约 28 GiB 几乎可以肯定是钉在 GPU 上的内存,只能通过显卡厂商的进程 API 看到。我们的 profile A 在干净启动(没有 LM Studio 预留)下验证过 31 tok/s;少了这 27 GiB 之后,profile A 约 111 GiB 的峰值内存超预算约 22 GiB,验证中途机器跟第一次事故一模一样地死了:同样的 fork 上限、同样的连不上 SSH、同样的 ping 通但没人接受连接。
由此得出的规则:
-
每个模型服务自启动都是你的问题,包括过去的你(或者 LM Studio 安装器)在你自己的用户下配的 LM Studio server。要把每个都当成崩溃风险,而不是只盯着名字里带自己模型的那些。
-
统一内存硬件上,显卡也要审计。 加载自己的模型之前先做预检:
nvidia-smi --query-compute-apps=pid,used_memory --format=csv # 现在谁在 GPU 上 awk '/MemAvailable/{print "available:", $2/1024 " MiB"}' /proc/meminfo如果
available减去最大的那个 GPU 钉子户,还小于你模型的峰值占用加约 8 GiB 余量,先把那个服务停掉(systemctl --user stop lmstudio-server.service),别直接在旁边加载。
5. 加固幸存下来的服务
如果确实想让模型开机自启,就必须让它故障安全,坏日子也能收场。编辑单元文件,加上资源护栏:
[Unit]
Description=DeepSeek V4 Flash 0731 via llama.cpp
After=graphical-session.target # 先让桌面/ssh 起来
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/<user>/llama.cpp
# 加载前丢缓存,告诉内核别死命压缩
ExecStartPre=/usr/bin/bash -c 'echo 1 > /proc/sys/vm/drop_caches; sysctl -w vm.compaction_proactiveness=0 vm.swappiness=10'
ExecStart=/home/<user>/llama.cpp/build/bin/llama-server \
-m /srv/models/.../model.gguf \
-ngl 999 -fa on --jinja \
-c 131072 -np 1 --host 0.0.0.0 --port 8000
# --- 故障安全 ---
Restart=on-failure
RestartSec=10
TimeoutStartSec=1800 # 冷加载留约 5 分钟
MemoryMax=118G # 上限:OOM 杀手杀的是模型,不是 sshd/gnome
OOMScoreAdjust=-500 # 优先杀模型而不是系统服务
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7
[Install]
WantedBy=default.target
把你从医院门口拉回来的两行:
MemoryMax=118G:加载要超这个数,内核杀的是这个服务,不是你的 sshd。你重启进一台干净机器,看到的是一个 failed 但能诊断的服务,而不是一块砖。After=graphical-session.target:桌面和ssh.socket在模型开始加载之前起来。就算加载要 5 分钟、期间内存被钉住,你的救援通道(重启,黄金窗口里杀进程加 disable)依然开着。
重载配置,然后做一次真正的冷启动验证,再宣布安全:
systemctl --user daemon-reload
systemctl --user enable --now llama-ds4-0731
# 然后: sudo reboot, 等 6 分钟, 从另一台机器 SSH 试试
6. 教训,一屏看完
- 会死机的操作不要开机自启。 任何加载模型的服务,不只是名字里带你的模型的,也不只是你记得自己写过的,都是能把机器搞成砖的操作。别
systemctl enable。人坐在机器前面(或 SSH 进去)能看着第一次加载成功的时候,才手动启动模型加载。开机应该只拉起桌面、ssh 和别的轻东西;救援窗口只有在自启动不抢在你到场之前把它吃掉的时候,才是金的。 - 机器卡在 fork 上限时,重启。 重启会重新打开 30 秒救援窗口,而不是跟一个 fork 不了的内核死磕。别神化 SysRq 组合键,它们是兜底,不是方案。
- 黄金窗口里,一行结束战斗:
sudo pkill -9 -f <model>; sudo systemctl disable --now <service>,拿到 shell 立刻敲。内存几秒就释放。 - 永远不要 enable 两个加载同一个大模型的服务。 一次开机只加载一个,明确写出来。
- 任何 mmap 超过机器内存 70% 左右的服务,
MemoryMax=是必须的。 没有它,OOM 杀手按自己的启发式挑目标,可能先杀 sshd。 - 用
After=graphical-session.target延迟加载(或After=network-online.target ssh.service),让救援通道在加载窗口期间保持存活。 - Linger 是朋友也是敌人。
enable-linger会让用户服务自启,包括你忘了自己 enable 的第二份模型加载,以及过去的你(或安装器)接上的 LM Studio server。审计loginctl show-user和所有--user单元,不要只看你记得自己写的那些。 - 别信静默的 grep。 套用写给别的产品的教程时,用
systemctl cat <name>确认服务名存在,用模型家族的 regex 放宽搜索,并且查--state=failed,真凶一直藏在那里。 - 统一内存硬件上,
free会骗你谁在吃机器。 GPU 钉住的内存算在free的used里,但不在进程 RSS、/proc/meminfo标准字段、甚至 cgroupmemory.current里。拿used去对进程列表,缺口大的就是 GPU 内存,用nvidia-smi --query-compute-apps=pid,used_memory找它。(GB10 上聚合的Memory-Usage行显示 "Not Supported",按 PID 查的那行才管用。) - 救援命令要贴在服务器本体上,不要贴在遇到事时你根本连不上的远程 wiki 上。
- 救援后要验证,不要假设。
systemctl is-enabled两个层级都查;free -h;uptime;统一内存机器还要nvidia-smi --query-compute-apps。
7. 救援配方,复制粘贴
一台 fork 卡死的机器上:
-
重启(复位键、硬关机,或者还能回显的 tty 上
Ctrl+Alt+Del)。目标是回到开机后 30 秒的窗口。 -
拿到任何 shell 的瞬间,GUI 终端、tty、SSH 都行,只要你够快,马上跑:
sudo pkill -9 -f llama sudo systemctl disable --now llama-ds4-0731 systemctl --user disable --now llama-ds4-0731 free -h && uptime -
如果重启后还是来不及拿到可用的 shell,才退回控制台逃逸:
Ctrl + Alt + F3要一个裸 tty(省掉桌面的内存开销)Alt + SysRq+f触发内核 OOM 杀手- 最后手段
Alt + SysRq+r-e-i-s-u-b干净重启
启动之后,重新 enable 任何东西之前:
# 审计(关键词放宽,你不知道别人把服务命名成了什么)
systemctl --user list-unit-files | grep -iE 'llama|ds4|deepseek|lms|lmstudio|ollama|qwen|model'
systemctl list-unit-files | grep -iE 'llama|ds4|deepseek|lms|lmstudio|ollama|qwen|model'
systemctl list-units --state=failed # ← 真凶经常藏在这里
loginctl show-user $USER | grep Linger
# 统一内存机器还要审计 GPU:'free used' 藏得住 GPU 钉住的内存
nvidia-smi --query-compute-apps=pid,used_memory --format=csv # 现在谁占着 GPU
awk '/MemAvailable/{print "available MiB:", $2/1024}' /proc/meminfo
# 名字不存在会明确报错:相信静默之前先用 systemctl cat 验证
systemctl cat llama-ds4-0731.service # 不存在会大声报错,存在则吐出单元文件
systemctl --user cat llama-ds4-0731.service
systemctl --user cat lmstudio-server.service # 你自己用户下最容易被漏掉的模型自启动
# 清理重复
sudo systemctl disable --now <每个系统级匹配>
systemctl --user disable --now <每个用户级匹配>
# 留一个,删掉另一个,加上 MemoryMax 和 After 护栏,重启验证
8. 事故在 journal 里的样子
参考用,这是我重新进去之后讲完整个故事的原始日志行:
10:00:39 systemd[1]: Starting llama-ds4-0731.service ... (system level, no draft)
10:00:39 systemd[1]: Started llama-ds4-0731.service ...
10:01:23 systemd[1]: llama-ds4-0731.service: Main process exited,
code=killed, status=9/KILL ← 我们在 tty 上的 pkill
10:01:33 systemd[1]: Scheduled restart job (RestartSec=10)
10:01:37 systemd[1]: Stopped llama-ds4-0731.service ← disable --now 生效
44 秒的寿命,然后一次击杀、一次重试、最终一次 disable。整个模式就是这样。看到 "Main process exited … KILL" 后面跟着 "Scheduled restart job",说明有人或 OOM 杀手在跟 Restart=on-failure 打架。只有 disable 能结束这一切,杀进程只是买来下一次重启循环。
一句话总结:会死机的操作不要开机自启。 模型加载器永远不要自启;fork 卡死就重启打开窗口,开机后 30 秒内敲 pkill -9 -f <model>; systemctl disable --now <service>;把整台机器审计一遍(systemd 两个层级、--state=failed、linger,统一内存硬件还要 nvidia-smi --query-compute-apps),找重复的和别人的模型自启动;用 MemoryMax= 和 After=graphical-session.target 加固幸存者;救援命令贴在服务器上。就这些。
浙公网安备 33010602011771号