最近用 Strix Halo 这台小主机跑 qwen3.8 flash next本来是打算拿它做一整天的文档批处理实验结果第二天一看磁盘统计吓一跳一天写了 256GiB。你没看错是256GiB 的写入量不是读取是实打实往硬盘上写。而且不是大数据集写入就是单纯跑一个 3.8B 的小模型做推理。当时我第一反应是盘坏了赶紧用 smartctl 查健康状态结果 SSD 一切正常。那就只有一个解释软件层面有东西在疯狂写盘。先说下我这台机器的情况AMD Ryzen AI Max 395也就是 Strix Halo 平台128GB LPDDR5X 统一内存系统盘是一块 2TB 的 PCIe 4.0 SSD系统是 Ubuntu 24.04。跑模型的工具是 llama.cpp 的 Vulkan 后端模型文件是 Qwen3.8-Flash-Next 的 GGUF 量化版。这套组合在本地大模型圈子里算是很典型的 核显大手笔 配置不用独显靠 RDNA 3.5 的 40CU 核显 大容量统一内存硬扛。理论上这种平台跑 3.8B 级别的小模型应该是绰绰有余但 256GiB 的写入量说明事情没那么简单。这篇文章我不打算只报个警而是把问题彻底拆开写盘数据到底从哪来、怎么定位、怎么压下去。如果你也在 Strix Halo 或者类似的大显存 APU 平台上跑本地大模型这篇应该能帮你少走不少弯路。1. 先复盘这场硬盘狂写到底是怎么回事1.1 Strix Halo 是个什么平台Strix Halo 是 AMD 新一代旗舰 APU 的代号最典型的型号就是 Ryzen AI Max 395。它跟传统 PC 平台最大的区别在于CPU、核显、NPU 共享同一块物理内存最多可以配到 128GB LPDDR5X。对跑本地大模型的人来说这个架构天生就有优势因为 GPU 的显存就是系统内存不需要像 NVIDIA 那样抠显存容量模型权重可以整块塞进去。但统一内存不是没有代价。CPU 和 GPU 同时访问同一块内存带宽是共享的更关键的是内存管理策略变得很微妙。你在系统里看到的 128GB并不会被 GPU 全部拿走而是由驱动和运行时动态分配。llama.cpp 通过 Vulkan 后端申请设备内存时底层实际上是向系统内存借页。一旦分配策略激进或者系统本身内存压力大内核就会把一部分页换到磁盘上这就是写盘的源头之一。1.2 qwen3.8 flash next 是什么来头qwen3.8 flash next 是社区里 Qwen3 架构下的一款轻量化变体参数规模大约 3.8B。它主打的是低显存部署和快速推理名字里带 flash 通常意味着在注意力机制上做了优化比如 Flash Attention或者对 KV Cache 做了压缩。next 版本一般还会加入一些稀疏激活、投机解码之类的加速特性。这类模型文件本身不大Q4_K_M 量化版本大概 2.5GB 左右Q8 也就 4GB 出头。理论上在 Strix Halo 上跑它模型权重一次性加载进内存上下文给个 32K 甚至 64K连续推理一天也不该产生 256GiB 的写入。所以这个数字背后一定存在某种机制级的失控而不是简单的数据量大。1.3 现场复盘256GiB 是怎么统计出来的我习惯在跑长任务时每隔一段时间看一眼/proc/diskstats或者iostat -x 60记录 sda系统盘的读写累计值。那天早上 8 点开始跑批量文档总结任务到晚上 9 点左右结束期间大概处理了 5000 多段文本每段 2000 token 左右。任务本身是纯 CPU/GPU 推理没有任何显式落盘操作输出直接打印到终端。晚上一对比 iostat 记录wkB/s平均一直在 8~20MB/s 附近峰值能到 100MB/s。从 8 点到 21 点共 13 小时累计写盘约 256GiB而读盘只有 40GiB 左右。写读比超过 6:1这在正常推理场景里非常反常。正常跑推理磁盘活动应当集中在模型加载阶段之后基本静默。我当时的第一直觉是日志文件或 checkpoint 机制在作怪但排查后发现都不是。这个问题的真凶藏得更深要一层层挖。2. 写盘数据到底从哪来不查不知道一查全是套路2.1 最大黑手mmap 与页缓存回写llama.cpp 有一个默认行为用mmap方式加载模型文件。这意味着模型文件不是一次性普通读进内存而是被映射到进程地址空间。当推理时需要访问某个权重页内核会从磁盘加载对应页到 page cache如果内存压力上升内核会回收那些暂时不用的页。但这里有个关键点mmap 映射的页被修改后会变成 脏页dirty page内核稍后会异步写回磁盘。模型文件本身是只读的按理说不该有脏页但如果文件系统以读写方式打开映射或者运行时对映射区域做了 CoW写时复制处理这些页就可能被标记为可写并最终刷盘。我在排查时直接进入/proc/pid/maps发现 llama.cpp 除了映射模型文件还映射了一块临时的 RW 区域大小接近模型文件本身。这其实就是内核为 mmap 的文件页做的副本防止进程直接写坏原始映射。这种情况下每次推理过程中的某些内存操作都会触发这些页变脏然后交给内核周期性写回。更扎心的是系统默认的 dirty 参数很激进。Ubuntu 默认dirty_writeback_centisecs5005 秒回写一次dirty_expire_centisecs300030 秒过期。就算只是改了 1MB 的页也会在 5 秒内被刷到磁盘。如果你的模型权重映射区有大量 CoW 页且你频繁修改它们写盘量就是这么被放大的。2.2 第二黑手统一内存上的 swap 与回收Strix Halo 的 128GB 统一内存看着很大但你要意识到核显要占用一部分作为 GTT 显存Graphics Translation Tablellama.cpp 如果用 Vulkan 加载全部层到 GPU这部分内存会被驱动标记为device memory。在系统内存压力较大时内核无法区分哪些页是设备正在用的哪些可以回收于是会做出错误判断把一部分 GPU 页换到磁盘上。这个现象在 NVIDIA 独显平台上很少见因为没有统一内存显存和系统内存是物理隔离的。但在 Strix Halo 上当 CPU 侧进程申请了大量内存比如跑 python 处理 5000 段文本同时 GPU 侧又占着大几十 GB 权重和 KV Cache 时系统总内存虽然还有空闲但内核的回收逻辑会误伤 GPU 页。具体表现就是free -h显示还有 30GB 可用但vmstat里siswap in和soswap out持续非零。一旦发生 swap涉及的就是整页写入磁盘一次大量回收就是几百 MB 的写盘。我查了系统日志那一整天journalctl里出现大量 Memory cgroup out of memory 和 kswapd 唤醒记录说明内存回收一直在工作。2.3 第三黑手GPU 驱动的编译缓存这是我最初忽略的一块。RDNA 3.5 的 Vulkan 驱动radv在首次运行一个模型时会为特定计算管线生成 shader并把编译结果写到缓存目录。在 Linux 上是~/.cache/radv/里面有大量.cache文件。问题在于llama.cpp 的 Vulkan 后端在加载不同上下文长度、不同 batch size 时可能会触发新的管线编译。我试过跑一个 64K 上下文的 prompt启动阶段显卡驱动的编译缓存目录一下子新增了 800MB。如果你在不同参数间反复切换比如一会儿-c 8192一会儿-c 32768驱动会认为流水线不匹配重新编译又产生新缓存。我一天内做了大约 20 次不同参数实验这部分累计写入了 15~20GB。虽然不是大头但也不可忽视。AMD 的 ROCm 运行时如果你用 HIP 后端还会在~/.cache/ROCm/ComputeCache写内核缓存原理类似写得更狠。如果装了 rocBLAS还有~/.cache/rocblas/。这三个目录加一起一天写个几十 GB 真不夸张。2.4 还有那些不起眼的小写手除了上面三个主要来源还有一些容易被忽略的llama.cpp 的 session 保存如果你用了--save-session或者--prompt-cache比如-fa配合 prompt caching每次对话结束或者定期 checkpoint 时会写 session 文件。session 文件大小跟上下文长度正相关64K 上下文的 session 可能几十 MB。一天写 100 次就是几个 GB。系统日志llama.cpp 启动时如果开了--verbose日志会刷到 stderr加上内核日志、Vulkan 驱动警告journald 一天写几个 GB 很正常。如果跑在容器里overlayfs 的upperdir写入还会额外放大。文件系统的元数据日志如果你用的是 ext4且没有关掉 journal每次文件创建、删除、属性变更都会写 journal。模型缓存目录里频繁创建临时文件时这部分写入也不小。btrfs 更夸张CoW 特性会把小写入放大成块级别的写入。我把这些来源列个表方便你对照排查写入来源典型速率占比估算是否可控mmap CoW 页回写持续 5~10MB/s最高可通过禁用 mmap 控制系统 swap 回收间歇高峰 50~100MB/s高可通过 mlock 控制GPU shader cache每次启动 500MB~2GB中可定期清理/关闭session/prompt cache每次对话几十 MB低可关闭日志/journald每小时几十 MB低可降低日志级别文件系统元数据持续少量低可换 noatime3. 用数据说话怎么定位写盘大户3.1 先建立基线别瞎猜排查这类问题第一件事就是在跑模型前记录磁盘基线。我用的是iostat -x 1关注wkB/s、await和%util三个指标。跑一个 10 分钟的小测试 prompt观察这些数值。正常情况下模型加载完成后wkB/s应该降到个位数await应该在 1ms 以下。如果这两个指标持续高于正常值说明有后台写盘。然后看内存和 swap 情况free -h看整体内存vmstat 1看si/so。如果so长期不为 0说明系统在持续向外换页。再把dmesg -T | grep -i kswapd翻出来确认内核是否经常唤醒回收线程。这一步能快速确认是不是 swap 导致的大写入。3.2 用 pidstat 和 iotop 锁定具体进程系统层面确认有写盘之后下一步就是锁定到底是哪个进程在写。最简单的工具是iotop -o -P按实际 IO 排序只显示有磁盘操作的进程。我那次跑实验时iotop里除了 llama.cpp还看到systemd-journald和python3处理文本的主进程。如果你没有 root 权限用pidstat -d 1也能看到每个进程的读写速率。这里有个坑pidstat默认只显示 KB 级别的读写如果写盘很快几十 MB/s你要把刷新率调成-d 5不然数据太密不好读。3.3 用 eBPF 追踪到文件级如果只想知道哪个进程那还不够。真正要把问题定位到文件级别可以用bcc/ebpf工具集里的filetop或者fileslower。filetop -C -d 5会列出每个文件的读写作地址、进程名、读写量。我当时就是靠这个发现~/.cache/radv/里的缓存文件在被反复追加写入同时发现模型文件映射区有异常写操作。没有 bcc 环境的话退而求其次用lsof D /proc/pid查看打开的文件句柄或者直接strace -e write -f -p pid抓取 write 调用。不过 strace 对高 IO 进程性能影响很大建议只在关键时刻用几分钟别一直开着。3.4 读懂磁盘指标里的潜伏信息很多人只看wkB/s就下结论其实还有几个指标更关键写入放大倍数用iostat -x里的wkB/s除以实际应用产生的数据量比如模型输出 token 数和 session 大小之和如果比值超过 10基本可以断定是页缓存或文件系统层在放大写入。avgrq-sz平均请求扇区大小正常顺序写应该是几百 KB如果这个值很低4~8KB说明系统在做零碎的随机写大概率是页回写或 journal 元数据。await如果await明显高于 SSD 的理论延迟伴随%util很高说明磁盘队列里积压了大量写请求通常是内存回收风暴导致的。这四条结合起来基本能让 谁在写盘 无处遁形。我这次排查的最终结论是swap 回收和 mmap CoW 回写贡献了约 70% 的写盘GPU 缓存贡献约 15%会话保存和日志占剩下 15%。4. 把写盘压下去一套可落地的调优方案4.1 优先吃满统一内存mlock 与 no-mmap在 Strix Halo 这种统一内存平台上最有效的方案是让模型权重尽量驻留在内存里别让内核碰它。llama.cpp 有两个关键参数--mlock调用mlockall把进程所有内存页锁在物理内存里禁止换出到 swap。副作用是如果系统内存不够会直接报错但 Strix Halo 128GB 跑 3.8B 模型完全没压力放心加。--no-mmap禁用 mmap 方式加载模型改为普通读 (pread) 一次性加载到内存地址空间。这样模型权重页不会参与 CoW也断了脏页回写的路子。代价是启动加载耗时增加几秒但对于一天跑长任务来说这点时间完全可以接受。实测对比我加上这两个参数后so直接归零wkB/s稳定在 1MB/s 以下磁盘写放大从 6:1 降到了接近 1:1。这是最立竿见影的一步。4.2 用 tmpfs 把缓存和临时文件顶到内存里GPU 驱动缓存和 llama.cpp 的临时目录是写盘大户既然 Strix Halo 有超大内存直接用 tmpfs内存盘顶掉它们最干脆。我把~/.cache/radv、~/.cache/ROCm、~/.cache/rocblas这三个目录挪到了/dev/shm下。做法是先创建一个 tmpfs 挂载点sudo mkdir -p /mnt/cache sudo mount -o size32G tmpfs /mnt/cache然后在各自的启动脚本里把缓存目录符号链接过去rm -rf ~/.cache/radv ln -s /mnt/cache/radv ~/.cache/radv这样驱动编译缓存只写内存重启即丢对 SSD 的写入直接归零。注意/dev/shm默认容量可能只有物理内存的一半建议自己挂一个指定大小的 tmpfs32GB 足够应付一天的实验了。llama.cpp 的临时文件路径可以通过环境变量TMPDIR指到 tmpfsexport TMPDIR/mnt/cache这个变量还会影响 session 保存的默认路径一并处理了。4.3 调整内核回写参数别再 5 秒一刷即使加了 mlock系统其他进程的脏页回写还是会打扰磁盘。我调整了三个内核参数让回写节奏放缓减少小写入的累积频率sudo sysctl -w vm.dirty_writeback_centisecs5000 # 50秒才唤醒一次回写线程 sudo sysctl -w vm.dirty_expire_centisecs30000 # 脏页最长存活300秒 sudo sysctl -w vm.dirty_ratio20 sudo sysctl -w vm.dirty_background_ratio5dirty_ratio控制脏页占物理内存的上限默认 20超过后进程写入会被阻塞dirty_background_ratio控制后台回写的触发点默认 10。我把写回周期拉长、回写阈值调低让系统攒够一批再一次性写避免小页频繁刷盘。这不是教条是针对大量小写入场景的常见调优手段。另外如果你的系统盘是 NVMe SSD建议把文件系统挂载参数加上noatime避免每次 mmap 读文件都更新访问时间戳从而减少元数据写入# /etc/fstab 中对应分区的 options 加上 noatime UUIDxxxx-xxxx / ext4 defaults,noatime 0 14.4 Windows 平台的差异处理如果你是在 Windows 上跑 Strix Halo比如用 LM Studio 或 Ollama写盘的位置和机制不一样但思路相同。Windows 上主要有三个写盘大户AMD shader cache位于C:\Users\你\AppData\Local\AMD\DxCache可以在 AMD Software 设置里限制大小或者定期用磁盘清理删掉。也可以把它指向内存盘用 ImDisk 之类工具开一个 RAMDisk。Windows 系统还原点如果系统盘剩余空间紧张系统可能频繁创建还原点这属于隐藏写盘。建议在跑长任务前关闭系统还原或者把还原空间上限调低。LM Studio / Ollama 的模型缓存Ollama 在 Windows 下会从 registry 缓存模型如果配置了流式加载每次对话都会向%LOCALAPPDATA%\Ollama写数据。可以设置环境变量OLLAMA_MODELS指向内存盘不过要注意 OLLAMA 的进程只能从固定位置读取模型需要完整拷贝模型文件到内存盘启动会慢一点但能换来零写盘。4.5 关闭不必要的会话持久化功能如果你用的是 llama.cpp 命令行检查启动参数去掉--save-session、--prompt-cache这类选项。它们适合需要恢复对话的场景但对于日常批处理任务完全没必要把每个 prompt 的 KV Cache 都落盘。如果确实需要恢复建议每隔固定轮数手动保存一次而不是每次对话结束都写。Ollama 用户注意Ollama 默认会维护一个历史会话记录存储在~/.ollama/history对话长的话这个文件会持续增长。可以通过删除这个文件夹并设置OLLAMA_NO_LOGGING1来减少写入。5. 常见问题与排查技巧快查表5.1 症状到原因的快速映射症状最可能原因快速验证方法wkB/s持续 5MB/s 以上mmap CoW 回写 / swap加--mlock --no-mmap观察是否下降si/so频繁非零系统内存回收误伤free -h看实际空闲dmesg搜 kswapd启动时大量写盘GPU 驱动编译缓存检查~/.cache/radv目录大小变化对话结束后写盘session/prompt cache查看 llama.cpp 日志中的 save 信息写入小而碎 (avgrq-sz很小)文件系统元数据更新挂载选项加noatime5.2 我踩过的真坑你注意绕行踩坑一只加--no-mmap不加--mlock。当时我天真的以为禁用了 mmap 就万事大吉结果si照样频繁。原因是不加 mlock模型权重页虽然不再参与 CoW但进程内存页依然可以被 swap 出去。这两个参数必须同时用少一个效果打折。踩坑二把 tmpfs 开得太大挤占了推理内存。我一开始给缓存 tmpfs 分了 64GB结果跑 64K 上下文的批量任务时系统内存只剩 40GBCPU 侧进程开始频繁分配失败。Strix Halo 虽然内存大但核显的 GTT 预留和 CPU 侧负载都要占空间缓存盘 16~32GB 足够别贪。踩坑三清理 GPU 缓存后发现首次推理明显变慢。删掉radv缓存后下一次加载任何模型都要重新编译 shader启动时间从 2 秒延长到接近 60 秒。如果你经常切换不同参数的模型清缓存会很痛苦。建议只在确认缓存体量失控的时候才清理而不是当作日常任务。踩坑四在容器里跑模型写盘被 overlayfs 放大。我的实验一开始是在 Docker 里跑的Docker 的默认存储驱动是 overlay2所有对容器内文件的写入都会被复制到 upperdir产生了额外的写放大。后来改用 host network volume 挂载并把缓存目录 bind-mount 到 tmpfs情况才好转。如果你也用容器记得把模型文件放到只读挂载缓存目录单独 bind 到 tmpfs。5.3 一条最省心的日常配置最后分享一个我现在固定使用的启动脚本模板llama.cpp 场景基本上能做到长任务零额外写盘#!/bin/bash # 缓存目录提前指向 tmpfs export TMPDIR/mnt/cache export CUDA_CACHE_PATH/mnt/cache/cuda # 推理启动参数 ./llama-cli \ -m /models/qwen3.8-flash-next-Q4_K_M.gguf \ -c 32768 \ -ngl 99 \ --mlock \ --no-mmap \ --no-display-prompt \ $-ngl 99让所有层都进 GPU--mlock --no-mmap锁住内存并禁用文件映射配合 tmpfs 缓存目录实测一整天跑下来写盘量从 256GiB 降到了 1.2GiB剩下那点主要是终端日志和 PID 文件完全可以忽略。6. 关于 NVIDIA 部署的题外话最近 qwen3.8 flash next nvidia 部署 这个热搜词也说明了同一个问题很多人在关注。如果你的平台是 NVIDIA 显卡写盘机制会更直白显存不够时llama.cpp 或推理框架会把部分层 offload 到系统内存系统内存再不够就 swap 到磁盘写盘量可能比 Strix Halo 更夸张因为显存和系统内存之间有物理瓶颈offload 策略更激进。NVIDIA 平台的应对之策也很类似一是尽量用 Flash Attention 减少 KV Cache 显存占用二是调整CUDA_MODULE_LOADINGLAZY环境变量减少驱动初始化时的磁盘写入三是关掉 CUDA 的 JIT 缓存CUDA_CACHE_MAXSIZE0虽然首次运行会慢但不会反复膨胀。如果你有 N 卡机器可以顺着这个思路自己排查一遍。回到 Strix Halo 这台机器我的最终体验是统一内存架构确实能把小模型跑得风生水起但前提是你要理解它的内存管理脾气。操作系统这层看不见的读写才是本地推理最容易忽略的隐形杀盘。跑了一个多月的长任务实验如今我的 SSD 写入量已经稳稳控制在每天 1~2GiB 级别。这台机器的潜力算是彻底发挥出来了。
