夜雨聆风学习资料网

ARTICLE · 1285576

【AI工具链】跟着敲一遍⑫:8 核跑 vLLM 翻车记

【AI工具链】跟着敲一遍⑫:8 核跑 vLLM 翻车记
      
赞
AI工具链 | 每日一练 | 实用工具 |  职业发展  
                                                  感谢您抽出..阅读本文               
    系列:跟着敲一遍 · 21 天本地 AI 工具链 Day 12(第 12/21 篇) 前情:⑪ 给 Open WebUI 装上了 Filter;⑫ 换一个更"生产级"的推理后端。实测环境:openEuler 22.03 / VMware 虚拟化 / 8 vCPU / 15GiB / 无 GPU / 无 AVX512-BF16
     这篇不是教程,是翻车实录——最后跑通了,但代价是三次失败和两次整机假死。我原本打算写一篇"无 GPU 跑通 vLLM"的顺滑教程。结果在这台 8 核虚拟机上,前后折腾了近 6 个小时,翻了三次车,其中两次把整台机器拖到失去响应——SSH 断开、连登录密码校验都卡 22 秒。一开始我以为是内存不够,改成 float32;以为是 CPU 超订,加了 --cpus=4 围栏;以为是磁盘 I/O 慢,去查了 vmstat;以为是宿主机调度问题,查了快照、查了 vCPU 超卖。每一层都有"证据支持",每一层又都被下一个数据推翻。
    最后的真相有两层,而且第二层是在修好第一层之后才浮现的:这台虚拟机压根没装 VMware Tools —— 没有时间同步、没有内存气球,宿主一回收 guest 内存,guest 内部就是一段无规律的 20 秒停顿。表现是五个互不相干的子系统同时超时,而 dmesg 里一条磁盘错误都没有。1.5B 的 float32 峰值需要 11.1 GiB,可用只有 9.32 GiB —— 差 1.8 GiB。这个缺口在修好第一层之前,完全被"整机假死"掩盖了。
    最后降到 0.5B 才跑通。但真正值钱的不是"跑通了",而是这次踩到的几个坑,网上几乎没人写过。这篇的价值在于:讲"无 GPU 跑 vLLM"的文章一大把,讲"为什么我的虚拟机一跑大模型就整机假死、怎么一步步定位"的几乎没有。 后者才是真正会困住人的地方。
今日主要内容概要:
1.起点:一个看着很正常的开始
2.第一次翻车:dtype 静默降级,不报错也不退出
3.第二次翻车:围栏加到了,机器还是死了
4.第三次翻车:五个子系统同时超时
5.定位方法论:分层计时 + 隔离实验
6.真凶:VMware Tools 缺失的三个连锁后果
7.第四次:降到 0.5B,跑通了
8.正确选型:这类机器上该用什么
9.七个坑
10.动手练习题
01
症状:Pipelines 页面空白

 环境先摆清楚:

[root@LinuxPythonWorkshop ~]# nproc 8 [root@LinuxPythonWorkshop ~]# grep -o -m1 'avx512_bf16' /proc/cpuinfo || echo "NO-BF16" NO-BF16 [root@LinuxPythonWorkshop ~]# free -h                total        used        free      shared  buff/cache   available Mem:             15Gi       3.4Gi       6.9Gi       26Mi       5.2Gi       11Gi Swap:      5.0Gi        89Mi       4.9Gi 

看着完全够:8 核、15G 内存、还有 11G 可用。Dify 全套 17 个容器正跑着,load average: 0.18,D 状态任务 0 个,连续 17 小时零异常。

图 1:17 小时空载基线。17 个容器全部 Up,load 0.18,D 状态任务 0 个——平台此刻是健康的。

图 2:available 11Gi,Swap 未使用。这是整个排查里最不该被怀疑的一组数字。

[root@LinuxPythonWorkshop ~]# docker inspect vllm-server --format 'IMAGE={{.Config.Image}} ENTRYPOINT={{json .Config.Entrypoint}} CMD={{json .Config.Cmd}} ENV={{json .Config.Env}} ...' IMAGE=vllm-openai-cpu:day12 ENTRYPOINT=["vllm"] CMD=["serve","/root/.cache/modelscope/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master","--host","0.0.0.0","--port","8000","--dtype","bfloat16","--max-model-len","2048","--max-num-seqs","8","--enforce-eager"] ENV=["VLLM_TARGET_DEVICE=cpu","VLLM_CPU_KVCACHE_SPACE=1",...] 

图 3:把容器的原始配置导出成文本,后面每次改动都以这份为准做"只改一处"的对照实验。

起手第一坑:镜像选错,白拉 8.8GB

真正开始之前,还有一个不在"翻车三次"里的坑,因为它是纯浪费流量:

按官方命令 docker pull vllm/vllm-openai,拉下来 8.8GB,启动直接报 No CUDA driver。

原因:vllm-openai 带 openai 但没带 -cpu,是 CUDA 构建版。没有 GPU 的机器,拉了必然起不来,还白烧 9GB 流量。

镜像
下载(压缩)
落盘(解压)
适用
vllm/vllm-openai:latest
8.8GB
20GB+
必须有 NVIDIA GPU
vllm/vllm-openai-cpu:latest-x86_641.76GB
5.4GB
无 GPU 的 x86 机器

💡 判定口诀:仓库名带 -cpu 才是 CPU 版。

docker images 显示的是解压后体积(5.4GB),拉取时显示的是压缩体积(1.76GB)。两者都对,别被 5.4GB 吓到。

我按标准教程起了 vLLM 容器:

docker run -d --name vllm-server --network lpw-ai -p 8000:8000 \   --ipc=host --security-opt seccomp=unconfined --cap-add SYS_NICE \   -e VLLM_TARGET_DEVICE=cpu -e VLLM_CPU_KVCACHE_SPACE=1 \   -v /root/.vllm/models:/root/.cache/modelscope \   --entrypoint vllm vllm-openai-cpu:day12 \   serve /root/.cache/modelscope/models/Qwen--Qwen2.5-1.5B-Instruct/snapshots/master \   --host 0.0.0.0 --port 8000 --dtype bfloat16 \   --max-model-len 2048 --max-num-seqs 8 --enforce-eager 

📖 初学者讲解:这条命令本身没问题,用的是本地 ModelScope 快照,不用联网下载模型;--ipc=host 共享 /dev/shm;seccomp=unconfined + SYS_NICE 是 CPU 后端绑核所需。

然后就是等待。

