小芯片运行大模型的评估脚手架
小芯片运行大模型的评估脚手架
1. 演示现场突然崩溃:Killed 信号背后的内存踩爆
在嵌入式开发板 RK3588 上跑 4-bit 量化的 Qwen1.5-1.8B 时,很容易被单次对话的顺畅表现蒙蔽。单句问答延迟不到半秒,字符打字机效果也很丝滑。
然而把测试场景切到连续多轮对话或者传入 2K Token 以上的长文本时,程序打到一半瞬间崩溃:
root@rk3588-board:~# ./llama-cli -m qwen1.5-1.8b-chat-q4_k_m.gguf -n 512 -p "请详细阐述 Linux 内核虚拟文件系统..."
加载模型成功,分配内存 1.1GB...
生成中...
Killed
没有堆栈报错,也没有 C++ 异常信息。控制台只有一个冰冷的 Killed。
执行 dmesg -T | tail -n 25 查看内核日志,真相立刻浮出水面:
[Tue Aug 25 14:22:01 2026] Out of memory: Kill process 4102 (llama-cli) score 852 or sacrifice child
[Tue Aug 25 14:22:01 2026] Killed process 4102 (llama-cli) total-vm:2892100kB, anon-rss:1845120kB, file-rss:12400kB, shmem-rss:0kB
[Tue Aug 25 14:22:01 2026] oom_reaper: reaped process 4102 (llama-cli), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
嵌入式板卡总共只有 2GB 可用 SRAM/DRAM。模型权重文件静态占据了约 1.1GB,看似给系统留出了接近 900MB 的剩余空间。但实际上,由于 KV Cache(键值缓存)随着上下文长度呈线性爆发增长,叠加 C++ 运行时动态分配的临时 Tensor 空间,物理内存被瞬间拉爆,触发了内核的 OOM Killer。
单凭感觉评估边缘端 LLM 的能力不可靠。演示时的成功不代表生产环境的稳定,必须搭建一套能够实时抓取内存、吞吐与 Cache 命中的可复现评估脚手架。
2. 用 perf 与 smaps 分析 KV Cache 动态增长曲线
为了定量分析大模型在小芯片上的内存吃水线,不能只依赖 free -m 或 top 这类粗颗粒度的统计工具,因为它们的采样间隔通常在秒级,根本抓不到爆发性的内存分配峰值。
我们需要结合 Linux 内核的 /proc/[pid]/smaps 节点与 perf 事件。
编写诊断命令,监控进程在推理启动、上下文预热(Context Prefill)以及逐 Token 吐字(Decode)三个维度的物理内存(PSS/RSS)变化:
# 捕获 llama-cli 在推导过程中的内存细粒度分布
PID=$(pgrep llama-cli)
watch -n 0.1 "cat /proc/$PID/smaps 2>/dev/null | awk '/Rss:/{sum+=\$2} END {print \"Total RSS: \" sum \" KB\"}'"
同时,使用 perf 指令观察 TLB Miss 以及 Cache 缺失情况:
perf stat -e page-faults,L1-dcache-load-misses,L1-dcache-loads,tlb_flush \
./llama-cli -m qwen1.5-1.8b-chat-q4_k_m.gguf -c 2048 -n 128 -p "Benchmark test prompt..."
测试输出的指标暴露了极具价值的细节:
Performance counter stats for './llama-cli -m qwen1.5-1.8b-chat-q4_k_m.gguf ...':
24,102 page-faults # 12.301 K/sec
1,842,109,240 L1-dcache-load-misses # 28.45% of all L1-dcache accesses
6,475,201,980 L1-dcache-loads
412 tlb_flush
1.959281940 seconds time elapsed
在 Context Prefill 阶段,page-faults 频率极高,说明内存页在频繁进行物理分配与映射。当上下文窗口达到 1500 个 Token 时,KV Cache 占用的内存从初始的 40MB 飙升到了 420MB。这直接把留给系统其他进程的缓冲挤干了。
3. 边缘端 LLM 内存预配与 Token 采样推演图
问题的根本出在“未受限的上下文增长”与“未作预分配的内存池”。为了在边缘小芯片上让 LLM 稳定运行,脚手架必须具备两项硬性功能:
- 静态硬上限封顶:在模型初始化阶段就将 KV Cache 的最大空间一次性 Allocate 完成,禁止运行时动态再申请内存。
- 上下文滑窗丢弃:当 Token 长度达到预设闸门时,自动对历史对话进行 Slot 挤压或 Summarize,杜绝无限制膨胀。
4. 搭建自动化内存与 Token 吞吐压测脚手架
为了能够自动化量化不同量化参数(Q4_0, Q4_K_M, Q8_0)在芯片上的表现,我们构建了一个兼顾内存捕获与吞吐计算的 Python 评估脚手架。
该脚手架子进程启动推理引擎,使用 psutil 异步高频采样 RSS 内存峰值,同时计算 Prefill 速度(Tokens/s)与 Decode 速度(Tokens/s)。
import subprocess
import time
import psutil
import csv
import sys
def benchmark_edge_llm(cmd, timeout_sec=120):
"""
在边缘板卡上对 LLM 推理程序进行内存与 Token 吞吐的自动化压测脚手架
"""
print(f"[INFO] 开始压测指令: {' '.join(cmd)}")
start_time = time.time()
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
ps_proc = psutil.Process(proc.pid)
max_rss_bytes = 0
rss_samples = []
# 高频轮询采样物理内存 RSS
while proc.poll() is None:
try:
rss = ps_proc.memory_info().rss
if rss > max_rss_bytes:
max_rss_bytes = rss
rss_samples.append(rss)
except (psutil.NoSuchProcess, psutil.AccessDenied):
break
time.sleep(0.05) # 50ms 采样精度
stdout, stderr = proc.communicate()
elapsed_time = time.time() - start_time
if proc.returncode != 0:
print(f"[ALERT] 推理异常退出,Exit Code: {proc.returncode}")
print(f"[ALERT] Stderr 输出: {stderr}")
return None
# 解析 llama-cli 标准输出中的性能指标
eval_tokens = 0
tps = 0.0
for line in stderr.splitlines():
if "eval time =" in line:
# 解析形如: llama_print_timings: eval time = 4500 ms / 128 runs (35.15 ms per token, 28.44 tokens per second)
parts = line.split(",")
for p in parts:
if "tokens per second" in p:
tps = float(p.strip().split()[0])
max_rss_mb = max_rss_bytes / (1024 * 1024)
print(f"[RESULT] 内存峰值 RSS: {max_rss_mb:.2f} MB")
print(f"[RESULT] 解码吞吐速度: {tps:.2f} Tokens/s")
print(f"[RESULT] 总运行时间: {elapsed_time:.2f} s")
return {
"max_rss_mb": max_rss_mb,
"tps": tps,
"elapsed_sec": elapsed_time
}
if __name__ == "__main__":
test_cmd = [
"./llama-cli",
"-m", "qwen1.5-1.8b-chat-q4_k_m.gguf",
"-c", "1024",
"-n", "256",
"-p", "用 100 字介绍操作系统的分页机制。"
]
res = benchmark_edge_llm(test_cmd)
if res and res["max_rss_mb"] > 1600:
print("[WARN] 内存占用已接近边缘板卡警戒线,建议下调上下文长度 -c 参数!")
5. 避免基准测试虚高:上下文窗口的硬性隔离设置
拿到压测脚手架的数据后,还需要避免评估陷入“指标陷阱”。很多演示场景之所以看起来速度极快,是因为将上下文窗口(Context Window)设为了默认的 256 或 512。这在实际生产问答中根本不可用。
在编译 llama.cpp 或 NCNN 推理引擎时,必须显式配置编译参数与运行时锁死标志,封死动态内存追加的路径:
# 在 cmake 构建时针对低内存设备加入硬性隔离参数
cmake -B build \
-DLLAMA_FATAL_WARNINGS=ON \
-DGGML_DISABLE_DYNAMIC_ALLOC=ON \
-DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j4
运行时通过配置文件固定硬限制参数:
# edge_llm_config.ini
[memory]
# 锁死最大 KV Cache 为 1024 帧,防止长文本拖垮系统
max_context_tokens = 1024
# 启动即一次性预分配完整张量池,分配失败则立即报错退出,拒绝运行时动态扩容
preallocate_tensor_pool = true
# 强制开启内存锁定,防止系统 Swap 交换导致卡顿
mlock = true
把脚手架接入 Jenkins 或 GitHub Actions 的自托管 Runner 板卡上。每次提交新的模型量化权重文件或 C++ 引擎修改时,自动运行脚手架。如果 RSS 峰值突破了板卡物理内存的 80%,构建立即熔断。这样就能在开发阶段打绝 OOM 故障,避免把“跑不动”的盲目自信带到线上演示现场。
更多推荐




所有评论(0)