拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

统一内存跑大模型为何狂写盘?内存换页排查与优化实战

统一内存跑大模型为何狂写盘?内存换页排查与优化实战

先说个让人血压飙升的事:我手头这台 Strix Halo 平台(锐龙 AI Max+ 395,128GB LPDDR5X 统一内存,配的是一块 2TB NVMe)跑了一天 qwen3.8 flash next,第二天起来看统计,磁盘累计写入飙升了 256GiB。256GiB 是什么概念?相当于你睡觉这一晚,系统往硬盘里倒了接近一块 256GB SSD 的总容量。这个量级放在任何机器上都很吓人,尤其是我本来打算把它当长期本地模型服务器用的。

这问题不解决,下一步大概率就是 SSD 先走一步,或者推理时卡到怀疑人生。这篇记录不是劝退帖,也不是炫耀贴,而是一份比较完整的排查日志:为什么一台号称"随便跑大模型"的统一内存机器,反而会疯狂写盘,以及我最后到底怎么把它治住的。无论你用的是 Strix Halo、Mac 这类大统一内存设备,还是普通 PC 上挂了个大模型服务端,只要跑的是千问系这种长上下文模型,这套排查思路基本都能直接抄。

1. 机器的诱惑和一夜之间被写掉的 256GiB

1.1 Strix Halo 看着就是为本地大模型量身定做的

先说机器本身。Strix Halo 这一代最吸引人的点,就是把 CPU、GPU、内存全部塞进同一个物理封装里,内存直接给到 128GB LPDDR5X,带宽跑到 256GB/s 左右,GPU 部分有 40 个 RDNA 3.5 计算单元。对比一下传统独显,哪怕 24GB 显存的卡,想跑 70B 参数的量化模型也只能挤牙膏,而 128GB 统一内存在容量上直接把约束解开了。

所以当我拿到机器,第一件事就是把最新的 qwen3.8 flash next 拉下来跑。这个叫法我不去纠结具体是哪个 repo 的构建版本,本质上就是基于千问 3.8 量级模型的一套本地推理流程。它的吸引力很简单:模型体积不大、指令跟随能力不错、上下文还特别长,正好适配这种大统一内存平台。跑起来的那一刻,你确实会觉得"这才是本地 AI 该有的硬件"。

1.2 但磁盘写入数据不会骗人

问题就出在第二天早上。我看了一眼nvme smart-log,吓一跳——累计写入值往上跳了 256GiB。更准确的说法是,从当天零点到我睡醒这十几个小时里,主机平均每秒往 NVMe 写了差不多 3MB,看着不高对吧?但这是平均,实际观测时 swapd 一开动就是几百 MB/s 的脉冲,而且它不是一次性写入,是断断续续、持续不断地写。

我一开始还怀疑是模型下载或者量化转换产生的临时文件,但查了日志,那些操作早就结束了,写入集中在深夜到凌晨。这说明背后一定有个机制在持续把内存里的东西往磁盘倒腾,而不是正常的一次性写文件。

这里还要补充一个判断:NVMe 的write_totals属性反映的是主机发出来的逻辑写量,256GiB 的涨幅实打实。有些人会说闪存有写放大、有垃圾回收,但那是物理层的事,smart-log 里逻辑写就已经涨了这么多,说明系统是真的发了一大票写请求出来,不是固件在后台自嗨。

2. 统一内存不是免死金牌,换页机制才是写盘真凶

2.1 "显存=内存"的另一面是内存操作系统说了算

Strix Halo 的宣传话术是"显存即内存,内存即显存",听起来很完美,但实际操作上没那么浪漫。GPU 会通过 BIOS 或者驱动在物理内存里划走一块固定区域当 VRAM,剩下的才是系统可用内存。你如果 BIOS 里把 UMA 分配设成 96GB,那 128GB 机器开机后系统里看到的可用内存可能只有 28GB 左右,剩下的都在 GPU 手里。

