机器: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

这一行做两件事:

  1. pkill -9 -f llama 强杀正在进行的 llama-server / 模型加载,立刻释放内存(赶在加载完成、机器噎死之前)。
  2. 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,不是 ollamagrep 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-filessystemctl --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 内存就是系统内存:它出现在 freeused 列里,但在普通的 Linux 进程视图里哪里都看不到

  • 不在进程 RSS 里(那个 node 的 RSS 只有约 1.1 GiB,不是 27)。
  • 不在 /proc/meminfoAnonPages / 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. 教训,一屏看完

  1. 会死机的操作不要开机自启。 任何加载模型的服务,不只是名字里带你的模型的,也不只是你记得自己写过的,都是能把机器搞成砖的操作。别 systemctl enable。人坐在机器前面(或 SSH 进去)能看着第一次加载成功的时候,才手动启动模型加载。开机应该只拉起桌面、ssh 和别的轻东西;救援窗口只有在自启动不抢在你到场之前把它吃掉的时候,才是金的。
  2. 机器卡在 fork 上限时,重启。 重启会重新打开 30 秒救援窗口,而不是跟一个 fork 不了的内核死磕。别神化 SysRq 组合键,它们是兜底,不是方案。
  3. 黄金窗口里,一行结束战斗: sudo pkill -9 -f <model>; sudo systemctl disable --now <service>,拿到 shell 立刻敲。内存几秒就释放。
  4. 永远不要 enable 两个加载同一个大模型的服务。 一次开机只加载一个,明确写出来。
  5. 任何 mmap 超过机器内存 70% 左右的服务,MemoryMax= 是必须的。 没有它,OOM 杀手按自己的启发式挑目标,可能先杀 sshd。
  6. After=graphical-session.target 延迟加载(或 After=network-online.target ssh.service),让救援通道在加载窗口期间保持存活。
  7. Linger 是朋友也是敌人。 enable-linger 会让用户服务自启,包括你忘了自己 enable 的第二份模型加载,以及过去的你(或安装器)接上的 LM Studio server。审计 loginctl show-user 和所有 --user 单元,不要只看你记得自己写的那些。
  8. 别信静默的 grep。 套用写给别的产品的教程时,用 systemctl cat <name> 确认服务名存在,用模型家族的 regex 放宽搜索,并且查 --state=failed,真凶一直藏在那里。
  9. 统一内存硬件上,free 会骗你谁在吃机器。 GPU 钉住的内存算在 freeused 里,但不在进程 RSS、/proc/meminfo 标准字段、甚至 cgroup memory.current 里。拿 used 去对进程列表,缺口大的就是 GPU 内存,用 nvidia-smi --query-compute-apps=pid,used_memory 找它。(GB10 上聚合的 Memory-Usage 行显示 "Not Supported",按 PID 查的那行才管用。)
  10. 救援命令要贴在服务器本体上,不要贴在遇到事时你根本连不上的远程 wiki 上。
  11. 救援后要验证,不要假设。 systemctl is-enabled 两个层级都查;free -huptime;统一内存机器还要 nvidia-smi --query-compute-apps

7. 救援配方,复制粘贴

一台 fork 卡死的机器上:

  1. 重启(复位键、硬关机,或者还能回显的 tty 上 Ctrl+Alt+Del)。目标是回到开机后 30 秒的窗口。

  2. 拿到任何 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
    
  3. 如果重启后还是来不及拿到可用的 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 加固幸存者;救援命令贴在服务器上。就这些。

posted on 2026-08-14 10:57  HOTSFMOC  阅读(2)  评论(0)    收藏  举报