02
第一次翻车:dtype 静默降级,不报错也不退出
    现象

    容器 Up,日志在读权重,但端口死活不通:

    图 4:EngineCore 起来后,core_ids=[0,1,2,3,4,5,6]、reserved_cpus=[7] —— 注意这是绑核亲和性,不是 CPU 配额,两者不是一回事。

    [root@LinuxPythonWorkshop ~]# curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/v1/models 000 

    000 是 connection refused。第一反应是"还没加载完,等"。等了两分钟,还是 000。

    判断错在哪

    我第一反应是"CPU 在狂算":

    [root@LinuxPythonWorkshop ~]# docker stats --no-stream --format '{{.CPUPerc}}' vllm-server 4062.52% 

    这个数字是假的。 8 核机器的物理上限是 800%。docker stats 在容器刚启动、采样窗口不完整时会给出失真值。真正可信的证据在 docker top:

    [root@LinuxPythonWorkshop ~]# docker top vllm-server PID     USER   TIME  COMMAND 445326   root   0:31  vllm serve ... --dtype bfloat16 446189   root   0:54  VLLM::EngineCore 446721   root  29:14  VLLM::Worker 

    VLLM::Worker 累计烧了 29 分钟 CPU 时间,主进程只用了 31 秒。也就是说:真正在算的是 Worker,而它永远算不完。

    真凶

    [root@LinuxPythonWorkshop ~]# grep -o -m1 'avx512_bf16' /proc/cpuinfo || echo "NO-BF16" NO-BF16 

    这台 CPU 没有 AVX512-BF16 指令集。

    教科书上的说法是"会报 Unsupported dtype"。实际不会。 bf16 走软件模拟路径:每个算子先把 bf16 转成 fp32 算,再转回去,内存带宽和 CPU 开销直接翻倍。

    于是它的表现是:

    教科书预期
    实际表现
    抛 ValueError 退出
    不报错、不退出
    端口立刻返回错误码
    端口永不监听
    日志里能看到原因
    日志一个字都不提

    这就是它比直接报错更迷惑人的地方——静默降级没有任何信号。

    判据是什么

    两条命令就够了:

    docker top vllm-server                    # Worker 的 TIME 在涨,但服务没起 vmstat 1 3                                # bi 接近 0(磁盘没在读),us 极低 sy 极高 

    vmstat 那三行是决定性的:

    r  b   swpd   free   buff   cache   si so  bi  bo    in    cs  us sy id wa40 1 417084 188272 305594 6712684   0  0  47  92   393   484   2  3 93  133 1 417084 184576 305952 6712864   0  0   4  36 10098   551  26 74  0  029 0 417084 150844 305660 6703686   0  0   4   0  1912  1312   7 93  0  0
    • bi ≈ 4~47 块/秒
       → 磁盘没在读,权重早就读完了
    • sy = 74~93%
      ,us 只有 2~26% → CPU 绝大多数时间耗在内核态(线程争抢、缺页、调度),不是在算矩阵
    • r = 40
       → 8 核上 40 个可运行线程,5 倍超订

    💡 这一条最值钱:内存增长速率是唯一可靠的前进信号。当时我采样了 12 次:

    图 5:12 次内存采样。注意末行时间戳始终是 08:05:49 没变,但内存从 2.98Gi 一路涨到 4.54Gi——进度条用 \r 原地刷新,不产生新行。

    增速从 11 MB/s 降到 1.5 MB/s 并趋于平稳——说明权重读取已经结束,卡在了 process_weights_after_loading 和 warmup。

    不要拿"日志不动"当卡死信号,要拿内存增量当信号。

    修正

    --dtype bfloat16  →  --dtype float32 

    改完日志立刻给了确认:

    图 6:Upcasting torch.bfloat16 to torch.float32 —— dtype 改对了之后,日志里必须出现这条。它下面紧接着的 Available RAM: 6.98 GiB 也提醒了内存其实并不宽裕。

    INFO [model.py:2492] Upcasting torch.bfloat16 to torch.float32. INFO [model.py:2152] Using max model len 512 INFO [scheduler.py:307] Chunked prefill is enabled with max_num_batched_tokens=512. 

    Upcasting torch.bfloat16 to torch.float32 —— vLLM 明确告诉你,它按 float32 执行了。这就是正确姿势的判据:dtype 改了之后,日志里必须出现这条。

    03
    第二次翻车:围栏加到了,机器还是死了

    我以为剩下的问题只是内存。算了一笔账:

    项
    占用
    openEuler 基础
    ~1.0 GiB
    Dify 11 个容器
    ~3.4 GiB
    float32 权重(1.54B × 4 字节)
    5.75 GiB
    KV 缓存(VLLM_CPU_KVCACHE_SPACE=1)
    1.0 GiB
    运行时 + 激活值
    ~1.5 GiB
    合计~12.6 GiB

    15GiB 里只剩 2.4GiB 给页缓存,而权重文件本身 2.88GiB。加载瞬间还要同时容纳"正在构建的 fp32 张量"和"正在读的文件页",峰值约 8.6GiB——必然触发驱逐与重读。

    于是我只改围栏相关参数:

    --cpus=4 --memory=9g --memory-swap=9g   # 新增-e OMP_NUM_THREADS=4                    # 新增-e PYTHONUNBUFFERED=1                   # 新增,日志实时--dtype float32                         # 已改--max-model-len 512 --max-num-seqs 1    # 已改

    这已经是这台机器能给出的最保守配置:一半 CPU、一半内存、最小上下文、单并发。

    结果——

    16:45:41  MEM=6.245GiB / 9GiB   LOAD=14.78 16:47:20  Connection closed by foreign host 

    SSH 被对端关闭。

    这里有两个必须记住的教训

    教训一:--cpus=4 下 LOAD=14.78 是不可能出现的。

    围栏把 CPU 时间锁在 4 核,纯计算负载最多把 load 推到 4~6。14.78 只能意味着:大量进程卡在 D 状态等 I/O。D 状态进程也计入 loadavg。

    教训二:D 状态的进程收不到 SIGKILL。

    我执行 docker kill vllm-server,容器"看起来"停了。但 79 秒后:

    [20380.103193] C1 watchdog: soft lockup - CPU#1 stuck for 26s! [VLLM::Worker:477248] 

    VLLM::Worker 在 16:53 还活着——docker kill 是假成功。

    💡 D 状态是"不可中断睡眠",信号要等进程从内核调用返回后才能投递。它没返回,所以信号一直挂着。

    这解释了为什么当时连登录密码校验(unix_chkpwd)都会被卡 22 秒——那个进程读 /etc/shadow 也要走文件系统。

    📖 初学者讲解:这就是为什么"kill 掉了"和"真的死了"是两回事。一定要用 docker ps + ps -eo pid,stat,comm 双向确认,不要只看命令没报错。

    04
    第三次翻车:五个子系统同时超时

    到了 BMC 网页控制台(149.132),我看到了完整画面:

    图 7:容器已被 kill,但日志里 VLLM::Worker 仍在报 soft lockup——这是整个排查最关键的一张图。

    [20192.3585751]  C31 watchdog: BUG: soft lockup - CPU#3 stuck for 22s! [unix_chkpwd:478978] [20380.1031931]  C11 watchdog: BUG: soft lockup - CPU#1 stuck for 26s! [VLLM::Worker:477248] [19815.4475761][T477568] drm_kms_helper[1] *ERROR* [CRTC:38:crtc-0] flip_done timed out [19846.0416621][T477598] hub 2-2:1.0: hub_ext_port_status failed (err = -110) 

    图 8:task blocked for more than 122 seconds 连续出现,Tainted: G 说明加载过非 GPL 模块(与本问题无关)。

    另外还有 I/O 层的:

    [1848.265641][T63] INFO: task exc:454626 blocked for more than 122 seconds. [1848.265753][T63] Tainted: G  L 5.10.0-327.0.0.220.oe2203sp4.x86_64 #1

    hung_task_timeout_secs(默认 120 秒)触发,说明有进程在 D 状态超过 120 秒。调用栈:

    rsem_down_write_slowpath+0x323/0x490 do_reconfigure_mnt+0x6d/0x110 __se_sys_mount+0x165/0x1d0 

    一个 remount 操作卡在 mount 写信号量上。整个文件系统层被一个长时间不返回的操作堵住,后续所有碰挂载点的进程(Docker、syslog、tty)依次排队变成 D,loadavg 堆到 74。

    关键推理:为什么"五个子系统同时超时"无法用 CPU 忙解释

    时间
    现象
    子系统
    16:25
    soft lockup - CPU#1 stuck for 21s
    时钟/调度
    16:30
    hung_task
    ,74 个任务 D 状态 120s+
    块设备 I/O
    16:47
    SSH 断开(Connection closed by foreign host)
    网络
    16:48
    drm ... flip_done timed out
    、USB -110
    显示、USB

    显示、USB、网络、I/O、时钟——五个互不相干的子系统在同一时段全部超时。

    这不能用"vLLM 占了 CPU"解释(五个子系统不会同时因为 CPU 忙而集体失联);也不能用"某块盘坏了"解释(显示和 USB 不经过那块盘)。

    唯一能同时解释这五个现象的是:vCPU 在一段时间内完全没有被 hypervisor 调度。

    我走过的两个弯路

    弯路一:怀疑外接硬盘掉了。

    看到 usb 2-2: reset 就以为移动硬盘掉线了。但 lsusb -t 显示:

    图 9:usb 2-2 的 reset 记录横跨 16:07 到 16:32,正好覆盖崩溃区间;而所有 USB 设备都写着 "VMware Virtual"。

    VMware Virtual USB Mouse VMware Virtual USB Hub VMware Virtual Bluetooth Adapter 

    lsblk 只有一个块设备:sda 80G TRAN=spi HOTPLUG=0。这是 VMware 虚拟机,usb 2-2 是虚拟 USB 控制器在反复 reset,不是真外接盘。 根因从"盘坏了"变成"虚拟化层出问题了"。

    弯路二:怀疑 docker stats 的 4062% 是"40 线程狂算"。

    8 核物理上限 800%。这个数字不是 docker 的 bug,是 VM 停顿的副作用——当 guest 收不到时钟中断时,CPU 时间统计本身就会失真。

    📖 初学者讲解:在虚拟化环境里,docker stats、top、vmstat 的数字都可能是"症状"而不是"原因"。先看有没有跨子系统的共性,再去解释单个指标。

    05
    定位方法论:分层计时 + 隔离实验

     到这一步,我已经猜了五六轮,全被推翻。得换个方法。

    关键判断:这不是 vLLM 的问题

    有一个反证一直摆在那里却没被重视——

    容器被 kill 之后 79 秒,仍出现了 soft lockup ... [unix_chkpwd]。

    unix_chkpwd 是你登录时 PAM 用来校验密码的辅助进程,它只做一件事:读 /etc/shadow 比对哈希。这是一次微秒级操作。它能在内核里卡 22 秒,说明停顿与 vLLM 无关。

    于是思路从"vLLM 参数不对"转向"平台本身在周期性停顿"。

    分层计时:把"容器启动"拆成三层

    不再整体怀疑,改成逐层单独测量:

    # 第① 层:只测镜像读取 time docker run --rm --cpus=1 --memory=1g --network none \   --entrypoint python vllm-openai-cpu:day12 -c "print('ok')"  # 第② 层:测框架导入(重头戏) time docker run --rm --cpus=2 --memory=2g --network none \   --entrypoint python vllm-openai-cpu:day12 -c "import torch; print(torch.__version__)" 

    判读标准很干脆:

    第② 层 real
    结论
    意味着什么
    < 20 秒
    torch 页读取正常
    问题在 vLLM 的多进程/绑核路径
    20~60 秒
    偏慢但能接受
    随机读性能差,需要换盘或换宿主
    > 60 秒虚拟盘随机读是硬伤vLLM 这条路在这台机器上判死

    在这台机器上实测(此时已装好 VMware Tools):

    图 10:分层计时的两条实测。print('ok') 用了 11.3 秒,import torch 用了 53.1 秒——两个数字的劣化倍数一致,这才是关键。

    # ① print('ok') real    0m11.304s user    0m0.024s sys     0m0.029s  # ② import torch real    0m53.117s user    0m0.022s sys     0m0.027s torch 2.13.0+cpu 

    这两组数字给出的信息比单个数字多得多:

    其一,两条的劣化倍数一致(约 10 倍)。 正常跑一句 print('ok') 是 0.5~1 秒,这里 11.3 秒;正常 import torch 是 3~5 秒,这里 53.1 秒。如果只是 torch 装坏了,实验一应该是 1 秒。 实际是"连启动容器都要 11 秒"——说明慢的不是 torch,而是整个镜像层的读取路径。

    其二,user + sys 加起来只有 0.049 秒。 53 秒里 CPU 几乎没干活,全部时间花在等待上。这是纯 I/O 等待的典型特征,不是算力不足。所以"CPU 太慢所以加载慢"这个直觉是错的。

    其三,一个偶然发现,把"慢"量化得非常直观。 后来下载 0.5B 模型时,下载器打出了实时速率:

    图 11:从 ModelScope 下载 988 MB 权重,速率 5.68 MB/s——这个数字和本地虚拟磁盘读取权重的 6.4 MB/s 几乎相同。

    model.safetensors:  2%|▏  | 22.9M/988M [00:06<02:49, 5.68MB/s] 

    从阿里云下载的速度是 5.68 MB/s,从本地虚拟磁盘读文件的速度是 6.4 MB/s。

    两者几乎一样。换句话说:这台机器的"本地磁盘",已经慢到了广域网的量级。 正常的本地 SSD 顺序读应该是几百 MB/s,比网络下载快两个数量级。

    💡 这个对照比任何形容词都有说服力。

    而且它能反推验证:torch 那一堆 .so 文件合计几百 MB,按 5~6 MB/s 读,正好是 50 秒左右——和实测的 53 秒对得上。

    为什么必须分层?因为之前那个数据太有迷惑性:容器内存从 2.914MiB 爬到 23.76MiB,花了 2 分 42 秒。当时我以为是"权重加载太慢"或"内存不够",实际上它根本没走到加载模型那一步。之前所有关于内存的推理,方向全错了。

    💡 这就是分层计时的价值:它把一个笼统的"慢",变成一个可归因的具体层级。

    而且它顺便否掉了另一个常见猜测:I/O 慢 10 倍 ≠ 磁盘坏了。这台机器的 dmesg 里从头到尾没有一条 sd/ata 的 I/O error,SMART 也正常。慢和坏是两件事。

    隔离实验:空载基线

    平台是否自病,也要单独测:

    d0=$(dmesg | grep -c "soft lockup") for i in $(seq 1 20); do   echo "$(date +%H:%M:%S) load=$(awk '{print $1}' /proc/loadavg) 新增lockup=$(( $(dmesg | grep -c 'soft lockup') - d0 ))"   sleep 30 done 

    10 分钟空载,一条容器都不起。

    • 新增 lockup = 0 → 平台健康,vLLM 是触发器
    • 空载也出现 lockup → 宿主/虚拟化层问题,与 vLLM 无关
    06
    真凶:VMware Tools 缺失的三个连锁后果

    跑一条命令,答案就出来了:

    [root@LinuxPythonWorkshop ~]# systemctl status vmtoolsd --no-pager Unit vmtoolsd.service could not be found. 

    这台虚拟机压根没装 open-vm-tools。

    这不是小事。它缺三个能力,每一个都直接对应一类症状:

    缺失的能力
    导致的症状
    时间同步
    (host→guest)
    宿主暂停/恢复 VM 后 guest 时钟跳变,watchdog 把"22 秒没走时钟中断"报成 soft lockup
    内存气球
    (balloon)
    宿主内存紧张时无法从 guest 回收空闲页,只能把 VM 的物理内存换出到宿主磁盘 → guest 内任意内存访问都可能卡 20 秒
    vmwgfx 协作
    显存管理缺协作,直接对应 flip_done timed out

    第二条尤其关键:宿主侧换出 guest 内存,guest 完全看不见(guest 的 swap used 只有 1.7%,看起来内存很宽裕)。但每一次缺页都要等宿主磁盘,表现就是"随机 20 秒全局停顿、所有设备一起超时、磁盘却没有任何 I/O error"——和日志里五个子系统同时失联的现象完全吻合。

    修复

    dnf install -y open-vm-tools systemctl enable --now vmtoolsd timedatectl set-ntp true 

    装完确认三件事:

    systemctl status vmtoolsd --no-pager | head -6 timedatectl | head -6 lsmod | grep -i vmw_balloon 

    图 12:崩溃时的 BMC 控制台。System load: 46.13、Memory used: 20.7%——内存占用并不高,却把负载推到 46,说明卡住的是等待而不是计算。

    实际输出:

    图 13:open-vm-tools 安装完成,vmtoolsd 处于 active (running),System clock synchronized: yes。这一行是关键——之前 guest 没有时间源,宿主一暂停 VM 时钟就跳变。

    ● vmtoolsd.service - Service for virtual machines hosted on VMware    Loaded: loaded (/usr/lib/systemd/system/vmtoolsd.service; enabled)    Active: active (running) since Thu 2026-10-29 10:19:41 CST; 195ms ago      Main PID: 1600823 (vmtoolsd)  
      Local time: Thu 2026-10-29 10:19:45 CST System clock synchronized: yes            NTP service: active 

    图 14:vmw_balloon 内核模块已加载,并被 vmw_vmci 引用——guest 与 hypervisor 的内存回收通道打通了。

    vmw_balloon          28672  0 vmw_vmci             94208  2 vmw_balloon,vmw_vsock_vmci_transport 

    三个条件全部就位:时间同步生效、气球驱动已加载、guest 与 hypervisor 的回收通道打通。

    📖 初学者讲解:vmw_balloon 是内核模块,vmtoolsd 是用户态代理。两个都要有——只有模块没有代理,宿主依然无法安全地回收 guest 内存。

    验证:同样的命令,同样的时间点,完全不同的结果

    修完不能只看"命令装好了",要看行为是否变了。于是用完全相同的命令再跑一次,只在启动后按同一节奏采样 load 和 D 任务数,并挂了一个自动刹车:load ≥ 20 或 D ≥ 5 就立即 docker kill。

    图 15:修复后的刹车循环。前 7 分钟 load 在 1.20~4.98 之间,D 任务从未超过 1;第 450 秒 load 冲到 53.15,刹车立即生效。

    图 16:Upcasting torch.bfloat16 to torch.float32 正常打印;下一行的 Available RAM: 9.32 GiB 成了后面算内存账的关键输入。

    11:00:16 load=1.20 D=1 第60s 11:01:19 load=4.04 D=0 第120s 11:02:20 load=2.32 D=1 第180s 11:03:21 load=3.99 D=1 第240s 11:04:23 load=4.04 D=0 第300s 11:05:26 load=4.98 D=1 第360s 11:06:32 load=3.19 D=1 第420s ⚠️ 止损:load=53.15 D=6,第 450s 

    和修复前同一时间点的对比:

    启动后
    修复前
    修复后
    第 180 秒
    LOAD=14.78,90 秒后 SSH 断开load 2.32
    ,D=1
    第 420 秒
    整机已不可用
    load 3.19
    ,D=1
    第 450 秒
    —
    load 53.15
    ,触发刹车

    前 7 分钟的曲线上,修复前后是两个世界。 但第 450 秒仍然冲上去了——这说明还有第二个问题。

    决定性的区分:soft lockup 计数

    刹车之后立刻取证:

    图 17:loadavg 16.83 已开始回落,ps | grep vllm 为空,容器 Exited (137),soft lockup 计数为 0。

    [root@LinuxPythonWorkshop ~]# ps -eo pid,stat,comm | grep -i vllm [root@LinuxPythonWorkshop ~]# docker ps -a --filter name=vllm-server --format '{{.Status}}' Exited (137) About a minute ago [root@LinuxPythonWorkshop ~]# dmesg | grep -c "soft lockup" 0 

    三个结果放在一起,把两件事彻底分开了:

    观察项
    修复前崩溃
    这次(修复后)
    soft lockup
    CPU#1 卡 21s、CPU#3 卡 22s、CPU#1 卡 26s
    0
    docker kill
     效果
    假成功
    ,VLLM::Worker 又活了 11 分钟
    真成功
    ,Exited (137) + 进程表清空
    load
     峰值
    74.79,持续不降
    53.15,几十秒内自愈
    恢复方式
    只能强制重启
    容器自杀,系统 2 分钟自动恢复

    💡 docker kill 这次能杀掉,本身就是最硬的证据。

    上次那个 VLLM::Worker 从 16:42 活到 16:53,是因为它卡在不可中断的 D 状态,信号投递不进去。这次它在用户态,信号一投就死。同一个命令,两种结果,说明卡住的位置根本不同。

    于是两个根因可以明确分开了:

    根因
    表现
    状态
    错误一
    VMware Tools 缺失,虚拟化层周期性停顿
    跨子系统超时、D 状态杀不掉、整机假死
    ✅ 已修复
    错误二
    内存峰值不足,页回收死循环
    load
     短暂冲高、容器自杀、系统自愈
    ❌ 仍在

    这是整轮排查最重要的结构。 在修好错误一之前,错误二的表现被错误一掩盖了——你看到一个 74 的 load,根本分不清是"虚拟化层卡死"还是"内存不够"。

    顺便:错误二的账怎么算

    图 18:Available RAM: 9.32 GiB 这行日志是现成的输入——它比"权重多大"更能决定成败。

    Available RAM: 9.32 GiB 这行日志是现成的输入。再算一遍:

    项
    数值
    checkpoint 文件页(bf16 2.88 GiB)
    2.88 GiB
    fp32 张量(1.54B × 4 字节)
    5.75 GiB
    两者同时驻留的瞬时峰值8.63 GiB
    Python + torch 运行时
    ~1.5 GiB
    KV 缓存(KVCACHE_SPACE=1)
    1.0 GiB
    合计~11.1 GiB
    实际可用9.32 GiB
    缺口约 1.8 GiB

    ⚠️ 两张表别直接相减:03 节按整机算(含系统 1.0 + 容器 3.4,不含 checkpoint 文件页),本节按容器内算(含 checkpoint 页 2.88,不含系统与容器)。换算:12.6 − 4.4 + 2.88 ≈ 11.1。

    而崩溃点在第 450 秒——正好是 2.88 GiB ÷ 6.4 MB/s ≈ 7.5 分钟 读完权重的时刻。这不是巧合,是算术:读完的瞬间,文件页还在缓存里,而 fp32 张量开始构建,两份同时驻留,撞上 1.8 GiB 的缺口。

    ⚠️ 修正一个常见说法:网上常说"加载模型需要 2 倍权重的内存"。更准确的说法是 checkpoint 文件大小 + fp32 张量大小。

    因为 checkpoint 本身可能是 bf16(2.88 GiB),而运行时张量是 fp32(5.75 GiB),两者不是同一个数。按"2×权重"算会得到 5.76 GiB,低估了近 3 GiB。

    顺便记一条方法论:修复前后各跑一次 dmesg | grep -c "soft lockup"。dmesg 只保存本次启动的日志,重启后计数清零,所以这个差值就是本次修复的净效果。

    07
    第四次:降到 0.5B,跑通了

           到这里账已经算清了:1.5B 需要 11.1 GiB,可用只有 9.32 GiB,缺口 1.8 GiB。

          补这个缺口有两条路:停掉 Dify 腾出 3.4 GiB,或者换更小的模型。我选了后者,理由很实际——停 Dify 要动生产环境,换模型只要下一个 1 GB 的文件。

          内存预算:先算,再动手

          项
          1.5B
          0.5B
          checkpoint(bf16)
          2.88 GiB
          0.92 GiB
          fp32 张量
          5.75 GiB
          1.96 GiB
          瞬时峰值(两者同时驻留)
          8.63 GiB
          2.88 GiB
          运行时
          ~1.5 GiB
          ~1.5 GiB
          KV 缓存
          1.0 GiB
          ~0.8 GiB
          合计11.1 GiB~5.2 GiB
          实际可用
          9.32 GiB ❌
          7.21 GiB ✅

          0.5B 需要的 5.2 GiB,在 7.21 GiB 可用下余量 2 GiB。 这次不会触发页回收死循环。

          下载:顺手拿到一个关键数字

          docker run --rm -v /root/.vllm/models:/root/.cache/modelscope \   --entrypoint python vllm-openai-cpu:day12 \   -c "from modelscope import snapshot_download; print(snapshot_download('Qwen/Qwen2.5-0.5B-Instruct'))" 

          11 个文件、共 943M,耗时 3 分 06 秒:

          Downloading: 100%|██████████| 11/11 [03:06<00:00, 16.94s/file] /root/.cache/modelscope/models/Qwen--Qwen2.5-0.5B-Instruct/snapshots/master 

          📖 初学者讲解:下载完一定要 ls -lh 确认 model.safetensors 的完整大小。中断的下载最容易在权重文件上留下"看起来存在、实际只有几 MB"的残缺文件,而这种文件会让 vLLM 报出与实际原因毫无关系的错误。

          ls -lh /root/.vllm/models/models/Qwen--Qwen2.5-0.5B-Instruct/snapshots/master/ 
          total 954M -rw-r--r-- 1 root root  659 config.json -rw-r--r-- 1 root root  242 generation_config.json -rw-r--r-- 1 root root 1.6M merges.txt -rw-r--r-- 1 root root 943M model.safetensors      ← 权重 -rw-r--r-- 1 root root 7.2K tokenizer_config.json -rw-r--r-- 1 root root 6.8M tokenizer.json -rw-r--r-- 1 root root 2.7M vocab.json 

          一个小坑:容器名冲突

          docker: Error response from daemon: Conflict. The container name "/vllm-server" is already in use by container "a81adff0dbe66...". 

          上一轮被刹车杀掉的容器虽然 Exited (137),但容器名仍然被占用。Docker 要求名字全局唯一,已停止的容器也占名。

          修正:启动前先删。

          docker rm -f vllm-server 2>/dev/null 

          💡 为什么是 -f 而不是普通的 docker rm:普通 rm 要求容器处于停止状态才肯删;-f 会先发 SIGKILL 再删。这里旧容器已经 Exited,两者都能用,但 -f 更省事。

          前提是旧容器不在 D 状态——在的话 -f 也会卡住,那就得先重启系统。

          启动参数:只改四处

          docker run -d --name vllm-server \   --network lpw-ai -p 8000:8000 \   --ipc=host \   --cpus=4 --memory=9g --memory-swap=9g \   --security-opt seccomp=unconfined --security-opt label=disable \   --cap-add SYS_NICE \   -e VLLM_TARGET_DEVICE=cpu \   -e VLLM_CPU_KVCACHE_SPACE=1 \   -e OMP_NUM_THREADS=4 \   -e PYTHONUNBUFFERED=1 \   -v /root/.vllm/models:/root/.cache/modelscope \   --entrypoint vllm \   vllm-openai-cpu:day12 \   serve /root/.cache/modelscope/models/Qwen--Qwen2.5-0.5B-Instruct/snapshots/master \   --host 0.0.0.0 --port 8000 \   --dtype float32 \   --max-model-len 2048 \   --max-num-seqs 4 \   --enforce-eager 
          #
          项目
          1.5B 那次
          这次
          理由
          1
          模型路径
          Qwen--Qwen2.5-1.5B-InstructQwen--Qwen2.5-0.5B-Instruct
          降级
          2
          --memory12g9g
          峰值只需 5.2 GiB,不必给 12g
          3
          --max-model-len5122048
          内存有余量,恢复实用上下文
          4
          --max-num-seqs14
          同上,验证可用性而非"最小能起"

          后两项放宽是有意的:1.5B 那次把上下文压到 512、并发压到 1,是为了"能在最小配置下起来";这次内存充足,要验证的是"能不能实用"。

          全文最重要的判据转折:load 26.77 但 D=1

          启动后按 15 秒一次采样,得到了这组数据:

          11:24:56  load=1.22   R=—   D=0   code=000 11:25:58  load=1.75   R=—   D=0   code=000 11:27:52  load=15.83  R=—   D=1   code=000 11:29:51  load=16.17  R=—   D=2   code=000 11:30:43  load=26.77  R=15  D=1   code=000 11:31:09  load=20.13  R=8   D=1   code=000 11:31:43  load=24.69  R=5   D=2   code=000 11:32:23  load=25.72  R=10  D=1   code=000 11:33:13  load=25.94  R=13  D=0   code=000 11:33:55  load=27.19  R=5   D=1   code=000 

          如果你只看 load,这十行数据会让人得出"卡死了,赶紧杀"的结论——load 一直在 20~27,远超我为它设的 25 阈值。

          但把 load 拆成 R 和 D 之后,性质完全变了:

          指标
          1.5B 崩盘那次
          0.5B 这次
          load
          53.15
          20~27
          D(不可中断睡眠)6 且持续增长0~2,无增长趋势
          R
          (运行中)
          —
          5~15,波动

          D 只有 0~2,说明没有 I/O 等待堆积。 那 20~27 的 load 全部来自 R 状态的线程——它真的在算,不是在等。

          ⚠️ 这是整篇文章最该记住的一条:

          load 高本身不是故障,D 任务堆积才是。

          load 把 R(运行中)和 D(不可中断睡眠)加在一起统计。前者代表"在干活",后者代表"在等 I/O 卡住"。同一个数字,两种截然相反的含义。

          判断方法只要一行:

          bash echo "R=$(ps -eo stat | awk '$1 ~ /^R/' | wc -l)  D=$(ps -eo stat | awk '$1 ~ /^D/' | wc -l)"

          • R
             多、D ≤ 2 → 正在算,继续等
          • D
             ≥ 5 且递增 → 真的卡住了,该杀

          我的刹车脚本只设了 load ≥ 25 这一条阈值。按这个阈值,11:30:43 的 load=26.77 就会把它杀掉——而那一刻正是它做最后一次 warmup 前向的时刻,差 6 分钟就成功了。

          这一段是"差点误杀"的完整现场。阈值本身没错,错的是用了一个含义模糊的指标做阈值。

          时间账:12 分 40 秒

          阶段
          起止
          耗时
          import torch
           + 初始化
          11:23:56 → 11:25:05
          1 分 09 秒
          权重加载 988 MB
          11:25:46 → 约 11:28:30
          约 2 分 45 秒
          fp32 转换 + KV profiling + warmup
          11:28:30 → 11:36:36
          约 8 分 06 秒
          总计12 分 40 秒

          最后那 8 分钟是 warmup。它比"加载权重"本身还长 3 倍,这是最反直觉的一点:

          无 AVX512-BF16 的 CPU 上,vLLM 的启动耗时不由模型体积决定,而由 warmup 决定。

          warmup 要做一次完整的前向计算,耗时 ≈ 模型参数 × 上下文长度 × 线程数 三者共同决定,而且 float32 比 bf16 慢一个量级。

          一次差点被误判的探测时机

          采样循环跑完 6 次(11:33:55)时全部是 code=000,看起来像"跑了 10 分钟还是失败"。

          但服务实际在 11:36:36 才就绪——比最后一次探测晚了 2 分 41 秒。

          2026-10-29T03:36:35  INFO: Waiting for application startup. 2026-10-29T03:36:36  INFO: Application startup complete. 

          💡 教训:000 只代表"此刻端口还没监听",不代表"永远不会监听"。探测循环必须有足够长的窗口,而且不能因为窗口用完就下结论。

          判断"还在加载"还是"已经失败",靠的是 docker ps 的状态和日志时间戳,不是 curl 的返回码。

          验收

          curl -s -o /dev/null -w "code=%{http_code}\n" --max-time 10 http://127.0.0.1:8000/v1/models code=200 

          图 19:code=200,端口通了。

          curl -s http://127.0.0.1:8000/v1/models | python3 -m json.tool | head -30 

          图 20:/v1/models 返回完整信息。max_model_len: 2048 说明参数正确生效;id 是绝对路径,因为用本地快照启动时模型名就是路径本身。

          {   "object": "list",   "data": [{     "id": "/root/.cache/modelscope/models/Qwen--Qwen2.5-0.5B-Instruct/snapshots/master",     "object": "model",     "owned_by": "vllm",     "max_model_len": 2048,     "permission": [{"allow_sampling": true, "allow_logprobs": true, ...}]   }] } 

          三个字段各自印证一件事:

          字段
          值
          说明
          owned_byvllm
          服务正常注册,不是空壳
          max_model_len2048--max-model-len 2048
           生效
          id
          绝对路径
          本地快照启动时,模型名就是路径

          预算验证:内存峰值落在预期区间

          跑通之后回头看,最关键的不是"通了",而是实际内存峰值是否落在预算内。

          项
          预算
          实际
          Available RAM
          应 ≥ 5.2 GiB
          7.21 GiB
           ✅
          上下文长度
          2048
          2048 ✅
          并发数
          4
          4 ✅

          预算 5.2 GiB、可用 7.21 GiB、余量 2 GiB —— 没有触发页回收死循环。 这反向确认了 1.5B 的失败纯粹是那 1.8 GiB 缺口,而不是别的模糊原因。

          💡 这就是"先算账、再动手"的价值:不是猜"应该够吧",而是算出 5.2 < 7.21 再动手;跑完再核对实际值是否落在区间内。

          对比前面三次失败——每次都是"先试,试了再说",于是每次都要花 10 分钟以上才知道行不行。

          速度实测:首次 262 秒,稳态 4.3 秒

          接口通了之后,最后一步是测真实速度——这一步差点让我得出完全相反的结论。

          第一次请求:

          curl -s http://127.0.0.1:8000/v1/chat/completions \   -H 'Content-Type: application/json' \   -d '{"model":".../Qwen--Qwen2.5-0.5B-Instruct/snapshots/master","messages":[{"role":"user","content":"你好"}],"max_tokens":16}' \   -w "\n耗时=%{time_total}s\n" 

          图 21:choices 正常返回,但末尾那行 耗时=261.912227s 让人心凉——16 个 token 用了 4 分 22 秒。

          按这个数字算:16 ÷ 261.9 = 0.061 token/s。当时我几乎就写下"工程上不可用"了。

          幸好又跑了一次:

          图 22:同一请求第二次 103.625552s,比第一次少 158 秒——差值就是冷启动成本。

          再跑五次,答案就完全不同了:

          图 23:五次连续测试,用 python 直接解析 usage.completion_tokens 算准确速度——不能只看耗时,必须同时知道生成了几个 token。

          第1次 4.27s  tokens=16  finish=stop   速度=3.745 token/s 第2次 4.61s  tokens=16  finish=length 速度=3.473 token/s 第3次 4.86s  tokens=16  finish=length 速度=3.291 token/s 第4次 4.43s  tokens=16  finish=length 速度=3.612 token/s 第5次 3.49s  tokens=13  finish=stop   速度=3.725 token/s 

          稳态稳定在 3.3~3.7 token/s,五次波动不超过 15%。

          请求时序
          耗时
          token/s
          状态
          第 1 次(容器刚起)
          261.912 s
          0.061
          权重页全部未命中
          第 2 次
          103.626 s
          0.154
          部分命中
          第 3~7 次
          4.3~4.9 s3.3~3.7页缓存全命中

          从 0.061 到 3.5,差 57 倍。 同一个模型、同一条命令、同一个 16 token 的输出。

          ⚠️ 这是本文最容易犯的统计错误,也是最值钱的一条:

          测推理速度必须跑多次取稳态,而且必须同时记录 completion_tokens。

          两个陷阱叠在一起: 1. 拿首次请求当速度 → 把冷启动成本摊进每 token,低估 57 倍 2. 只看耗时不看 token 数 → 如果模型只生成 4 个 token 就停了,4 秒的耗时会被误算成 1 token/s

          正确做法(就是上图那段):

          python for i in range(1, 6):     t0 = time.time()     out = subprocess.run(["curl","-s",url,"-H","Content-Type: application/json",                           "-d",json.dumps(payload,ensure_ascii=False)],                          capture_output=True, text=True).stdout     el = time.time() - t0     d = json.loads(out)     n = d["usage"]["completion_tokens"]     print(f"第{i}次 {el:.2f}s tokens={n} 速度={n/el:.3f} token/s")

          慢的到底是什么:不是算力,是 I/O

          这五次数据把前面所有推测都校正过来了:

          阶段
          耗时
          真实瓶颈
          容器启动到 startup complete
          12 分 40 秒
          I/O
          (读 943 MB 权重 + warmup)
          首次推理
          262 秒
          I/O
          (权重页全未进缓存,要现读)
          稳态推理4.3 秒 / 16 token算力(3.5 token/s)

          算力其实是够的。慢的全部是 I/O 冷启动。

          这跟 05 节那个发现完全自洽:import torch 53 秒、本地虚拟盘 6.4 MB/s ≈ 网络下载 5.68 MB/s。943 MB 的 safetensors 按 6 MB/s 读,就是 157 秒——正好对上第一二次请求之间那 158 秒的差值。

          💡 所以 ⑫ 真正的结论是:vLLM CPU 版在这台机器上可用(稳态 3.5 token/s),但首次成本极高(启动 12 分 40 秒 + 首答 262 秒),适合长期常驻,不适合用完就停。

          对比一下两种选型:

          方案
          稳态速度
          输出 16 token
          从零到可用
          vLLM CPU + 0.5B float32
          3.5 token/s
          4.3 秒
          约 13 分钟
          Ollama + Q4 量化(同机参考量级)
          10~20 token/s
          1~2 秒
          数十秒

          稳态差 3~6 倍,冷启动差 20 倍以上。

          所以选择取决于场景:

          • 常驻服务
            (一天用几十次)→ 13 分钟的启动成本可以摊薄,vLLM 能用
          • 随手一问
            (一天用两三次)→ Ollama 明显更合适,vLLM 每次冷启动都亏

          顺带记下版本:system_fingerprint: vllm-0.32.0-2743d51d。不同版本对启动日志和参数语义有差异(老版本用 --model,新版本用 serve <path> 位置参数),排查时先确认版本。

          最后看一眼系统健康度

          跑完这一串之后,R/D 依然是健康的:

          图 24:R=4 D=1。4 个线程在跑、1 个任务在等 I/O,与 OMP_NUM_THREADS=4 一致——围栏与线程限制都正常工作。

          整机没有出现任何 soft lockup,也没有 D 状态堆积。 这次是真的"跑起来了",不是"把系统拖垮了"。

            08
            正确选型:这类机器上该用什么

             跑通了,但结论不是"上就完了"。四条经验,按重要度排。

            结论一:先算账,再动手

            内存必须按这个公式估:

            峰值 = checkpoint 文件大小 + fp32 张量大小 + KV 缓存 + 运行时 

            而不是"权重 × 2",也不是"权重 × 1"。以 1.5B 为例:

            项
            数值
            checkpoint(bf16 文件,会被读进页缓存)
            2.88 GiB
            fp32 张量(1.54B × 4 字节,运行时构建)
            5.75 GiB
            KV 缓存
            1.0 GiB
            运行时
            ~1.5 GiB
            合计约 11.1 GiB

            按"2×权重"算会得到 5.76 GiB——低估了将近 3 GiB,而这 3 GiB 恰好是决定成败的部分。

            结论二:容器围栏能挡 OOM,挡不住虚拟化层

            --cpus=4 --memory=9g --memory-swap=9g 的价值在两次失败里体现得很清楚:

            它做到了
            它做不到
            失败被限制在容器内,不再拖垮整机
            限不了宿主换出 guest 内存
            系统 2 分钟内自动恢复
            限不了虚拟盘 5~6 MB/s 的随机读
            docker kill
             能真正杀掉进程
            限不了 warmup 阶段 8 分钟的算力开销

            围栏不是万能护身符,但它是"失败可恢复"的前提。 没有它,第一次崩溃就要强制重启。

            结论三:这类机器的正解是 Ollama + GGUF 量化

            把实测数字摆在一起,不用形容词:

            方案
            权重体积
            内存峰值
            稳态速度
            从零到可用
            这台机器上的表现
            vLLM CPU + 1.5B float32
            5.75 GiB
            11.1 GiB
            —
            —
            内存差 1.8 GiB,起不来
            vLLM CPU + 1.5B bf16
            2.88 GiB
            ~5.4 GiB
            —
            —
            静默降级,端口永不监听
            vLLM CPU + 0.5B float32
            1.96 GiB
            ~5.2 GiB
            3.5 token/s约 13 分钟
            可用,但冷启动极贵
            Ollama + Q4 量化~1.0 GB~2 GiB10~20 token/s数十秒
            推荐

            五个差别值得说清楚:

            其一,内存差一个数量级。 同一个模型,GGUF Q4 约 1GB,float32 要 5.75GB。

            其二,启动路径短得多。 Ollama 加载 GGUF 是"打开一个文件、顺序读、映射进内存",没有 import torch 这一步。而这台机器上 import torch 要 53 秒,vLLM 的 warmup 还要再花 8 分钟——Ollama 把这两块全省了。

            其三,首次成本差 20 倍。 vLLM 从启动到首答可用要 13 分钟;Ollama 加载约 1 GB 的 GGUF 只需数十秒。对"一天用两三次"的场景,这个差距是决定性的。

            其四,稳态也差 3~6 倍。 3.5 token/s vs 10~20 token/s,长回答的体验差距明显。

            其五,不依赖内核级绑核。 vLLM CPU 后端要用 SYS_NICE 调线程优先级、绑定核、开 --ipc=host。这些在虚拟机里恰恰是最容易被 hypervisor 调度策略打乱的部分。Ollama 用普通线程池,容错性高得多。

            结论四:vLLM 不是"更专业",是"目标场景不同"

            vLLM 的核心优势全都在 GPU 上:

            特性
            在 GPU 上的价值
            在 CPU 上的表现
            PagedAttention
            显存利用率提升数倍
            无显存,收益归零
            连续批处理
            高并发吞吐翻倍
            单流场景无差别
            CUDA Graph
            消除调度开销
            没有 CUDA
            张量并行
            多卡扩展
            CPU 只有绑核

            搬到无 GPU 的虚拟机里,它的优势全部失效,只剩下"启动路径长、依赖多、warmup 慢"的负担。

            一句话

            单机单流推理用 Ollama + GGUF;vLLM 留给有 GPU、要高并发的场景。

            这不是"vLLM 不行",是用错了工具——等于拿 F1 赛车去送外卖:技术上能到,但油钱和保养费高得离谱。

            09
            七个坑
            现象
            根因
            修正
            No CUDA driver
            拉了 CUDA 版镜像
            换 -cpu 镜像(1.76GB vs 8.8GB)
            端口 000,Worker 疯狂烧 CPU,不报错
            CPU 无 avx512_bf16,bf16 静默降级
            --dtype float32
            ,日志须出现 Upcasting ... to torch.float32
            内存装不下,load 冲到 53 后容器自杀
            峰值 = checkpoint 大小 + fp32 张量大小 + KV + 运行时
            按峰值估,别按权重估
            docker kill
             后进程还活着
            D
             状态进程收不到 SIGKILL
            用 ps -eo pid,stat,comm 双向确认
            五个子系统同时超时
            虚拟化层停顿,宿主换出 guest 内存
            装 open-vm-tools + 开 NTP + 查宿主快照
            日志十几分钟不动
            进度条 \r 原地刷新,不产生新行
            看内存增量,不看日志末行时间
            Conflict. The container name is already in use
            已停止的容器仍占名字
            启动前 docker rm -f vllm-server

            补充一:load 高要拆成 R 和 D 看

            这条差点让我把已经跑通的容器杀掉:

            echo "R=$(ps -eo stat | awk '$1 ~ /^R/' | wc -l)  D=$(ps -eo stat | awk '$1 ~ /^D/' | wc -l)" 
            组合
            含义
            动作
            R
             多、D ≤ 2
            正在计算
            继续等
            D
             ≥ 5 且递增
            I/O 卡死
            该杀
            D
             高、R 低、load 低
            有进程挂住
            看日志末行

            load 把 R(运行中)和 D(不可中断睡眠)加在一起算。同一个数字,两种截然相反的含义。

            补充二:日志相关的坑

            docker logs -f vllm-server 2>&1 | grep -E "startup complete" 

            会输出空白——但不是没有日志。grep 的标准输出接到管道(非终端)时会启用块缓冲(默认 4KB),日志行要攒够 4KB 才刷出来。vLLM 一分钟才输出几百字节,所以你永远等不到。

            docker logs -f vllm-server 2>&1 | grep --line-buffered -E "startup complete" 

            补充三:vllm --version 会卡住,改用 pip show

            docker exec vllm-server vllm --version 

            这条命令会挂住几十秒甚至更久,看起来像死机。

            原因是 vllm 这个 CLI 入口在解析参数之前就会 import vllm,连带 import torch——在这台机器上光 import torch 就是 53 秒。再加上 docker exec 非交互执行时 Python 的 stdout 是块缓冲,输出要攒够 4KB 才刷,所以即使算完了也可能不显示。

            改用只读元数据、不加载任何模块的方式,秒出:

            docker exec vllm-server pip show vllm | head -3 
            Name: vllm Version: 0.32.0+cpu Summary: A high-throughput and memory-efficient inference and serving engine for LLMs 

            💡 通用规律:想知道某个 Python 包的版本,用 pip show 而不是 包名 --version。前者读 dist-info 元数据,后者要加载整个包。这条在 I/O 慢的机器上差别是"1 秒"和"1 分钟"。

            补充四:Exited (137) 有三种来源,别一看就以为 OOM

            整个排查过程中 Exited (137) 出现了两次,但原因完全不同。137 = 128 + 9,即被 SIGKILL 终止,而 SIGKILL 可能来自三个地方:

            来源
            OOMKilled
            触发方式
            是否正常
            内存超 cgroup 上限,被内核杀
            true
            容器内 OOM
            真故障
            docker stop
             10 秒超时,被强制杀
            false
            vLLM 优雅退出没跑完
            正常
            docker kill
             主动终止
            false
            手动发 SIGKILL
            正常

            一条命令区分:

            docker inspect vllm-server --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Error={{.State.Error}}' 
            OOMKilled=false ExitCode=137 Error= 

            OOMKilled=false 就说明不是内存问题。

            💡 为什么 docker stop 会让 vLLM 超时:vLLM 收到 SIGTERM 后要销毁 EngineCore 子进程、释放全部 fp32 张量和 KV 缓存(本例约 5 GB)。在 I/O 慢、内存回收慢的机器上,10 秒根本不够,于是被 SIGKILL。这不是 bug,是默认 timeout 太短——想让它优雅退出可以加 -t 30:

            bash docker stop -t 30 vllm-server

            10
            动手练习题

             一、选择题(每题4个选项)

            1. 判定 CPU 版 vLLM 镜像的口诀是?

            A. 仓库名带 latest

            B. 仓库名带 -cpu

            C. 镜像小于 2GB 

            D. 标签带 x86_64

            2. CPU 不支持 bf16 时,vLLM 的实际表现是?

            A. 抛出 Unsupported dtype 立即退出 

            B. 静默降级为软件模拟,不报错、端口永不监听

            C. 自动切换到 fp16 

            D. 拒绝启动容器

            3. 加载 1.5B 模型(bf16 checkpoint + float32 运行时),内存峰值应该如何估算?

            A. 等于权重体积 

            B. 约 1.5 倍权重体积 

            C. checkpoint 文件大小 + fp32 张量大小 + KV + 运行时

            D. 固定 8GB

            4. 进程处于 D 状态时,docker kill 会怎样?

            A. 立即终止 

            B. SIGKILL 投递不进去,kill 是"假成功"

            C. 触发 OOM 

            D. 自动重启容器 【答案提示】见 03 节教训二。

            5. 排查"虚拟机一跑重活就整机假死",最该先确认的是?

            A. 磁盘 SMART 状态 

            B. VMware Tools 是否安装、时间是否同步

            C. 内存条频率 

            D. 网卡驱动版本

            6. load 冲到 27、但 D 任务只有 1 个,说明什么?

            A. 系统即将崩溃 

            B. 正在计算,不是卡死

            C. 磁盘 I/O 阻塞 

            D. 内存溢出

            二、填空题(3 题)

            1. 判断"容器还在正常加载权重"的可靠信号是内存的 ______ 速率,而不是日志末行时间。

            2. docker logs -f | grep 看不到内容时,需要加 ______ 参数解除管道块缓冲。

            3. 判断容器是"在算"还是"在等",应把 load 拆成 __ 和 ____ 两个状态分别计数。

            三、简答题(1 题)

            在 8 核 / 15GiB 的虚拟机上做本地推理,你的选型是什么?请给出两条理由。

               🎁 答对粉丝福利:

                将答案按顺序发至评论区, 全部答对的前50位粉丝,将获得:  

               ✅ 赠送完整的《21天工具链实验手册 》

            【AI工具链】跟着敲一遍⑪:Filter 插件跑通

            【AI工具链】跟着敲一遍⑩:1.5b 退回单次 RAG 跑通

            下篇内容预告;
            1.推理后端的事告一段落,下一篇回到入口:Day 13 边缘 Nginx 加固——80/443 被占怎么查,以及把管理端口藏起来的三条规则。🚀  
            👍 觉得这篇文章有帮助?点赞 + 在看 + 分享,是对我最大的支持!
            💬 有疑问?评论区留言,我会一一回复~

            #vLLM #Ollama #GGUF #CPU推理 #虚拟化 #VMwareTools 

                #故障排查 #AI工具链

             本文由LinuxPythonWorkshop(清风)独家发布,请在获得账号管理员授权后转载。
            策划制作
              策划:清风    丨   监制:明月
              编辑:啸啸    丨   图片来源:稿定设计及网络,侵删

            回首页,搜索你想要的文档

            在首页搜索框输入关键词,快速定位资料

            文库首页搜索指引 立即搜索

            相关学习资料