模型推理时,权重、KV cache、激活值这些大头,一部分住在设备内存里,一部分住在主机系统内存里。操作系统不关心你跑的是什么 AI 模型,它关心的是页够不够用。一旦系统内存吃紧,内核就会把长期不访问的匿名页从内存里扔到交换空间,也就是 Linux 的 swap 或者 Windows 的 pagefile。这一扔,就变成了实实在在的磁盘写入。

2.2 KV Cache 和上下文窗口才是真正的内存大户

很多人有个错觉:8B 模型,Q4_K_M 量化之后也就 5.2GB,128GB 内存怎么可能不够?问题不在权重,在 KV Cache 和上下文。

千问系列现在的 token plan 动辄号称几十万 token 上下文,听着很爽,但 KV Cache 和上下文长度是严格成正比的。我按 Qwen3-8B 这个量级的典型结构粗略算过一笔账:36 层 Transformer、8 个 KV Head、head_dim 128,半精度下每个 token 的 KV 大约是 72KB。那么 128K 上下文就是 9.4GB,512K 上下文直接飙到 37.6GB。如果 KV 用 FP32 保存,还得再翻一倍。

再加上模型权重 5-8GB、推理时的激活值和计算图 1-2GB、系统后台服务加浏览器 10GB 起步,你算算,128GB 里扣掉 UMA 预留,剩下的系统内存还能撑多久?更别提有人会同时开几个会话,或者同一个模型跑两个实例,上下文一叠加,内存直接击穿。

我把这几项放在一起做了个估算表,供参考:

内存去向典型占用
模型权重(8B Q4_K_M)约 5.2GB
模型权重(8B BF16)约 16GB
KV Cache(128K 上下文,FP16)约 9.4GB
KV Cache(512K 上下文,FP16)约 37.6GB
推理激活值/计算图约 1-2GB
系统桌面环境+浏览器约 8-15GB

2.3 内存看着大,不代表可以随便透支

关键点在于:操作系统允许应用超额申请内存。Llama 系引擎在启动时会根据你设置的上下文大小一次性申请一大块 KV Cache,申请归申请,真正物理分配要到实际访问页面才发生。于是白天你看着内存还有富余,晚上浏览器开了几十个标签,模型又把整段上下文填满,系统内存水位一上去,内核就开始默默地把最冷的内存页换到 swap 里。

Windows 上对应的是 pagefile,Linux 上是 swapfile 或 swap 分区。交换空间只要存在,内核就会用。哪怕你没有主动设置太高,系统还是会根据压力自动调整。我这次遇到的情况,八成就是上下文拉满 + 页面文件在 NVMe 上,导致一整晚都在做这种"内存不够、磁盘来凑"的搬运工作。这也解释了一个反直觉的现象:内存越大的机器,用户越敢开大上下文,反而越容易踩进换页坑。

3. 完整排查链路:我到底是怎么锁定它的

3.1 第一步,先确认写入确实落在系统盘上

排查这种问题最忌讳拍脑袋。我先把"谁在写、写在哪、什么时候写"全部用数据拉出来。

先看磁盘级统计:

iostat -dx 1

w_await和wkB/s能告诉我写请求来自哪条块设备路径,但还不能看到进程。接着用iotop -ao按累计写入排序,注意-a参数必须加,否则你看到的永远是瞬时速率,很容易漏掉半夜的脉冲。

sudo iotop -ao

结果很明确:排在写入榜前列的不是模型下载程序,也不是日志服务,而是内核的kswapd0和几个跟页面回收相关的内核线程。到这里,方向基本清楚了,十有八九是内存换页。

3.2 第二步,把内存压力和 swap 使用量抓出来

然后我盯了一轮内存和 swap 的实时数据:

free -h vmstat 1 10 cat /proc/pressure/memory

vmstat输出里的si和so两个字段是关键,so表示从内存换出到磁盘的页面量。在写入高峰段,so数值飙到每秒几万 KB,和磁盘写速率完全对上。再配合/proc/pressure/memory,能看到内存压力已经频繁亮红灯。

接着找具体是哪个进程占着 swap:

grep -E "VmSwap|Name" /proc/[0-9]*/status 2>/dev/null | awk '/Name:/{name=$2} /VmSwap:/{if ($2 > 0) print name, $2}' | sort -k2 -n

