方案总览
Qwen2.5-1.5B-Instruct (Q4_K_M) 主模型
Qwen2.5-0.5B-Instruct (Q4_K_M) draft 模型(投机解码)
↓
llama-server 常驻进程(单 slot、单请求、绑大核、流式 SSE)
↓
车机业务通过 HTTP API 调用(127.0.0.1,仅本机)
Step 1:模型准备(x86 机器上下载转换)
# 1. 下载 GGUF(HuggingFace 或 ModelScope 镜像)
huggingface-cli download Qwen/Qwen2.5-1.5B-Instruct-GGUF \
qwen2.5-1.5b-instruct-q4_k_m.gguf --local-dir ./models/
huggingface-cli download Qwen/Qwen2.5-0.5B-Instruct-GGUF \
qwen2.5-0.5b-instruct-q4_k_m.gguf --local-dir ./models/
# 2. 校验后推送到车机
adb push models/*.gguf /data/ai/models/
如果车机上网络受限,也可以在 x86 上自己转换:
python convert_hf_to_gguf.py Qwen/Qwen2.5-1.5B-Instruct --outtype q8_0
llama-quantize qwen2.5-1.5b-q8_0.gguf qwen2.5-1.5b-instruct-q4_k_m.gguf Q4_K_M
Step 2:编译 llama.cpp(在 8295 车机上原生编译,推荐)
原生编译能开启
GGML_NATIVE=ON自动探测 i8mm/dotprod,比交叉编译调参省事且不会踩指令集坑。车机上装 NDK/clang 太重的话,用同名 Android 架构的开发板(如 RK3588/骁龙 qemu 不行,必须真 ARM 环境)编译后再拷贝也可。
2.1 车机环境准备
# 确认 CPU 特性(重点看 i8mm、dotprod 是否存在)
adb shell cat /proc/cpuinfo | grep -i "Features"
# 期望看到: ... aes sha2 i8mm dotprod asimdfp ...
# 安装工具链(车机若有 apt/yum;没有则用 Android NDK 交叉编译,见 2.3)
apt install -y git cmake clang libopenblas-dev
2.2 编译
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
# 拉一个稳定 release(避免 main 分支行为变动)
git checkout b4589 # 示例,选当时的最新 release tag
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DGGML_NATIVE=ON \ # 关键:自动启用 NEON + i8mm + dotprod
-DGGML_OPENMP=ON \
-DGGML_LLAMAFILE=ON \ # sgemm 内核,ARM GEMM 提速
-DLLAMA_CURL=OFF \ # 车机不需要网络下载模型
-DGGML_RPC=OFF \
-DBUILD_SHARED_LIBS=OFF # 静态链接,部署只拷一个二进制
cmake --build build --config Release -j8
# 验证编译是否启用了 i8mm/dotprod:看 configure 输出
# 应包含: NEON = 1 | DOTPROD = 1 | MATMUL_INT8 = 1
# 如果 MATMUL_INT8 = 0,说明工具链/flag 有问题,量化 kernel 会走慢速路径
编译自检(必做):
./build/bin/llama-bench -m /data/ai/models/qwen2.5-1.5b-instruct-q4_k_m.gguf \
-p 0 -n 64 -t 1,2,3,4 -r 3
# 输出对比 t=1/2/4 的 tg128(生成速度),取最高的线程数
# 预期: t=4 在 8295 上 12~18 tok/s,若只有 5~8,回头检查 MATMUL_INT8
2.3 交叉编译备选(车机无法装工具链时)
# Ubuntu x86 上用 Android NDK
export ANDROID_NDK=/opt/android-ndk-r26d
cmake -B build-android \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-33 \
-DGGML_NATIVE=OFF \ # 交叉编译时关闭,手动指定
-DCMAKE_C_FLAGS="-march=armv8.2-a+dotprod+i8mm" \
-DCMAKE_CXX_FLAGS="-march=armv8.2-a+dotprod+i8mm" \
-DGGML_LLAMAFILE=ON -DLLAMA_CURL=OFF -DBUILD_SHARED_LIBS=OFF
cmake --build build-android -j16
# 拷贝到车机后务必用 llama-bench 复测速度(交叉编译参数易错)
Step 3:单用户专用优化——这是本方案的差异点
端侧单请求、无并发,那么 llama-server 的多 slot / continuous batching / 并行调度全是无用开销甚至负优化,要显式关干净:
| 配置项 | 值 | 原因 |
|---|---|---|
--parallel (-np) | 1 | 只留 1 个 slot,省掉多余 slot 的 KV cache 显存预留(每 slot 都按 -c 预留 KV 内存,np=4 会白占 4 份) |
--cont-batching | 关闭 | 单请求下 continuous batching 无意义,还引入调度延迟 |
--threads | 4 | = 大核数;绝不能 -t 8(A510 小核拖慢同步) |
--threads-batch | 4 | prefill 同样只用大核 |
--ctx-size | 1024 | 闲聊够用;上下文每多 1K,prefill 变慢、KV 翻倍 |
--flash-attn (-fa) | on | KV 量化的前置条件,本身也省计算 |
--cache-type-k/v | q8_0 | 1K 上下文 KV 从 ~95MB 压到 ~48MB |
--mlock | 开 | 权重锁页,解码中途无 page fault 尖刺 |
--no-mmap | 关闭(即用 mmap) | 配合预热,启动快且内存按需换入 |
--metrics | 关 | 单用户不需要,省一点原子操作开销 |
--slots/monitoring endpoint | 关 | 同上 |
启动脚本(车机上直接用)
#!/system/bin/sh
# /data/ai/start_llm_server.sh
CORES_BIG="0-3" # 8295 大核,按实际 cpuinfo 确认 A715 所在核号
taskset -c $CORES_BIG /data/ai/bin/llama-server \
-m /data/ai/models/qwen2.5-1.5b-instruct-q4_k_m.gguf \
-md /data/ai/models/qwen2.5-0.5b-instruct-q4_k_m.gguf \
--host 127.0.0.1 \
--port 8899 \
--parallel 1 \
--no-cont-batching \
--threads 4 \
--threads-batch 4 \
--ctx-size 1024 \
--flash-attn on \
--cache-type-k q8_0 \
--cache-type-v q8_0 \
--mlock \
--temp 0.7 --top-p 0.8 --repeat-penalty 1.05 \
-n 256 \
--draft-max 4 --draft-min 2 --draft-p-min 0.6 \
-ngld 99 \
> /data/ai/logs/llm_server.log 2>&1 &
投机解码参数(
-md / -ngld / --draft-*)llama-server 原生支持,与 CLI 相同。首次启动后看日志里的accept rate:连续几个请求低于 0.5,就把--draft-max降到 3 或 2;闲聊高频句式一般能到 0.6~0.7。
单请求下多轮对话的 KV 复用(重要)
llama-server 的 slot 机制在单 slot 下有个免费优化:客户端每轮把完整对话历史发上来,server 会自动检测公共前缀、复用上一轮的 KV cache,只对增量 token 做 prefill。
前提两条:
-np 1保证永远命中同一个 slot;- 客户端拼接历史时不要改动 system prompt 和早期消息(哪怕改一个字,前缀缓存全部失效)。
这样第 2 轮起 prefill 从几百 token 降到几十 token,多轮首字延迟稳定在 400ms 以内。
Step 4:冷启动预热(首条请求体验的关键)
llama-server 自带 --warmup(默认开,仅做一次小的前向),但 mmap 模式下页面仍可能未完全驻留。车机开机后加一个预热脚本:
#!/system/bin/sh
# /data/ai/warmup.sh —— 开机后后台执行
# 等服务起来
until curl -s http://127.0.0.1:8899/health | grep -q ok; do sleep 1; done
# 发一次 1 token 的完整链路请求,把权重页换入 page cache、
# 让投机解码 draft 模型也完成首次加载
curl -s http://127.0.0.1:8899/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hi"}],"max_tokens":1}' > /dev/null
echo "$(date) warmup done" >> /data/ai/logs/warmup.log
效果:用户第一句真实对话的首字延迟从 3~5s 降到 500ms 左右。
内存注意:
--mlock+ 预热后,1.5B + 0.5B + KV 总驻留约 1.5~1.7GB。确认车机给该进程的 cgroup/VM 内存限额 ≥ 2GB,否则去掉-md(放弃投机解码)或--mlock二选一。
Step 5:客户端接口调用(单请求约定)
业务侧遵守三条纪律:
# 1. 串行调用:上一条响应(含 stop)返回前,不发下一条
# (服务端单 slot,并发请求会排队并互相顶掉 KV 缓存)
# 2. 流式 SSE 必开——体感速度的关键
curl -N http://127.0.0.1:8899/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "system", "content": "你是车载助手,回答简短口语化。"},
{"role": "user", "content": "今晚吃什么好"}
],
"max_tokens": 128,
"stream": true
}'
# 3. max_tokens 按场景给小值:闲聊 128、导航改写 64,防跑题长输出
推荐的车载客户端策略:
- 超时:连接 2s / 首字 3s / 总时长 30s,超时降级到云端或固定话术;
- 断流处理:SSE 断开后立即调
DELETE /slots/0(或重启进程)清空状态,避免脏 KV 影响下一轮; - 历史裁剪:客户端维护对话历史,超过 ~700 token 就把最早几轮摘要成一条 user 消息,保证单请求总长 < 1024;
- 意图分流(配合 NPU 侧 BERT):车控/导航类指令不进 LLM,只有闲聊和开放问题才调本接口——这是整体响应体验最大的一项优化。
Step 6:常驻进程管理
# 用车机 init 脚本或 systemd(若为 Linux 车机)托管
# 关键点:
# - Restart=on-failure,崩溃自动拉起(llama.cpp 长跑极稳,但车机环境要兜底)
# - MemoryMax=2G(cgroup 限额,防止 OOM 拖垮座舱)
# - CPUAffinity=0 1 2 3(与 taskset 双保险)
# - Nice=-10 / SCHED_OTHER nice 值:比后台进程高,低于车控进程
# systemd 示例
[Service]
ExecStart=/data/ai/start_llm_server.sh
ExecStartPost=/data/ai/warmup.sh
MemoryMax=2G
CPUAffinity=0 1 2 3
Nice=-10
Restart=on-failure
RestartSec=2
验收基准(8295 标准版预期)
| 指标 | 目标值 | 测法 |
|---|---|---|
| 解码速度(含投机解码) | 15~25 tok/s | 日志 timings 的 tock/s |
| 首字延迟(冷启动首轮) | <1s | 预热后首条真实请求 |
| 首字延迟(多轮) | <500ms | 第 3 轮起观察 |
| 投机接受率 | ≥0.55 | 日志 accept rate,<0.5 调低 draft-max |
| 内存驻留 | ≤1.7GB | dumpsys meminfo / cgroup |
| 启动到可服务 | <5s | health 检查通过时间 |
调参口诀:先跑 llama-bench 确认硬件速度达标 → 再看投机接受率调 --draft-max → 最后测多轮首字确认前缀缓存命中(日志里 prompt cache 相关行)。 | ||
| 需要的话,我可以把客户端串行调用 + 历史裁剪 + 超时降级的参考实现(Java/Kotlin 或 C++)也写出来。 |
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/mhhyoucom/article/details/164585943



