事情发生在一个再普通不过的周六晚上。我在自己的 Strix Halo 主机上折腾一个社区打包的 qwen3.8 flash next 模型,本意是测试这颗 SoC 跑本地推理的实际体验。结果第二天早上习惯性看了一眼硬盘的 SMART 信息,整个人直接清醒了:过去 24 小时,这块 256GiB 的固态硬盘被写入了整整 256GiB。
什么意思?相当于这块盘被从头到尾翻着面完整写了一遍。单看数据量,256GiB 在 SSD 的 TBW 寿命面前不算吓人,但关键在于“一天干完的”——如果你机械式地让这个状态持续一周,就是 1.7TiB 的写入,持续一个月就是 7TiB 上下。对一块健康盘来说这还不到报废线,但对一个跑本地模型的日常环境来说,这种写入速度已经属于异常事故级别了。
这篇博文就把整个事件完整复盘一遍:为什么一个大模型推理服务会让硬盘遭这么重的罪、我用了哪些命令把写入源一个个揪出来、最后又是怎么把每天几百 GiB 的写入降到可以忽略的水平。如果你也在用带大核显的高带宽内存平台跑本地模型,这篇文章应该能帮你省下一块硬盘的寿命。
1. 事件复盘:模型还在跑,硬盘先“折寿”了
1.1 一台本地 AI 主机是怎么在一天内写掉 256GiB 的
先说下这台机器的配置背景。Strix Halo 是 AMD 面向轻薄本和迷你主机推出的整合平台,最大的特点是把高性能核显和内存控制器做进同一颗 SoC,同时支持把系统内存直接当显存用。我手上这台机器配的是 64GiB 的 LPDDR5X 内存,没有独立显卡,理论上跑 7B 到 8B 参数的量化模型非常合适。qwen3.8 flash next 这名字一看就知道是社区重打包的 Qwen3-8B 量化变体,主打 Flash 注意力加速和“下一代”蒸馏技巧,实际用起来对话流畅度确实可以。
问题出在我不该让它带着默认配置裸跑。一般本地模型推理分两个阶段:预填充阶段处理完整用户输入,解码阶段逐 token 生成回答。很多人以为这两个阶段纯粹是 CPU/GPU 计算,跟硬盘没关系,但事实是,现代 Linux 环境下模型加载、显存共享、页面缓存回收、日志记录全都可能转化为磁盘 I/O。尤其是当内存被核显共享机制吃走一大块之后,系统的 swap 逻辑就会非常活跃。
我这次的情景大概是这样的:早上启动模型服务,加载 8B 模型权重加 KV cache,内存占用直接顶满。核显为了保持图形输出又预留了 8GiB 左右的共享显存,系统内存进一步吃紧。一旦物理内存不够,Linux 内核开始把部分匿名页和文件缓存写回磁盘,qwen3.8 flash next 推理过程中的临时变量、中间缓存层也频繁触发换页。等到晚上我再看日志,整个系统在 24 小时内发生了无数次内存压力回收。每次回收都意味着磁盘写入,最终累计出 256GiB 这个“天文数字”。
这里还要强调一个容易混淆的点:SSD 写入量翻倍不只是你显式写了那么多数据。闪存介质本身有写放大效应,尤其是文件系统日志、元数据更新、小尺寸随机写入这些场景。如果你看到 SMART 里 Host_Writes 是 256GiB,实际闪存磨损可能是 300-400GiB。所以一天 256GiB 的 Host_Writes 背后,真正的 NAND 磨损更大。
1.2 先分清“写入”和“读取”:别把账算在模型头上
排查之前先做一个关键区分:模型推理本身到底会不会大量写盘?答案是,正常加载权重文件后,推理计算主要是从内存或显存里读权重,这个过程本身写入很少。真正造成持续大量写入的往往是周边机制:
- 系统层面:swap 换页、page cache 回收后的脏页写回。
- 应用层面:模型服务的日志系统、量化中间文件、缓存目录、core dump。
- 容器层面:如果你用 Docker 跑模型,OverlayFS 的数据复制会带来额外写入。
- 工具层面:一些嵌入向量、会话历史保存类工具,会在每次交互时把数据落盘。
这也是为什么很多人遇到类似问题后第一反应是怀疑模型代码有 Bug,但查来查去发现进程本身 CPU 占用并不高,反而是 kswapd0、jbd2 这类内核线程在疯狂写盘。我遇到的正是这种情况,系统负载不高,但 SSD 的写入量报表非常恐怖。
搞清楚这一点,排查的顺序就清楚了:先看系统级写放大来源,再看应用级日志和缓存,最后才是模型本身的行为。下面我按这个顺序展开,每一步都会给出可复制的命令和判断逻辑。
2. 原理深挖:大模型推理为什么会让硬盘遭罪
2.1 权重加载、页面缓存与 mmap 的三方博弈
先讲一个很多教程不会提的知识点:Linux 加载大文件不一定一次性把全量数据读进内存,而是通过 mmap 把文件映射到进程地址空间,等真正访问到某个页时才触发缺页中断从磁盘读取。这个机制对模型推理来说非常友好,因为模型文件动辄十几个 GiB,如果一次性全部载入,启动就得等半天;用 mmap 后可以按需读页。
但问题也出在这里。文件映射的页面属于 page cache,内核在内存压力下会把不活跃的缓存页淘汰掉。当系统内存被大模型吃满,那些已经映射过的权重缓存页会被内核回收,等你下一次推理需要用到这些权重时,又得从磁盘重新读。读本身不增加 Host_Writes,但会产生大量磁盘读 I/O。如果此时系统的脏页写回机制同时被触发,就会把一些原本属于缓存且被修改过的页写回磁盘,而这些修改往往和文件系统元数据更新交织在一起。
qwen3.8 flash next 这种模型推理时,权重文件是只读使用的,正常情况下 mmap 页面不会产生脏页写回。但如果你把模型放在一个可写挂载分区里,又叠加了文件系统快照、备份同步、杀毒扫描或者容器层复制,情况就完全不同了。
我一个朋友遇到过一个很典型的问题:把模型目录放在一个启用了 atime 更新的分区上,每次读取文件都会触发元数据写入。看似微小的 atime 更新,在几万次缺页读取、几十万次目录访问的累积下,一天多写几十 GiB 毫不夸张。这提醒了第一个排查方向:检查 mount 参数,确认是否挂了 noatime。
2.2 KV Cache 的反复计算和缓存写回
大模型推理有一个特有的内存大头:KV Cache。所谓 KV Cache,就是把已经算过的历史 token 的 Key 和 Value 向量缓存下来,避免每生成一个新 token 就重新算一遍全部历史。对 8B 级别模型来说,长上下文对话下 KV Cache 会达到数个 GiB 甚至十几 GiB。
Strix Halo 的核显共享内存设计在这种情况下是双刃剑。好处是 GPU 可以直接访问系统内存,不需要显存和内存之间来回拷贝;坏处是如果核显已占用了大量共享内存,系统可用内存就会快速减少。一旦内存不足,KV Cache 中不活跃的部分会被换出到 swap 分区。等下一个请求进来,这些 KV Cache 又可能被重新换入,中间伴随大量换页写入。
更隐蔽的是,有些量化框架在长上下文场景下会把一部分历史状态的中间结果“spill”到磁盘,尤其是你打开了一些自动释放历史缓存的选项。这属于应用层的二次写放大。在我的案例里,qwen3.8 flash next 的启动脚本里带了一个 cache 目录,用于缓存部分预计算中间状态。这个目录位于正常的磁盘分区上,每次推理都会往里写文件。
换句话说,KV Cache 的反复计算不一定全在内存里完成,磁盘可能成为它的“垃圾中转站”。想要根治,就得从内存分配策略入手,具体方案会在第 4 节给出。
2.3 内存被核显共享吃掉之后,Swap 成了写放大加速器
这是整件事的罪魁祸首。Strix Halo 的核显最少会预留一部分系统内存作为显示帧缓冲,我机器上那部分大约是 8GiB。再加上桌面环境、浏览器、开发工具,可用内存从 64GiB 降到 40GiB 以下。而一个 8B 量化模型跑起来,KV Cache 加激活值轻松占掉 20-30GiB,内存压力直接拉满。
Linux 内核在这种情况下会启用回收机制,优先回收匿名页还是文件页,取决于 vm.swappiness 参数。很多发行版默认 swappiness 是 60,意味着系统更倾向于把匿名页换到磁盘,而不是回收文件缓存。这个策略对普通桌面用户是合理的,因为文件缓存被回收后下次读取可能要重新读盘;但 swap 换页的写入却是实打实的磁盘写入。
我在那天的配置里 swappiness 就是 60。模型推理过程中,每次长对话产生大量内存页,内核就开始把老页面换出到 swap。推理结束后,新请求进来又需要这些页面,内核再次换入。一天 256GiB 的写入里,至少 60% 是 swap 换页制造的。讽刺的是,我明明有 64GiB 内存,却因为页面管理策略不当物理内存没被用满、磁盘却成了替罪羊。
处理方式其实不复杂:大内存机器上把 swappiness 降到 1-10 之间,让内核优先回收文件页而不是换出匿名页;如果内存真的不够,直接用 zram 或 zswap 把换页压缩后放进内存,而不是直接写 SSD。这些细节我会在第 4 节具体展开。
2.4 隐藏的写入大户:日志、容器层与中间文件
除了系统层面,应用层面也藏着不少写入大户。先说日志。qwen3.8 flash next 这类模型通常通过 Gradio 或自定义 API 提供服务,如果启动时用 nohup python app.py > logs/run.log 这种方式记录输出,每次推理的日志都会追加写入文件。默认 Python print 是行缓冲,日志量不大的时候还好;但如果开启了调试级别日志,一天积累几个 GiB 轻轻松松。
Docker 场景更麻烦。如果你把模型服务容器化,默认的 json-file 日志驱动会把所有输出写到宿主机的 /var/lib/docker/containers 目录,这个目录没有日志轮转时,能够无限膨胀。更隐蔽的是 OverlayFS 的 “copy-up” 机制:容器内如果修改了镜像里的文件,会先把整个文件复制到容器层,再在容器层上修改。对模型这种大文件场景,一次误写就相当于整文件复制写入。
我还在 /var/lib/systemd/coredump 里发现了一个十几 GiB 的 core dump 文件。哪个进程崩溃了?就是模型服务里某个子进程在压力测试时段崩过一次。systemd 默认把 core dump 完整写入磁盘,这个动作一次就是十几 GiB。如果当天崩溃或重启了多次,光是 core dump 就能贡献几十 GiB 的写入。
所以排查时不要只盯着模型主进程,要把整个系统的 IO 行为都看一遍。接下来我整理了一套可以直接照做的定位流程。
3. 实操定位:用一组监控命令把元凶揪出来
3.1 先看总量:smartctl 和 iostat 一分钟定位
排查第一步,确认写入总量不是幻觉。NVMe 固态硬盘的 SMART 信息里有一个关键指标 Host_Writes,统计主机实际发起的写命令数据量。Linux 下用 smartctl 看:
smartctl -a /dev/nvme0n1 | grep -E "Data Units Written|Host_Writes|Percentage Used"注意不同固件字段名不同。如果是 Intel 或其他老盘,可能显示为 Data Units Written,单位是 1000 个 512 字节扇区。比如我看到的值如果换算后是 256GiB,说明确实是盘级真实写入。
拿到总量后,再看实时写入速度。iostat 是 sysstat 包自带的工具,能按设备给出每秒读写字节数:
iostat -dx 1 5重点关注每个设备的 wkB/s 列。如果你看到某个设备持续每秒几十 MB 的写入,那就说明系统层面确实有进程在狂写。用这种方式先确认写入是持续性的还是间歇性的,持续性更可能是 swap 或缓存回收,间歇性则更像日志、轮转或后台任务。
这里分享一个经验:观察至少 5 分钟,不要只看几秒钟。因为 swap 换页通常是波动的,模型推理时的请求密集期和空闲期差异很大,5 分钟以上的采样才能看出平均写入速率。我当时用 iostat 采了 10 分钟,得到的平均写入速率大约是 6MB/s。这个值单看并不算高,但一天的持续运行乘以 86400 秒就是 518GB 左右的写入量。所以不要觉得“每秒几 MB 不算什么”,24 小时累计下来非常惊人。
3.2 再看进程:iotop、pidstat 与 lsof 的组合拳
总量确认后,下一步定位是哪个进程在写。这里最直接的工具是 iotop,它可以按实时 I/O 排序显示各进程的磁盘读写速率:
iotop -o -P-o 表示只显示有 I/O 行为的进程,-P 表示显示进程而不是线程。我那次运行 iotop 后立刻看到几个嫌疑对象:python3 模型服务进程、systemd-journald 日志进程、还有 kswapd0 内核线程。
这里要注意一个坑:iotop 默认不带 sudo 看不了进程名,只会看到 PID,所以记得用 sudo 运行。另外内核线程 kswapd0 本身不归某个用户进程管,很多新手看到它就懵了。kswapd0 活跃说明内核在回收内存页,它的 I/O 行为本质上是被其他进程的内存压力诱导出来的,因此你没法通过杀掉 kswapd0 来解决问题,得减少内存压力本身。
如果想看每个进程累积的读写量,可以用 pidstat:
pidstat -d 1 10它会显示每个进程每秒的读写 KB 数。结合 lsof 看某个进程打开了哪些文件:
lsof -p <PID> | grep -E "deleted|log|core|swap"这里有一个非常实用的技巧:lsof 输出里出现 “deleted” 标记的文件,意味着文件已被删除但仍有进程持有句柄。这种场景下日志进程会继续往“幽灵文件”里写入,既看不到文件,又持续消耗磁盘空间和写入量。我当时没有第一时间发现这个问题,是因为 /var/log 下根本没有对应文件,但写入量却始终不减。
3.3 看具体文件:fatrace 审计写入路径
如果进程级别还不够细,就看文件级别。fatrace 是一个基于 fanotify 的工具,能实时跟踪哪些进程访问了哪些文件:
sudo fatrace -t -m /home -m /var -m /tmp-t 选项会给每条记录加上时间戳,-m 可以限定监控的挂载点。跑一段时间后,你会看到每一行都类似:
python3(24831): W /home/user/.cache/qwen/flash_next/tokenizer_cache.bin这种输出能直接告诉你应用层在写哪个文件。我那次监控了半小时,就发现两个高频写入路径:一个模型服务自带的 cache 目录,另一个是 /var/log/messages 下的系统消息日志。这两个路径的写入频率都很高,半小时各自写了几百 MB。
fatrace 的缺点是需要 root 权限,而且在高 IO 负载下自身会产生额外开销,所以建议只定位用,不要长时间持续跑。定位到具体文件后,后续优化就有的放矢了。
3.4 时间线复盘:用 dstat 和 journalctl 还原案发过程
有时写入不是平稳发生的,而是集中在某个时段。为了还原一天的写入曲线,可以用 dstat 来记录带时间戳的磁盘吞吐数据:
dstat -d -D nvme0n1 -t 60 > disk-timeline.txt这个命令会每 60 秒记录一次磁盘读写速率。第二天再来看这个文件,就能画出 24 小时写入曲线,找到写入高峰对应的时间段,再结合 journalctl 看该时段系统发生了什么:
journalctl --since "2025-01-11 14:00" --until "2025-01-11 15:00" -p warning我复盘时发现,写入高峰集中在模型持续对话期间,凌晨空闲时段几乎没有写入。这说明问题主要和推理交互强相关,排除了夜间备份、杀毒扫描等因素。有了这个结论,就能把优化重点放在模型服务运行时的内存和缓存策略上。
4. 让模型继续跑、让硬盘歇口气:五条实用优化
4.1 给 swap 立规矩:限制回收和换页数据落盘
既然 swap 是大头,第一条优化就是调整系统的内存回收策略。最简单有效的是把 swappiness 调低,让内核优先保留匿名页,而不是动不动就换到磁盘:
sudo sysctl vm.swappiness=10想永久生效就写到 /etc/sysctl.d/99-swap.conf。这里提醒一下:swappiness 从 60 调到 10 后,系统会更多回收文件缓存而不是匿名内存页,对本地模型推理来说体验会更稳。代价是某些文件重复读取时可能重新走磁盘,但考虑到大模型权重文件主要是 mmap 读取并且有局部性,整体收益远大于损失。
如果你的内存还是吃紧,建议启用 zram 而不是直接用磁盘 swap。zram 会先在内存里压缩再换出,SSD 写入量大幅下降。Strix Halo 这种内存带宽充足的平台,zram 的性能影响很小。启用方式很简单:
sudo modprobe zram echo 16G > /sys/block/zram0/disksize mkswap /dev/zram0 swapon /dev/zram0我在机器上直接关了磁盘 swap,只保留 zram。效果立竿见影,之后 SMART 里 Host_Writes 每小时的增量从几百 MiB 降到了几十 MiB。
另外,检查一下是否真的需要 swap 分区。64GiB 内存的机器如果只跑一个 8B 模型,理论上完全可以不 swap,直接 swapoff -a 一了百了。我最后选择保留了一点 zram,只为防止极端情况下内存直接 OOM。
4.2 模型加载与缓存策略:能锁内存就锁内存
模型服务自身也有两个可以优化的点。第一,有些推理框架支持把权重加载进内存后锁定,防止页面被回收。以 llama.cpp 系和部分 vLLM 系框架为例,可以通过环境变量或启动参数开启内存锁定(mlock)。开启后权重页不会被换出,也就不会反复从磁盘重读。
实操上,如果是 qwen3.8 flash next 的 Python 启动脚本,通常会调用底层 C 库或者 ONNX Runtime 来载入模型。你可以先在进程启动前用资源限制大力出奇迹:
ulimit -l unlimited python3 run_model.py这个命令允许进程锁定无限量内存。很多教程忽略了 ulimit -l 这一步,导致程序明明支持 mlock 却报“无法锁定内存”。我踩过的坑是:在 systemd 服务里跑模型,service 文件里忘了加 LimitMEMLOCK=infinity,模型服务一直用普通页面换入换出,性能差且写盘。改成:
[Service] LimitMEMLOCK=infinity之后写盘明显下降。
第二,缓存目录路径尽量指向 tmpfs。很多框架会往缓存目录写中间计算结果、tokenizer 缓存、预计算状态等。如果机器内存有富余,直接把缓存目录挂到 tmpfs:
mkdir -p /dev/shm/qwen-cache mount -t tmpfs -o size=8g tmpfs /dev/shm/qwen-cache export QWEN_CACHE_DIR=/dev/shm/qwen-cachetmpfs 的所有写入都发生在内存里,断电即失,恰好适合那些可再生的缓存数据。这个改动让我每天又减少了几十 GiB 的写入。
4.3 日志与临时文件:能进内存的绝不落盘
日志问题虽然看起来不起眼,但积少成多。首先是 systemd journal,默认会把日志写到 /var/log/journal,而且持久化存储。如果你不需要日志跨重启保留,可以改成 volatile 模式:
sudo mkdir -p /var/log/journal sudo systemctl restart systemd-journald注意确认 SystemMaxUse 设了一个合理上限。我建议在 /etc/systemd/journald.conf 里设置:
SystemMaxUse=500M SystemMaxFileSize=50M RuntimeMaxUse=200M这样即便日志写入频繁,也会被限制在几百 MB 内并自动轮转。
其次是应用自身的日志。如果你是手动启动的模型服务,最好用 nohup 时把输出重定向到 /dev/null,或者使用 logrotate 按大小轮转。我这里给出一个 logrotate 配置示例:
/home/user/qwen/logs/*.log { daily rotate 7 maxsize 100M compress delaycompress copytruncate }重点是 copytruncate 参数:它先复制日志再清空原文件,这样程序持有文件句柄也不会出问题,可以避免日志轮转时服务崩溃。
还有一类隐藏写入是 core dump。Linux 的 systemd-coredump 默认把崩溃进程的完整内存镜像写入磁盘。大模型进程如果崩溃,一次就是十几个 GiB。建议限制 core dump 文件的体积,或者干脆关闭:
echo "kernel.core_pattern=|/bin/false" > /etc/sysctl.d/99-coredump.conf sysctl -p /etc/sysctl.d/99-coredump.conf关掉 core dump 的代价是崩溃现场信息丢失,但本地模型环境更看重硬盘寿命,我认为值得。如果确实要留,用 ulimit -c 0 限制单进程。
4.4 Docker/容器部署:别忘了 OverlayFS 的“复制放大”
如果你是在 Docker 里跑模型,还需要单独处理几层问题。首先,Docker 的 json-file 日志驱动默认无上限,容器内应用疯狂打印日志时,宿主机写入量会暴涨。可以在 daemon.json 里加日志轮转:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }然后重启 Docker 服务,老容器需要 recreate 才能生效。其次,模型权重文件如果被复制进镜像内,每次容器重建都会产生大量写入;更推荐的是把权重目录以只读卷挂载进容器:
docker run -v /mnt/models/qwen3.8-flash-next:/models:ro ...只读挂载可以避免很多“莫名其妙的写入”。容器层里如果发生文件修改,OverlayFS 会触发 copy-up,把整个文件从镜像层复制到容器层。模型权重动辄好几个 GiB,因为一次误写就产生整个文件的复制写入,代价极高。
我在自己的环境里就不用 Docker 跑模型,而是直接宿主机运行拉取的 Python 依赖虚拟环境,省掉容器层的同时也少了一层排查障碍。如果你只是图省事用容器,建议至少把 /root/.cache、/tmp 等路径挂载成 tmpfs 或者匿名卷,避免频繁写宿主机磁盘。
4.5 写入监控告警:别等硬盘报废才后悔
优化完成之后,最好建立一个持续监控机制,防止类似问题再次发生。最简单的方案是用 cron 每 10 分钟采集一次 SMART 写入量,对比上次值,如果超过阈值就发告警。这里给一个我能直接用起来的脚本思路:
#!/bin/bash CURRENT=$(smartctl -a /dev/nvme0n1 | awk '/Data Units Written/{print $NF}' | tr -d ',') echo "$(date +%s) $CURRENT" >> /var/log/disk_write_history配合一个简单的 awk 对比脚本,在 10 分钟内 Host_Writes 增量超过 5GiB 时通知自己。这个阈值可以根据日常使用量调节,我实测优化前的机器 10 分钟增量经常超过 1GiB,优化后普遍低于 50MiB,所以阈值设成 500MiB 能覆盖异常但不误报。
有条件的话还可以配合 Grafana + Prometheus 的 node_exporter,它自带 NVMe 写入量指标,画出来更直观。不过对个人用户来说,cron 脚本足够用了,关键是“报警后能定位”的闭环能力。
5. 排障速查与避坑记录
5.1 排查问题速查表
这里把整个排查过程整理成一张速查表,下次再遇到硬盘疯狂写入时,按顺序查一遍:
| 检查项 | 命令/配置 | 判断标准 |
|---|---|---|
| SMART 总量 | smartctl -a /dev/nvme0n1 | 对比前后两次 Host_Writes 差值 |
| 实时磁盘写入 | iostat -dx 1 10 | wkB/s 是否长时间超过 20MB |
| 进程级 IO | iotop -o -P | 看用户进程、内核线程谁在写 |
| 文件级写入路径 | fatrace -t -m /home -m /var | 确认具体写入文件 |
| swap 回收状态 | vmstat 1 | si/so 列是否持续非零 |
| 日志轮转 | logrotate -d /etc/logrotate.d/模型日志 | 检查有没有轮转策略 |
| core dump 大小 | du -sh /var/lib/systemd/coredump | 单个文件是否超过 GiB 级 |
| Docker 日志 | docker inspect 容器名 | LogPath 对应的 json 文件大小 |
| 挂载参数 | mount | 是否没有 noatime 导致 atime 更新 |
每一行都值得实际跑一遍。我这次的排查顺序就是从第一行开始,逐步缩小范围,最后在第四行找到具体文件、在第六行和第七行发现辅助大坑。
5.2 实操中踩过的三个坑
第一个坑是我一开始只在用户目录下找大文件,忽略了 /var/lib/systemd/coredump。系统崩溃时生成的 core dump 文件非常大,但普通用户没有权限看 /var/lib 下的内容,导致排查了很久没发现。后来用 sudo du 挨个目录扫才发现这个隐藏写入源。这个教训很重要:排查磁盘写入不要只盯着用户目录,系统目录、容器目录、journal 目录都可能藏着更大的写入源。
第二个坑是修改 swappiness 和 zram 后没有立刻重启模型服务。系统参数改了,但模型进程里已加载的权重页和缓存策略不会自动刷新,导致优化后的头一两个小时仍然有旧策略触发的大量写入。正确做法是调整完系统参数后,重启一次模型服务,让所有内存页重新按新策略加载。
第三个坑是日志重定向到 /dev/null 时忘了处理程序内部的 logging handler。很多 Python 模型服务在代码里自己配置了 FileHandler,输出到固定路径,这种情况下即使你把 stdout 重定向到 /dev/null,FileHandler 照样疯狂写文件。要记得在代码层面把 logging 的输出目标改成 StreamHandler 或者加一个 RotatingFileHandler 设置 maxBytes。
5.3 一个隐藏很深的元凶:预填充阶段的临时文件
最后还发现一个不容易注意到的写入源:qwen3.8 flash next 在预填充(prefill)和深层蒸馏推理时,会在临时目录生成一些中继文件。我去翻了源码,发现它对长输入的 prefill 做了分块处理,每块算完的中间状态先写到临时目录,全部算完后统一合并。这个设计本意是节省内存,但在默认配置下临时目录落在磁盘上,导致每完成一次长文档对话就写入几 GiB 的数据。
处理办法有两个:一是把临时目录环境变量指到 tmpfs:
export TMPDIR=/dev/shm/qwen-tmp二是在加载模型时把分块大小调小,减少单次落盘的块体积。具体参数要看模型加载器支持什么,一般叫 chunk_size 或 prefill_chunk_size。调小后内存占用上升,但写入量下降。考虑到 Strix Halo 本身内存带宽足够,我选择把临时目录放 tmpfs,内存压力小幅上升但磁盘写入直接归零。
如果再遇到这类问题,按照上面 5.1 的速查表跑一遍,绝大部分写入源都能定位出来。关键是不要一拍脑袋去“优化模型文件”,先把系统层、容器层、日志层这三层都排查干净。毕竟本地大模型推理对很多人来说是 7x24 小时后台服务,硬盘的长期健康比单次推理速度重要得多。