结果毫无悬念,排在前面的是推理引擎进程,VmSwap 已经到了几十 GB 的量级,后面跟着的是浏览器和一堆桌面服务。至此,"谁干的"已经基本坐实。

3.3 第三步,反向验证一下,别让数据骗了你

为了确认不是日志轮转、病毒扫描这类偶发因素,我做了个控制变量实验:

把上下文窗口从原来的 512K 调回 32K,加上内存锁定,重启推理引擎跑了一夜,第二天看nvme smart-log,写入增量还不到 2GiB,基本全是系统正常后台写入。然后再把上下文调回 512K、去掉锁定,同样的负载跑几个小时,写入曲线肉眼可见地陡增。

这个实验做下来,两个结论跑不掉:

  • 写入来自内存换页机制,不是应用主动写文件。
  • 触发换页的直接原因就是过大的 KV Cache 抢占系统内存,导致内核持续把冷数据倒腾到 swap。

3.4 顺便排除几个容易误伤的对象

排查时我还顺手排除了几个常见的干扰项:

  • journald 日志量大不大?抽查了/var/log大小和增长,正常水平。
  • 是不是模型文件反复重新下载?看了家目录缓存,没有增量下载的痕迹。
  • 是不是杀毒/索引服务在扫盘?这类服务的写模式一般是均匀小 IO,不至于一个晚上睡出 256GiB,而且线程对不上。

内存换页是唯一同时满足"写入量大、持续、发生在低负载时段、由内核线程驱动"的解释。

4. 止血实测:三档优化我挨个做了

4.1 第一档:改引擎参数,立刻止损

既然根子是上下文太大导致内存不够,最直接的做法就是把上下文压下来,同时让引擎别把内存页交出去。

我用的启动命令大致长这样:

llama-server \ -m /models/qwen3.8-flash-next-q4_k_m.gguf \ -c 32768 \ -b 512 \ --mlock \ --no-mmap \ --swap 0 \ -ngl 999

几个参数的作用说清楚:

  • -c 32768:上下文砍到 32K。我日常对话根本用不了 512K,硬撑那个数字就是给 swap 送业绩。
  • --mlock:把模型和相关内存页钉在物理内存里,不让内核置换出去。这是最直接的反换页手段。
  • --no-mmap:避免模型文件以 mmap 方式映射到地址空间,减少页缓存被回收导致的额外 IO。
  • --swap 0:显式告诉引擎不要在磁盘上做自己的 swap 空间。
  • -ngl 999:尽可能把所有层都放在 GPU 侧执行,减少主机内存里的搬运。

注意一个搭配坑:--mlock开了以后,如果系统内存真的不够,内核可能会走向 OOM Killer,可能直接把推理引擎干掉。所以这套配置必须和合理的上下文窗口、合理的量化精度配合,而不是无脑锁内存。

4.2 第二档:系统层面加 zram,给换页一个缓冲垫

有些场景确实需要长上下文,比如离线总结一个几百页的文档。这时候光靠压上下文不现实,于是我在系统层面加了 zram。

zram 就是在内存里划一块区域,压缩后当作 swap 用。内核要换页时,先进 zram,而不是直接写 NVMe。压缩后的匿名页可能只剩原来的三分之一到四分之一,磁盘写入几乎归零,代价是 CPU 要多干一点压缩活。Strix Halo 上开 zstd,开销可以忽略不计。

Linux 下用 systemd 的 zram-generator 配置最简单:

# /etc/systemd/zram-generator.conf [zram0] zram-size = ram / 4 compression-algorithm = zstd

然后启用:

systemctl daemon-reload systemctl start systemd-zram-setup@zram0.service

启用之后,把内核的 swappiness 调高一些,让匿名页更愿意进 zram 而不是磁盘:

sysctl -w vm.swappiness=100

有人会问,为什么不干脆把 swap 禁用?我也试过。禁用 swap 的后果是内存一旦压力上来直接 OOM,模型进程被杀,服务不可用。我的结论是:完全禁用不如保留一个小容量的 zram 当保险,既能吸收突发压力,又不会落到 NVMe 上。

