一航jason头像
关注

8295 端侧 llama-server 单用户闲聊服务:完整落地方案

方案总览

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 无意义,还引入调度延迟
--threads4= 大核数;绝不能 -t 8(A510 小核拖慢同步)
--threads-batch4prefill 同样只用大核
--ctx-size1024闲聊够用;上下文每多 1K,prefill 变慢、KV 翻倍
--flash-attn (-fa)onKV 量化的前置条件,本身也省计算
--cache-type-k/vq8_01K 上下文 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
前提两条:

  1. -np 1 保证永远命中同一个 slot;
  2. 客户端拼接历史时不要改动 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日志 timingstock/s
首字延迟(冷启动首轮)<1s预热后首条真实请求
首字延迟(多轮)<500ms第 3 轮起观察
投机接受率≥0.55日志 accept rate,<0.5 调低 draft-max
内存驻留≤1.7GBdumpsys meminfo / cgroup
启动到可服务<5shealth 检查通过时间
调参口诀:先跑 llama-bench 确认硬件速度达标 → 再看投机接受率调 --draft-max → 最后测多轮首字确认前缀缓存命中(日志里 prompt cache 相关行)。
需要的话,我可以把客户端串行调用 + 历史裁剪 + 超时降级的参考实现(Java/Kotlin 或 C++)也写出来。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/mhhyoucom/article/details/164585943

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--