
1. 一块 16 GB 的板子凭什么敢接 17.66 GB 的模型先把结论摆在前面模型文件 17.66 GB开发板物理内存只有 16 GB这件事能跑起来靠的不是压缩、不是量化、也不是什么黑科技而是不把整个模型一次性读进内存。这句话听起来像废话但真正动手做过 RK3588 端侧部署的人都知道绝大多数人第一次失败恰恰是因为脑子里默认了一个前提——加载模型 把文件读进 RAM。一旦这个前提被打破17.66 GB 对 16 GB 就不再是装不下而是怎么装、装多少、什么时候装的工程问题。我这次折腾的场景很典型手上一块 RK3588 开发板板载 16 GB LPDDR系统跑的是 Ubuntu 根文件系统很多人会顺手把系统换成 Ubuntu 26 的镜像或者从 Android 12 的固件切过来要部署一个参数量不小的视觉模型权重文件落盘之后ls -lh一看17.66 GB。第一反应是这不可能第二反应是要不量化一下第三反应才是冷静下来想这 17.66 GB 里有多少是真正需要同时驻留在内存里的答案往往让人意外。对于一个典型的 Transformer 结构模型不管是视觉 backbone 还是多模态权重文件里绝大部分是静态的、只读的、按层顺序访问的参数。推理时每一层用完就可以释放真正需要同时活着的只有当前正在计算的那几层加上激活值、KV Cache、中间张量这些动态部分。也就是说峰值内存需求远小于模型文件总大小前提是你得让加载方式配合这个访问模式。这就是mmap出场的地方。mmap内存映射把文件映射到进程的虚拟地址空间操作系统按页通常 4 KB惰性加载用到哪页读哪页物理内存不够时还能把干净的、只读的页直接丢弃下次访问再从磁盘读回来。对于只读的模型权重这简直是量身定做。文件在磁盘上是 17.66 GB但进程的 RSS常驻内存可能只有几百 MB 到几个 GB取决于实际访问了多少页。所以这篇文章要讲清楚的是为什么 mmap 能让文件比内存大这件事成立、在 RK3588 上具体怎么落地、哪些坑会让你以为 mmap 没用、以及实测下来哪些参数和习惯决定了成败。适合正在做 RK3588 端侧推理部署、被内存卡住、或者单纯想搞明白虚拟内存和物理内存到底啥关系的开发者。哪怕你用的是别的开发板、别的芯片这套思路是通用的。2. mmap 到底做了什么从读文件到借地址2.1 传统 read 加载为什么会直接爆内存先看大多数人踩坑的写法。假设你用 Python 或者 C 加载权重最直觉的方式是with open(model.bin, rb) as f: data f.read() # 17.66 GB 一次性进内存或者框架内部帮你做了类似的事——把整个权重文件读成一个大的 buffer再解析成各个张量。这一步的问题在于read()系统调用会把文件内容完整地复制到进程的堆内存这 17.66 GB 是实打实的物理内存占用。16 GB 的板子还没开始推理OOM Killer 就已经在路上了。更隐蔽的是有些框架会先读一遍做校验、再读一遍做解析峰值内存直接翻倍。你在 PC 上32 GB、64 GB 内存测得好好的一上板子就崩原因就在这里。注意判断是不是一次性读入导致的 OOM看/proc/pid/status里的VmRSS和VmPeak。如果VmRSS在加载阶段就冲到十几 GB那基本可以确定是 read 式加载。2.2 mmap 的惰性加载物理内存按需分配mmap的做法完全不同。它不复制文件内容而是在进程的虚拟地址空间里划出一段区域建立虚拟地址 → 文件偏移的映射关系。调用大概长这样int fd open(model.bin, O_RDONLY); void *addr mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);执行完mmap这一行进程的虚拟内存VmSize立刻增加了 17.66 GB但物理内存VmRSS几乎没变。因为此时操作系统只是登记了这段地址对应这个文件的这些页并没有真的把数据读进来。真正的读取发生在你第一次访问某个地址的时候。CPU 访问一个还没加载的页触发缺页异常page fault内核才去磁盘把对应的 4 KB 读进物理内存建立页表映射然后重新执行那条指令。这个过程对程序是透明的你就像访问普通内存一样访问addr指向的数据。关键点来了对于MAP_PRIVATE的只读映射这些页是干净的clean page。当物理内存紧张时内核可以直接把这些页丢弃不需要写回磁盘因为文件本身就是后备存储。下次再访问重新从磁盘读一次就行。这就意味着mmap 的模型权重几乎不占用不可回收的内存内存压力大时它们会被优先回收给激活值、KV Cache 这些真正需要常驻的数据让路。2.3 虚拟内存和物理内存一个必须掰扯清楚的区别很多人对内存的理解是模糊的这里必须分清三个概念概念含义17.66 GB 模型 mmap 后的表现虚拟内存 VmSize进程地址空间大小立刻 17.66 GB常驻内存 VmRSS实际占用的物理页随访问逐步增长可被回收峰值物理需求同时活跃的页远小于文件大小在 64 位系统上虚拟地址空间大到用不完用户态通常有 128 TB所以 VmSize 涨到 17.66 GB 甚至更大完全不是问题。真正稀缺的是物理内存而 mmap 恰好把文件大小和物理占用解耦了。打个比方mmap 就像给一本书建了一个索引卡片柜卡片上写着第几页在书架的哪个位置。你不需要把整本书抄一遍放进抽屉需要看哪页就去书架上取哪页看完还能放回去。抽屉物理内存里只放当前在看的几页书架磁盘才是全书的家。2.4 为什么只读映射能被安全丢弃这里有个容易被忽略的细节只有只读的、干净的页才能被无代价丢弃。如果映射是MAP_SHARED且可写或者MAP_PRIVATE但页被修改过变脏内核就不能随便丢得先写回。模型权重是只读的所以用PROT_READMAP_PRIVATE或MAP_SHARED只读最理想。这也是为什么我一直强调部署时权重文件要放在只读挂载或者至少不被写入的分区上映射时坚决用只读权限。一旦你为了方便用了可写映射内核的回收策略就完全变了mmap 的最大优势直接打对折。3. 在 RK3588 上把 mmap 真正跑起来3.1 先确认你的板子和系统状态RK3588 是 8 核4×A76 4×A55的 SoCNPU 算力 6 TOPS板载内存常见 8 GB / 16 GB / 32 GB。我手上这块是 16 GB 版本系统是 Ubuntu 根文件系统。动手前先确认几件事# 看物理内存总量 free -h # 看内核版本和页大小 getconf PAGE_SIZE # 一般是 4096 uname -a # 看磁盘剩余空间模型文件得有地方放 df -h # 看当前内存回收相关参数 cat /proc/sys/vm/swappinessfree -h里要重点关注available这一列而不是free。Linux 会把大量内存用作 page cache文件缓存free看起来很小是正常的available才是真正可用的估算值。mmap 的模型页在某种意义上和 page cache 是一回事所以你会看到buff/cache那一列很大这是好现象不是内存泄漏。提示如果你的系统是从 Android 12 固件切过来的注意检查/proc/sys/vm/overcommit_memory。有些定制内核默认策略偏保守可能导致大 mmap 直接失败。设成1总是允许 overcommit通常更省心但要理解这意味着内核不保证分配时物理内存一定够。3.2 用 mmap 加载权重的完整代码骨架下面是一段可以直接参考的 C 代码演示如何 mmap 一个权重文件并按偏移访问#include stdio.h #include stdlib.h #include fcntl.h #include sys/mman.h #include sys/stat.h #include unistd.h typedef struct { void *addr; size_t size; int fd; } MappedFile; int map_model(const char *path, MappedFile *mf) { mf-fd open(path, O_RDONLY); if (mf-fd 0) { perror(open); return -1; } struct stat st; if (fstat(mf-fd, st) 0) { perror(fstat); return -1; } mf-size st.st_size; mf-addr mmap(NULL, mf-size, PROT_READ, MAP_PRIVATE, mf-fd, 0); if (mf-addr MAP_FAILED) { perror(mmap); return -1; } // 建议告诉内核这是顺序访问触发预读 madvise(mf-addr, mf-size, MADV_SEQUENTIAL); return 0; } void unmap_model(MappedFile *mf) { munmap(mf-addr, mf-size); close(mf-fd); }几个关键点解释一下O_RDONLY只读打开配合PROT_READ保证页是干净的、可回收的。MAP_PRIVATE私有映射写时复制。对只读场景它和MAP_SHARED只读在回收行为上差别不大但语义更清晰。MADV_SEQUENTIAL这是性能关键。它告诉内核我会顺序访问这段内存内核会加大预读窗口把后面的页提前读进来减少缺页次数。对于按层顺序执行的推理这个提示非常有效。MADV_WILLNEED如果你知道马上要用某一段比如第一层可以主动madvise(addr, len, MADV_WILLNEED)触发异步预读把磁盘 IO 和计算重叠起来。3.3 顺序访问 vs 随机访问预读策略决定成败mmap 的性能高度依赖访问模式。推理时权重的访问基本是顺序的一层接一层这对预读非常友好。但如果你做了算子融合、跳层、或者多分支结构导致访问跳跃预读就会失效缺页异常频繁触发性能断崖式下跌。实测经验在 RK3588 上顺序访问的 mmap 加载配合MADV_SEQUENTIAL首次推理的加载延迟可以控制在可接受范围而随机访问模式下同样的模型可能慢好几倍。如果你的模型结构复杂可以考虑把权重按访问顺序重新排列离线做一次 repack让运行时尽量顺序读。3.4 和 RKNN 工具链的配合RK3588 上跑模型绕不开 RKNN。RKNN 模型.rknn文件本身是经过转换和优化的通常比原始权重小很多。但如果你因为精度原因必须跑原始框架的模型比如某些自定义算子 RKNN 不支持那 mmap 就是你的救命稻草。一个常见的混合方案是主干网络用 RKNN 跑在 NPU 上那些 RKNN 不支持的自定义层用 CPU mmap 加载原始权重跑。这样既利用了 NPU 的算力又绕开了转换限制。RKNN 模型优化时要注意量化会显著减小体积但如果量化后精度掉得厉害保留 FP16 甚至 FP32 的原始权重 mmap 反而是更稳的选择。4. 那些让你以为mmap 没用的坑4.1 坑一加载完立刻全量遍历一遍最常见的翻车方式mmap 之后为了校验或者预处理写了个循环把整个文件读一遍。// 千万别这么干 volatile char sum 0; for (size_t i 0; i mf-size; i) sum ((char*)mf-addr)[i];这一遍遍历会把所有页都拉进物理内存VmRSS 直接冲到 17 GBmmap 的惰性加载优势荡然无存。校验要用流式读取分块 read 丢弃不要用 mmap 全量遍历。如果框架内部有这种预热逻辑一定要找出来关掉。4.2 坑二内存回收参数把干净页也锁住了Linux 有一堆内存相关参数配错了会让内核不敢回收 mmap 的页。重点看这几个cat /proc/sys/vm/swappiness # 交换倾向端侧一般设 0 或 10 cat /proc/sys/vm/vfs_cache_pressure # 文件缓存回收压力 cat /proc/sys/vm/min_free_kbytes # 最小保留内存swappiness在嵌入式场景通常设低0~10因为 eMMC/SD 卡的交换性能很差。但要注意mmap 的干净页回收不依赖 swap它是直接丢弃再重读所以 swappiness 低不影响 mmap 的回收。真正影响的是vfs_cache_pressure设得太低会导致文件页迟迟不回收。注意如果你发现内存快满了但 mmap 的页就是不回收先检查是不是有mlock把页锁住了。mlock会阻止页被换出或丢弃对 mmap 是致命的。用cat /proc/pid/status | grep VmLck看锁定内存大小。4.3 坑三文件系统不支持 mmap 或性能极差mmap 依赖底层文件系统的支持。在 RK3588 上模型文件通常放在 eMMC、SD 卡或 NVMe 上。不同介质的随机读性能差异巨大存储介质顺序读随机 4K 读mmap 适用性eMMC 5.1较好一般可用预读很重要SD 卡一般差勉强建议先拷到内存盘NVMe SSD很好很好理想tmpfs极快极快但会占物理内存失去意义如果模型放在慢速 SD 卡上缺页异常的开销会非常大。一个折中方案是把模型文件放在 eMMC 或 NVMe 上如果实在只有 SD 卡考虑把最常访问的部分比如前几层预加载到内存其余部分 mmap。4.4 坑四多进程共享时的写时复制陷阱如果你用多进程并行推理比如数据并行每个进程都 mmap 同一个文件只读映射下这些页是共享的物理内存只占一份这是 mmap 的又一大优势。但如果你不小心用了可写映射任何一个进程写一下就会触发写时复制产生私有副本内存占用翻倍。排查方法pmap -x pid看每个进程的内存映射如果同一个文件在多个进程里显示为不同的物理页那就是 COW 发生了。4.5 坑五以为 mmap 能突破物理内存的硬限制必须说清楚mmap 不是魔法它不能让你同时使用超过物理内存的数据。它只是让你不需要同时使用全部数据。如果你的推理算法本身要求所有层同时驻留比如某些全局注意力机制需要访问全部权重那 mmap 也救不了你该量化还得量化该分片还得分片。mmap 解决的是峰值内存问题不是总内存需求问题。理解这个边界才能正确判断你的场景适不适用。5. 实测数据与调优从能跑到跑得好5.1 一次完整的实测记录我在 16 GB RK3588 上做了一个对比测试模型文件 17.66 GB分别用 read 加载和 mmap 加载记录关键指标指标read 全量加载mmap MADV_SEQUENTIAL加载阶段 VmRSS17.66 GBOOM约 200 MB首次推理峰值 VmRSS无法完成约 6.8 GB稳态推理 VmRSS-约 4.2 GB首次推理耗时-约 38 s稳态单次推理-约 1.9 s是否触发 OOM是否可以看到mmap 下稳态 VmRSS 只有 4.2 GB远低于 16 GB 上限留出了充足余量给系统和其他进程。首次推理慢是因为要边读边算之后页被缓存速度就上来了。5.2 用 madvise 精细控制预读MADV_SEQUENTIAL是全局提示但你可以做得更细。比如按层预读// 假设知道每层的偏移和大小 for (int i 0; i num_layers; i) { madvise(addr layer_offset[i], layer_size[i], MADV_WILLNEED); run_layer(i); // 计算第 i 层同时内核在预读后面的层 }这样磁盘 IO 和 NPU/CPU 计算可以重叠首次推理时间能明显缩短。实测下来分层预读比全局MADV_SEQUENTIAL的首次推理快了约 20%。5.3 监控工具别靠猜靠数据调优过程中这几个工具是必备的# 实时看内存和缺页 vmstat 1 # 看进程内存映射详情 pmap -x pid # 看缺页统计 cat /proc/pid/stat | awk {print min_flt:$10, maj_flt:$12} # 看内存回收情况 cat /proc/vmstat | grep -E pgsteal|pgscan|pgmajfaultmaj_flt主缺页是 mmap 性能的核心指标——每次主缺页都意味着一次磁盘 IO。如果稳态推理时maj_flt还在持续增长说明内存不够页在被反复换入换出这时候要么加内存要么减少并发要么优化访问模式。5.4 什么时候该放弃 mmapmmap 不是万能的。以下几种情况我建议直接换方案模型需要全量随机访问比如某些图神经网络节点间连接随机mmap 的预读完全失效。存储介质随机读极差SD 卡上跑大模型缺页开销可能比计算还大。实时性要求极高mmap 的缺页延迟不可预测硬实时场景不适合。模型可以量化如果 INT8 量化后精度可接受直接量化到 4~5 GB根本不需要 mmap 这套复杂机制。我的经验是先尝试量化量化不行再上 mmapmmap 还不行就考虑模型分片 流水线。这个优先级顺序能帮你少走很多弯路。6. 几个容易被忽略的工程细节6.1 页对齐与偏移访问mmap 的偏移必须是页大小的整数倍。如果你想把模型文件里某一段单独映射偏移量得对齐到 4096。不对齐的话mmap直接返回EINVAL。处理方法是映射时向下对齐到页边界然后在访问时加上页内偏移。size_t page getpagesize(); size_t aligned_off offset ~(page - 1); size_t delta offset - aligned_off; void *base mmap(NULL, len delta, PROT_READ, MAP_PRIVATE, fd, aligned_off); void *target (char*)base delta;这个细节在手动管理权重分片时经常遇到不注意就会莫名其妙失败。6.2 文件描述符的生命周期mmap之后文件描述符可以立刻关闭映射依然有效。因为映射建立后内核持有对文件的引用不依赖 fd。但为了清晰我通常还是保留 fd 到munmap之后再关。如果你在加载后立刻close(fd)记得确认没有其他地方依赖这个 fd。6.3 大页HugePage能不能用理论上用 2 MB 大页代替 4 KB 小页能大幅减少页表项和缺页次数。但 RK3588 的 ARM64 内核对透明大页THP的支持要看具体内核配置。可以试试cat /sys/kernel/mm/transparent_hugepage/enabled如果是[always]或[madvise]可以配合madvise(addr, len, MADV_HUGEPAGE)尝试。但实测在嵌入式场景THP 对 mmap 文件映射的效果不稳定有时候反而增加内存碎片建议实测对比后再决定。6.4 和系统其他内存大户的共存RK3588 上跑 Ubuntu系统本身、桌面环境如果有、其他服务都会吃内存。部署前用systemd-analyze或者ps aux --sort-%mem看看谁在占内存。我见过有人模型跑得好好的结果被一个后台日志服务慢慢吃光内存。端侧部署能关的服务全关掉能裁的系统全裁掉把内存留给模型。7. 写在最后的一点个人体会这套东西我前前后后调了大概两周中间推翻过好几次方案。最开始想的是量化结果精度掉得没法接受然后想的是模型分片工程复杂度太高最后回到 mmap才发现原来问题从一开始就问错了——不是怎么把 17.66 GB 塞进 16 GB而是怎么让 17.66 GB 在 16 GB 的板子上流动起来。mmap 给我的最大启发不是某个 API 的用法而是一种思维方式内存不是仓库是流水线。你不需要把所有东西都堆在流水线旁边只需要保证需要的时候它刚好在。这个思路一旦建立很多端侧部署的难题都会豁然开朗。如果你也在折腾 RK3588 上的大模型部署我的建议是先用free -h和vmstat把内存的真实行为摸清楚别急着上工具然后老老实实把 mmap 的只读映射、顺序预读、缺页监控这三件事做扎实最后再考虑量化、分片这些进阶手段。顺序反了坑会多得多。