4.3 第三档:检查 BIOS 里的 UMA 分配,给系统内存让路

参数优化完,还剩一个容易被忽略的层面:BIOS 里给 GPU 预留的内存大小。

Strix Halo 的 BIOS 里一般有 UMA 或类似命名的选项,可以指定给显卡的专用内存。我一开始是 Auto,系统实际可用内存比 128GB 小不少。后来发现推理引擎主要在 GPU 侧跑,我给显卡预留 48GB 足够装下 8B 模型的权重加一大段 KV Cache,剩下的统统还给系统。

检查当前分配可以直接看内核日志:

dmesg | grep -i amdgpu | grep -i "VRAM"

或者在 ROCm 环境下看内存池信息:

rocminfo

把 UMA 从 96GB 调到 48GB 之后,系统可用内存多了 40 多 GB,swap 压力肉眼可见地下降。如果你的目标是跑 70B 以上模型,再考虑把 UMA 调回去,但 8B 量级真用不了那么多显存预留。

4.4 验证结果:写入量从 256GiB 降到 2GiB

三档都做完,我重新跑满 48 小时做了验证。结果如下:

配置阶段24 小时磁盘写入增量
初始配置(512K 上下文,无锁定,swap 指向 NVMe)约 256GiB
压上下文 + mlock 锁定约 3GiB
压上下文 + mlock + zram 缓冲约 2GiB
压上下文 + mlock + zram + UMA 调优约 1.5GiB

最后的写入量,绝大部分还是系统日志、容器 overlay 这类正常开销,已经不具备威胁。推理时延也比优化前稳定很多,之前那种"跑着跑着突然卡一下"的毛刺基本消失。

5. 最终配置和该养成的习惯

5.1 我现在日常跑的稳定配置

经过这一轮折腾,最终定下来的一套配置就三句话:

  • 模型用 Q4_K_M 或者更低的 IQ2_M 量化档,8B 权重控制在 5-8GB,别用 BF16 去和内存过不去。
  • 上下文按需开,日常 32K,偶尔处理长文档才临时开到 128K 以上,用完立刻收回来。
  • 系统层面保留一个小容量 zram swap,把 UMA 预留调到实际够用的程度,剩下的内存尽量归还给系统。

进程层面的启动参数就是我前面贴的那组命令,没什么花哨的东西,全部都是围绕"别让内核产生磁盘换页"这一条主线。

5.2 SSD 寿命和写入监控,养成本能反应

这次经历之后,我给自己定了两个监控习惯。第一个是每周看一次 SSD 累计写入:

nvme smart-log /dev/nvme0 | grep -E "data_units_written|percentage_used"

第二个是每次启动完推理引擎,先跑一遍内存压力检查:

vmstat 1 5

重点是看so这一列,只要出现持续的换出,就说明配置有问题,趁早停掉排查,别等一个晚上再来看 256GiB 的账单。SSD 寿命这种事,单看 256GiB 其实不会立刻写死一块盘,真正伤的是长期这么干,一年下来一百多 TB 写入,普通 TLC 盘两三年就走完一大半寿命,而且换页抖动带来的延迟毛刺比寿命问题更影响日常体验。

5.3 一点复盘心得

Strix Halo 那 128GB 统一内存,本质上是把你从"显存不够"的焦虑里解放出来,但这并不是一张可以无限透支的信用卡。内存池再大,操作系统这张"账本"依然按物理页记账,上下文开多大,KV Cache 就占多大,系统内存被抢光之后,swap 照样会把你的 SSD 当救生圈。

如果你也想在 Strix Halo 或者类似统一内存平台上长期跑千问系模型,我的建议就一条:不要和上下文窗口较劲。千问的长上下文能力是给你偶尔处理长文本用的,不是让你 24 小时开着的默认选项。把上下文压到够用范围,再辅以一个 zram 缓冲,这台机器才能真正"跑着不吃力,睡一觉也不心慌"。

返回列表