8 GB 内存的手机,为什么塞不进 1.5 GB 的模型
端侧模型装不装得下,常被当成内存容量问题。真正决定上限的是另一条式子:可用于钉住权重的缓存,等于权重循环块总大小减去存储带宽乘允许延迟。一台 8 GB 的手机,系统只划出 256 到 512 MB 给这段循环;int4 量化下 15 亿参数要 0.75 GB,超出的部分每次换入就要几百毫秒,而生成一个 token 只给 80 到 125 毫秒。
八 G 内存里的半 G
一台 8 GB 的安卓手机,内存分配大致是这样:系统与常驻服务吃掉 3.5 GB,应用与图形占 2.5 GB,页缓存和后台留 1 GB,还有 0.5 GB 没被回收,落到模型权重上的只剩 0.5 GB。
这个 0.5 GB 不是随手划的,它对应的是操作系统的缓冲区缓存配额。真实机型上这个数字在 256 MB 到 512 MB 之间,取决于系统版本和后台压力。
而模型那边,一个 10 亿参数的模型做 int4 量化,权重是 0.5 GB;15 亿参数是 0.75 GB。两者在容量上都不算大,问题出在它们必须同时满足另一个条件:每一次生成 token,都要把整个权重循环扫一遍。
图 1 8 GB 内存的去向与模型实际拿到的份额(依据原文第 4 节容量模型与第 5 节平台参数绘制)
手机的内存管理里有两个容易混淆的量:空闲内存和可缓存内存。
空闲内存是没被任何进程占用的部分,系统会尽量把它压到很低,因为空着就是浪费。可缓存内存是操作系统拿来放文件页的那一块,模型权重文件被 mmap 进来之后,落在这一块里。
关键在于,可缓存内存的大小不等于空闲内存加上文件页。它是一个有配额的池子,池子的替换策略决定哪些页留下。空闲的 0.5 GB 不会自动变成模型能用的 0.5 GB,它要经过一轮分配和替换才可能被借用,而在内存压力下随时会被收回。
论文的做法是把这个问题拆成文件级的访问模式。作者用 strace 在真机上按 4 KB 粒度采了一段 trace,发现模型应用的 I/O 有三类形态:初始化时的一次性顺序扫描,常驻的小热集,以及推理过程中反复循环的权重块。
三类要区别对待。一次性扫描应该尽快驱逐,否则会把缓存冲干净;热集是分词器、元数据和索引文件,总共不到 4 MB,必须完整保留;权重块的循环要钉住,保证每一轮都确定性命中。
带宽乘延迟才是抵扣项
把三类分开之后,容量关系可以写成一条很干净的式子:
S ≥ max{ 0, N 降 BW × T }
S 是需要钉住的循环缓存,N 是权重循环块的总大小,BW 是存储的持续读带宽,T 是允许的取数延迟。
这条式子的意思是,缓存不必装下全部循环,只要装下带宽在允许延迟内取不回来的那部分。带宽越高、延迟越松,需要钉住的比例越小。
T 的取值由解码速度定。论文实测的生成速度是每秒 8 到 12 个 token,换算成每个 token 80 到 125 毫秒,取整之后把 T 定在 0.1 秒。
分词器那 4 MB 不能丢
三类访问模式里,最不起眼的是热集,代价最高的也是它。
热集是分词器、模型元数据和索引文件,加起来不到 4 MB。它的特点是每个 token 都要访问,而且访问位置分散,不像权重那样按顺序扫。
这 4 MB 如果被权重挤出去,下一次生成 token 时就要重新读回来。按 50 微秒的 I/O 建立延迟算,就算数据只有几百 KB,一次重新加载也要几毫秒,而且是在最关键的路径上。
所以论文的策略是先划出 4 MB 把热集钉死,剩下的才分给权重循环。这个顺序不能反。反过来做,缓存看着用满了,命中率反而掉。
手机上还有一块内存会来分食这个池子,就是 KV 缓存。
以 10 亿参数、16 层、16 个注意力头、每头 128 维、上下文 2048 为例,KV 缓存按半精度存,占用是两次乘层数乘头数乘维度乘序列长度乘两字节,算下来 256 MB。
0.5 GB 的 int4 权重,加 256 MB 的 KV 缓存,一共 0.75 GB。这个数字已经超过了手机能给模型的 512 MB 上界。
在手机的统一内存架构里,这两块从同一个物理池子里拿。KV 缓存随上下文长度线性增长,用得越久越大,权重文件占的页缓存份额就被一点点挤出去。这也是为什么长对话跑到后面会明显变慢,不是算力不够,是权重页被换出去了。
手表级差在带宽不在容量
同一条式子,代进两类平台,结果差一个数量级。
手表级:内存 1 到 2 GB,能给模型的缓存约 64 MB,热集占 4 MB,剩下 60 MB。存储是 eMMC,标称顺序读 200 到 300 MB/s,实际受热和功耗限制只有 100 MB/s 左右。按 T 等于 0.1 秒算,带宽能抵扣 10 MB,可承载的循环规模是 60 加 10,约 70 MB。
手机级:内存 6 到 8 GB,缓存池 512 MB 到 1 GB,划给模型的循环数据 256 到 512 MB。存储是 UFS 4.0,持续顺序读不低于 1 GB/s,抵扣 100 MB,可承载的循环规模 356 到 612 MB。
两边的缓存差 4 到 8 倍,可承载的循环规模差 5 到 8.7 倍。多出来的那部分不来自内存,来自存储带宽。把手表的 eMMC 换成 UFS,可承载规模立刻从 70 MB 跳到 160 MB 以上,而内存一个字节都没加。
图 2 可承载的循环规模随存储带宽的变化(依据原文第 4 节式 1 与第 5 节平台参数绘制)
一次换页就是半个 token 的时间
反过来说,超出缓存的那部分要付多少代价,可以直接算。
512 MB 的循环块,如果每次生成 token 都要从 UFS 全量读一遍,1 GB/s 下是 0.51 秒。而 token 的预算是 80 到 125 毫秒。也就是说,一次全量换入要花掉四到六个 token 的时间。
分块之后好一些。按 4 KB 一块算,每次只换入当前层要用的那几十兆,10 MB 在 1 GB/s 下是 10 毫秒,占 token 预算的 8% 到 12%,勉强能接受。代价是每层都要停下来等 I/O,流水线被打断,实际吞吐还要再降一档。
论文测的存储每次 I/O 建立延迟是 50 微秒。这个数字单看不小,但在 10 毫秒的传输面前只占 0.5%,可以忽略。真正的瓶颈是带宽本身,不是寻道。
记账本身的开销也要算。512 MB 按 4 KB 一块切开是 131072 块,每块一份元数据,合计 1 MB 上下,占容量的 0.2%。这块开销不能省,因为没有块级记账就没法判断哪一块该留。
模型规模被存储带宽定死
把这条链走完,会得到一个和常识相反的结论。
端侧模型能跑多大,不由内存容量决定,由存储带宽乘允许延迟这个乘积决定。同样 8 GB 内存,配 eMMC 的循环规模是 70 MB,折成 int4 权重约 1.4 亿参数;配 UFS 4.0 是 612 MB,约 12 亿参数。差 8.7 倍,内存完全一样。
这也解释了为什么很多标着 8 GB 内存的设备跑不动 15 亿参数的模型,而换一台同样 8 GB 但存储更快的机器就流畅。参数表上写的是容量,实际约束在带宽。
还有一层取舍。缓存开得越大,命中率越高,但留给应用的内存越少,系统会更容易杀后台。论文里提到,即使是 LRU 这种成熟策略,在缓存持续增大时也会出现收益停滞的区间,因为循环块本身比缓存大,怎么换都是全错过。这时候再加缓存没有意义,得改成分层加载,把不常用的层放到闪存上按需取。
一句话,端侧模型的容量上限写在存储带宽上,不写在内存条上。
内容来源:Mathematics(MDPI 开放获取,CC BY 4.0)
原文标题:Rethinking I/O Caching for Large Language Model Inference on Resource-Constrained Mobile Platforms
作者:Heejin Kim,Jeongha Lee,Hyokyung Bahn(韩国梨花女子大学)
DOI:10.3390/math13223689
配图说明:两张图均为自行绘制,为原理示意,非原文原图复制。
改动声明:本文为对开放获取文献的技术编译与解读,标题、行文与配图为原创。
来源:21ic电子网





