图 1:本文以 RK3588 上的文本推理为目标。板卡为概念示意,CPU 与三核 NPU 均属于 RK3588 SoC。
写在前面:跑起来只是起点,还要知道它跑在哪里
一块 8GB 的 RK3588 开发板,可以把 Qwen3.5-2B 做成局域网里的模型服务:浏览器直接聊天,程序通过 /v1/chat/completions 调用,部分计算交给板载 NPU。真正需要解决的是一整条链路:设备权限是否正确、程序是否编译了 NPU 后端、GGUF 量化是否匹配硬件管线、第一次推理是否稳定,以及停止服务后内存和端口是否确实释放。
本文完成的部署路径是:
Qwen3.5-2B GGUF
↓
rk-llama.cpp 的 RKNPU2 backend
↓
RK3588 NPU Core0 / Core1 / Core2,与 CPU 协同
↓
llama-server :8080
↓
Web UI / OpenAI Compatible API
开始前先明确边界:8GB RAM 需要同时容纳系统、权重和推理缓存;GGUF 的量化类型不等于 NPU 实际执行精度;NPU 加速也不代表所有算子都离开 CPU。NPU 利用率不是加速比例,Context 越大,缓存和缓冲区压力通常越高。本文服务只接受文本,普通图片不能直接输入。多个独立 NPU 模型进程同时计算存在驱动层风险,上游也仍在持续迭代。
能力澄清: Qwen 官方将 Qwen3.5-2B 定义为包含视觉编码器的模型。本文只加载实测 GGUF 文本路径,没有配置视觉编码组件,所以这里的“文本模型服务”不等于“Qwen3.5-2B 原模型没有视觉能力”。Qwen 官方模型卡
本文区分五类信息:实测记录指下表对应环境的已有部署结果;项目说明来自链接所指版本;原理解释用于理解机制;建议配置需要在自己的板卡验证;版本差异单独列出。下文没有新增板端跑分,也没有把语法检查当成 NPU 实测。
1. 先固定环境和版本
本文实测环境
| 项目 | 本文实测基线 |
|---|---|
| SoC / 内存 | RK3588 / 8GB |
| 系统 | Ubuntu 22.04.5 LTS,厂商镜像 |
| 架构 / 内核 | aarch64 / Linux 5.10 |
| NPU 软件路径 | RKNPU2 backend + RKNN Runtime |
| NPU 驱动 | RKNPU driver v0.9.8 |
| Runtime 文件检查 | /usr/lib/librknnrt.so;未记录独立版本号 |
| 源码 | invisiofficial/rk-llama.cpp 的 rknpu2 分支 |
| 实测提交 | 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62 |
| 模型 / 格式 | Qwen3.5-2B / GGUF |
| 本文默认量化 | Q8_0 |
| 已有数值跑分 | Q6_K,见性能测试一节 |
| 服务 / 端口 | llama-server / TCP 8080 |
| 部署目录 | $HOME/rk_llama_npu_test;用户名为 edge 时是 /home/edge/rk_llama_npu_test |
不同厂商镜像的内核、RKNPU 驱动和 RKNN Runtime 可能不同,不能仅凭“都是 RK3588”就认为软件环境一致。本文以镜像已包含匹配的驱动及运行库为前提;组件缺失时,应先按开发板厂商的镜像或 SDK 文档处理。
2. 为什么选择 rk-llama.cpp
llama.cpp、ggml 和 backend 分别是什么
llama.cpp 是使用 C/C++ 实现的模型推理项目,提供模型加载、分词、推理、采样和服务工具。ggml 是其底层张量计算库;backend,也就是“计算后端”,把一部分张量操作交给特定硬件执行。GGUF 则是存放权重及模型元数据的文件格式,量化后的权重也可以放在其中。llama.cpp 项目
普通 ARM 可执行程序不会因为设备上有 NPU,就自动调用它。NPU 需要自己的运行接口、数据布局和受支持运算路径。rk-llama.cpp 是 llama.cpp 的社区分支,在 ggml 层接入 RKNPU2 backend,让它可以通过 RKNN Runtime 与 Rockchip NPU 交互;它不是 Rockchip 官方的 RKLLM 产品。
选择这条路线的主要价值,是继续使用 GGUF、llama-cli、llama-bench、llama-server 及其 API 生态,同时使用 NPU 支持的计算能力。GGUF 文件可以继续统一分发和校验,但不意味着 NPU 会直接按文件中的块量化格式计算。实测版本后端说明
和 RKLLM 路线有什么区别
| 路线 | 常用模型格式 | RK3588 NPU | 模型及工具通用性 | llama.cpp 生态 | 部署工作 |
|---|---|---|---|---|---|
| 普通 llama.cpp | GGUF | 普通 CPU 构建不会自动调用 RKNPU2;其他加速后端需分别配置 | 较广,仍受具体模型支持限制 | 原生 | 相对较少 |
| rk-llama.cpp RKNPU2 | GGUF | 通过专用 backend 使用 | 受算子、量化管线和驱动组合约束 | 高度沿用 | 需要编译、权限和后端验证 |
| RKLLM | 转换后的 RKLLM 模型 | 通过 RKLLM Runtime 使用 | 受官方转换器及支持列表约束 | 接口和工具链不同 | 需要模型转换与 Runtime 配套 |
这里的部署工作量是工程判断,不是统一评分。RKLLM 的核心流程是使用 RKLLM-Toolkit 转换模型,再用板端 Runtime 推理;它更适合希望围绕 Rockchip 提供的模型工具链开展部署的项目。Rockchip RKLLM 项目
三条路线没有脱离条件的“绝对更快”。有意义的对比至少要固定板卡、模型、量化或质量目标、上下文、输入长度、生成长度和频率状态。
3. 软件栈与 CPU、GPU、NPU 的分工
图 2:CLI、Bench 和 Server 使用同一套模型与计算后端;RKNPU2 经 Runtime、驱动到达硬件,CPU 仍参与计算。
Runtime 是用户空间的运行库,Driver 是内核中的设备驱动,两者不应混称为“装了一个 NPU 库”。librknnrt.so 存在、设备节点存在、进程有权限,是不同层的条件。Rockchip RKNN-Toolkit2 与 Runtime 项目
RK3588 的 CPU 包含四颗 Cortex-A76 和四颗 Cortex-A55,另有 GPU 和 NPU。它们共享系统内存资源,但不会自动组成一套“所有加速器一起跑”的推理方案。Rockchip RK3588 介绍
图 3:CPU 负责调度、分词、部分算子、采样和服务;NPU 承担支持的矩阵计算;RAM 容纳权重、缓存、状态和工作缓冲区。
Tokenizer 把文本转换成 Token,即模型处理的基本单元,Token 不等于一个汉字。Sampling 根据模型输出分数选择下一个 Token。CPU 因而不会在 NPU 推理时闲置。本文实测镜像中 CPU4~7 对应 A76 大核,后文把 CPU 工作绑定到这一范围;换镜像后要核对编号,不能把操作系统 CPU 编号当成 SoC 固有接口。Arm Cortex-A76 资料
GPU 通常负责图形显示,也可被其他推理后端用于计算;本文没有启用 GPU 推理路径。NPU 三个 Core 是同一 NPU 内的计算核心,不是三张独立加速卡。RKNPU2 只接管满足后端条件的运算,余下工作仍由 CPU 等既定路径处理。
4. 准备板卡,并按可定位故障的顺序部署
准备 RK3588 8GB 开发板、稳定电源和散热,建议预留至少 15GB 磁盘空间。需要一台能 SSH 登录的电脑,负责下载、压缩和上传。
图 4:先用 CLI 排除模型和后端问题,再用 Bench 验证计算,最后增加 HTTP、网络和浏览器这一层。
除明确标注 “电脑 PowerShell” 的代码外,下面命令均在开发板的 Bash 中运行。示例用户名采用 edge。电脑端先记录连接信息;IP 和用户名需按实际设备输入,避免把尖括号占位符误当重定向符号:
$BoardIP = Read-Host '输入开发板局域网 IP'
$BoardUser = Read-Host '输入 SSH 用户名(例如 edge)'
$BoardPort = 22
$BoardTarget = "${BoardUser}@${BoardIP}"
$BoardBase = "/home/$BoardUser/rk_llama_npu_test"
ssh -p $BoardPort $BoardTarget
电脑上传示例按 Ubuntu 普通用户家目录 /home/用户名 编写;若 echo "$HOME" 显示不同目录,应同步修改 $BoardBase。不要把密码写进脚本。Windows 端需要 OpenSSH、curl.exe、tar,源码下载还需 Git;缺少 Git 时使用 Git 官方下载页。
5. 检查系统、NPU、Runtime 和用户权限
系统与资源
cat /etc/os-release
uname -a
uname -m
free -h
df -h /
核对 aarch64,以及 free -h 中的 available。不要仅看 free 列:Linux 会用空闲内存作文件缓存。RAM 紧张时,模型能加载并不保证长提示词或首次推理也能成功。
设备、驱动和运行库
ls -l /dev/dri/by-path/platform-fdab0000.npu-render
sudo cat /sys/kernel/debug/rknpu/version
ls -l /usr/lib/librknnrt.so
本文实测设备链接及驱动输出为:
platform-fdab0000.npu-render -> ../renderD129
RKNPU driver: v0.9.8
renderD129 是本文镜像的实际编号,其他镜像应以 by-path 指向和设备归属为准。若 debugfs 路径不存在,可先检查是否挂载:
findmnt -t debugfs
仅在确认系统支持 debugfs、但尚未挂载时执行:
sudo mount -t debugfs debugfs /sys/kernel/debug
挂载不会凭空安装驱动。设备节点缺失、驱动未加载、Runtime 缺失,需要回到厂商镜像或 SDK 处理,不能靠反复启动模型修复。
为什么优先检查 render 组
id
ls -l /dev/dri/renderD129
实测节点类似:
crw-rw---- 1 root render ... /dev/dri/renderD129
root:render 表示所有者为 root、所属组为 render;0660 表示所有者和所属组有读写权限,其他用户没有。这个权限模型下,普通用户应加入 render,仅加入 video 不足以保证可访问此节点。不同镜像也可能使用 ACL 或不同组名,实际权限以节点为准。Linux DRM render node 文档
sudo usermod -aG render "$USER"
注销 SSH 并重新登录,再运行 id。旧会话不会自动获得新的附加组。后续以普通用户运行模型,仅对驱动状态、系统配置等操作使用 sudo。
ldconfig 不能单独证明 NPU 是否可用
ldconfig -p | grep rknn
这个命令查询的是动态链接器缓存。没有结果不等于 librknnrt.so 不存在,更不等于 NPU 损坏;有结果也不等于后端已加载。判断必须联合使用:实际库文件 → 设备及权限 → --list-devices → 真实推理。
还有一个版本细节:实测提交的后端 CMake 链接了源码目录内 ggml/src/ggml-rknpu2/libs/librknnrt.so。检查系统库是环境基线检查,进程最终加载哪份库还应看 ldd 或 /proc/PID/maps,不要把系统文件存在误写成“运行时必定用了它”。该提交的后端构建文件
6. 安装依赖与建立部署目录
sudo apt update
sudo apt install -y \
build-essential cmake git curl wget ca-certificates \
pkg-config libssl-dev python3 python3-pip \
tar unzip zip rsync file util-linux iproute2 procps nano
for c in git cmake make gcc g++ python3 curl wget tar file taskset flock ss; do
printf '%-10s ' "$c"
command -v "$c" || echo '未安装'
done
BASE="$HOME/rk_llama_npu_test"
mkdir -p "$BASE/models"
cd "$BASE"
build-essential 提供编译工具,util-linux 提供 taskset、flock 等工具,iproute2 提供 ss,procps 提供进程检查工具。原实测镜像已经具备部分依赖;这里补列 libssl-dev、iproute2、procps、nano,使新镜像的依赖更明确。启用 HTTPS 的构建需要相应 OpenSSL 开发文件,不应遇错就随意关闭功能。
部署目录最终为:
rk_llama_npu_test/
├── rk-llama.cpp/
│ └── build/bin/
├── models/
│ └── Qwen3.5-2B.q8_0.gguf
├── start.sh
├── stop.sh
├── qwen35_server_8080.log
└── qwen35_server_8080.pid
7. 获取 rknpu2 分支:板端下载或电脑中转
方式 A:开发板直接 clone
下面用于新部署目录。先下载分支,再额外取回实测提交并切换到它:
BASE="$HOME/rk_llama_npu_test"
cd "$BASE"
git clone -b rknpu2 --depth 1 \
https://github.com/invisiofficial/rk-llama.cpp \
rk-llama.cpp
cd rk-llama.cpp
git fetch --depth 1 origin 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62
git checkout --detach 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62
git rev-parse HEAD
git log -1 --oneline
固定提交后处于 detached HEAD,git branch --show-current 为空属于正常现象。原实测教程直接浅克隆分支;本文为防止分支更新影响复现,明确增加固定提交步骤。此时判据是完整 SHA 与上文一致,不是要求分支名命令继续输出 rknpu2。
如果服务器拒绝对指定提交做浅 fetch,可获取完整分支历史再切换:
git fetch --unshallow origin rknpu2
git checkout --detach 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62
后一组仅用于仍为浅仓库且前一条 fetch 失败的情况。已有源码目录应先检查修改状态,不要对仍在使用的目录重复 clone 或强制覆盖。
方式 B:电脑下载、压缩,再 SCP 上传
开发板访问 GitHub 不稳定时,在电脑 PowerShell 的一个新源码下载目录执行。下列连接变量沿用第 4 节:
git clone -b rknpu2 --depth 1 https://github.com/invisiofficial/rk-llama.cpp rk-llama.cpp
git -C rk-llama.cpp fetch --depth 1 origin 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62
git -C rk-llama.cpp checkout --detach 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62
git -C rk-llama.cpp rev-parse HEAD
tar -czf rk-llama.cpp-rknpu2.tar.gz rk-llama.cpp
ssh -p $BoardPort $BoardTarget "mkdir -p '$BoardBase/models'"
scp -P $BoardPort .\rk-llama.cpp-rknpu2.tar.gz "${BoardTarget}:${BoardBase}/"
每条完成并确认无错误后再执行下一条;不要把下载失败后的残缺目录继续打包。上传的是源码及 Git 版本信息,不是在 Windows 编译好的二进制。开发板执行:
cd "$HOME/rk_llama_npu_test"
tar -xzf rk-llama.cpp-rknpu2.tar.gz
cd rk-llama.cpp
git rev-parse HEAD
git log -1 --oneline
8. 编译并证明 RKNPU backend 已注册
BASE="$HOME/rk_llama_npu_test"
REPO="$BASE/rk-llama.cpp"
cd "$REPO"
cmake -S . -B build \
-DLLAMA_RKNPU2=ON \
-DCMAKE_BUILD_TYPE=Release
cmake --build build -j4 \
--target llama-cli llama-bench llama-server
ls -lh \
build/bin/llama-cli \
build/bin/llama-bench \
build/bin/llama-server \
build/bin/libggml-rknpu2.so
保留 -DLLAMA_RKNPU2=ON,它在实测提交中启用 GGML_RKNPU2。Release 开启发布优化;-j4 控制并行编译任务数量,不是推理线程或 NPU 核心数。
| 产物 | 用途 |
|---|---|
llama-cli | 隔离网页、HTTP 和浏览器,先完成模型推理 |
llama-bench | 检查后端及输入处理、生成性能 |
llama-server | 加载模型并提供 Web UI、HTTP API |
libggml-rknpu2.so | RKNPU2 计算后端动态库 |
现在检查设备注册:
export LD_LIBRARY_PATH="$REPO/build/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
"$REPO/build/bin/llama-cli" --list-devices
关键输出必须包含:
RKNPU: Rockchip NPU
这只是第一层证据,说明程序能够发现该后端,尚不能证明当前模型的请求已经使用 NPU。只有程序启动成功、出现欢迎信息,都不足以代替它。
如果没有 RKNPU,按顺序检查:
grep -E 'LLAMA_RKNPU2|GGML_RKNPU2' "$REPO/build/CMakeCache.txt"
ls -l "$REPO/build/bin/libggml-rknpu2.so"
ldd "$REPO/build/bin/libggml-rknpu2.so"
ls -l /usr/lib/librknnrt.so
ls -l /dev/dri/by-path/platform-fdab0000.npu-render
id
ldd 中有 not found 就先修复库依赖;更换过源码提交时,用新的构建目录重新编译,避免把旧对象文件和新代码混用。
9. GGUF 怎么选:磁盘量化与 NPU 管线要分开看
FP16 用半精度浮点保存权重。Q8_0、Q4_0 是分块量化类型;Q6_K、Q4_K_M 属于 K 量化体系,后缀不只是“几个比特”的简单替换。模型还包含缩放信息、元数据及不同精度的张量,所以文件大小不会严格等于参数量乘以名义位宽。
图 5:GGUF 是输入权重格式;后端在 CPU 侧进行转换、校准及重新量化,再交给选定 NPU 管线执行。文件位宽不等于所有运算的执行位宽。
以下大小是公开模型文件的十进制 GB,不是进程运行内存。默认映射以实测提交为依据,核验日的上游 README 仍列出相同映射。
| 文件量化 | 文件大小约值 | RK3588 默认硬件管线 | 质量、内存与速度的取舍 |
|---|---|---|---|
| FP16 | 3.78GB | W16A16_STANDARD | 权重保留精度较高;默认管线和文件均偏大,不作为 8GB 默认配置;没有本文该模型的速度实测 |
| Q8_0 | 2.01GB | W8A8_STANDARD | 本文默认选择,作为质量和资源占用的起点;速度仍需 Bench 验证 |
| Q6_K | 1.56GB | W8A8_STANDARD 与 W4A4_HADAMARD 按层循环 | 文件更小,但执行是 INT8/INT4 混合;本文有已有跑分 |
| Q4_0 | 1.20GB | W4A4_HADAMARD | 磁盘占用较小,量化质量风险更高;并不保证比 Q8_0 更快 |
| Q4_K_M | 1.27GB | 实测及核验版默认映射表未单列 | 普通 llama.cpp 可用不代表此后端最佳;需检查实际回退或管线行为,不用于本文默认复现 |
文件来自社区转换仓库 AaryanK/Qwen3.5-2B-GGUF,不是 Qwen 官方直接发布的同名 GGUF。量化管线见 实测版本 RKNPU2 README 和 当前分支说明。
W8A8 表示权重和激活采用 8 位整数量化管线;W4A4 对应 4 位整数;这里 W16A16_STANDARD 对应 FP16 运算。Q6_K 文件不表示 NPU 在执行原生 INT6 乘法。HADAMARD 管线包含用于缓解离群值影响的变换,它还可能引入 CPU 处理开销。
因此,普通 llama.cpp 能加载一种 GGUF,不等于 RKNPU2 会按最佳 NPU 路径运行它。先用 Q8_0 得到稳定基线,再比较 Q6_K、Q4_0 的答案质量、内存和 pp/tg。提高执行位宽无法恢复已经在低位 GGUF 中丢失的信息;不要为了“精度高”把低位文件任意指定到更高精度管线。
10. 下载大文件,并验证 SHA256
固定文件版本
本文下载使用核验时模型仓库的提交 3b3b8686d74f2ccfff1bcf36f80e114aff1b7881,避免 main 后续替换文件后沿用旧校验值。Q8_0 的公开元数据与实测记录一致:
文件名:Qwen3.5-2B.q8_0.gguf
大小:2012012480 bytes
SHA256:44bead7f88cb7f904799449fecae257f299f68e948a27858402062ecd9f7be88
固定版本的 Q8_0 文件信息 中的 SHA256 是完整文件散列;不要把页面上的 Xet hash 当作 SHA256。
推荐:电脑下载后上传
电脑 PowerShell 执行下载。-L 跟随跳转,--fail 遇到 HTTP 错误返回失败,--retry 处理短暂网络故障,-C - 从已有文件位置续传:
curl.exe -L --fail --retry 20 --retry-delay 3 -C - -o "Qwen3.5-2B.q8_0.gguf" "https://huggingface.co/AaryanK/Qwen3.5-2B-GGUF/resolve/3b3b8686d74f2ccfff1bcf36f80e114aff1b7881/Qwen3.5-2B.q8_0.gguf?download=true"
certutil -hashfile .\Qwen3.5-2B.q8_0.gguf SHA256
这两条单行命令也适用于 CMD。以下上传命令使用第 4 节的 PowerShell 变量:
ssh -p $BoardPort $BoardTarget "mkdir -p '$BoardBase/models'"
scp -P $BoardPort .\Qwen3.5-2B.q8_0.gguf "${BoardTarget}:${BoardBase}/models/"
上传后在开发板运行:
cd "$HOME/rk_llama_npu_test/models"
stat -c '%n %s bytes' Qwen3.5-2B.q8_0.gguf
sha256sum Qwen3.5-2B.q8_0.gguf
printf '%s %s\n' \
'44bead7f88cb7f904799449fecae257f299f68e948a27858402062ecd9f7be88' \
'Qwen3.5-2B.q8_0.gguf' | sha256sum -c -
应输出 Qwen3.5-2B.q8_0.gguf: OK。2GB 左右的大文件可能经历中断、续传和多次拷贝;同时比较发布者散列、电脑散列和板端散列,可以区分下载损坏与传输损坏。校验失败就停止推理排障,重新获取正确文件;不能仅凭 ls -lh 看起来“差不多大”继续。
可选:开发板直接下载
MODEL_DIR="$HOME/rk_llama_npu_test/models"
mkdir -p "$MODEL_DIR"
wget -c \
-O "$MODEL_DIR/Qwen3.5-2B.q8_0.gguf" \
'https://huggingface.co/AaryanK/Qwen3.5-2B-GGUF/resolve/3b3b8686d74f2ccfff1bcf36f80e114aff1b7881/Qwen3.5-2B.q8_0.gguf?download=true'
cd "$MODEL_DIR"
printf '%s %s\n' \
'44bead7f88cb7f904799449fecae257f299f68e948a27858402062ecd9f7be88' \
'Qwen3.5-2B.q8_0.gguf' | sha256sum -c -
若换其他提交或转换者,应重新取得对应文件的 SHA256,不能强行套用上述散列。其他量化文件可从同一固定版本文件列表取得,本文不要求全部下载。
11. 首次 CLI 推理:把错误限制在最少的层里
先退出其他模型实例,再运行:
BASE="$HOME/rk_llama_npu_test"
BIN="$BASE/rk-llama.cpp/build/bin"
MODEL="$BASE/models/Qwen3.5-2B.q8_0.gguf"
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export RKNPU_CORES="0,1,2"
unset RKNPU_HYBRID
ulimit -n 65536
taskset -c 4-7 "$BIN/llama-cli" \
-m "$MODEL" \
-t 4 \
-c 512 \
-n 64 \
--single-turn \
--reasoning off \
--reasoning-budget 0 \
-p '1+1=?' \
--no-display-prompt
这条命令保留实测参数:三核 NPU、四个 CPU 线程、512 Token 小上下文,以及单轮短输出。问题使用 ASCII,便于先排除终端中文编码干扰。它应正常完成并输出包含答案 2 的内容,再继续 Bench;这只是通路检查,不是模型能力评测。
| 设置 | 为什么必须关注 |
|---|---|
RKNPU_CORES=0,1,2 | 显式指定三个 NPU Core;它本身不是实际执行证明 |
taskset -c 4-7 | 在本文镜像上把进程与后续线程限制到 A76 大核范围 |
ulimit -n 65536 | 提高当前 Shell 及其子进程的文件描述符上限,避免运行时句柄耗尽 |
unset RKNPU_HYBRID | 清除先前试验留下的手动管线配置,恢复自动选择 |
--reasoning off 与 --reasoning-budget 0 | 沿用本文首次验证的非思考配置,控制测试长度 |
文件描述符不限于普通文件,也可能承载设备和缓冲区相关句柄。实测曾出现上限过低导致 Too many open files,伴随 Processing 0%、NPU 无负载或首次推理异常。因此 ulimit 不是无关紧要的调优项。后端快速入门
若设置失败,先看:
ulimit -Sn
ulimit -Hn
普通用户不能把软限制提高到硬限制以上。此时应由管理员调整登录会话限制,再重新登录;不要在失败后继续运行模型,也不要以为另一个 SSH 窗口里执行过 ulimit 就会改变已启动的服务。
不要照搬 --device RKNPU -ngl 99。 本文实测命令依赖该分支的自动调度,没有额外强制 offload 参数。CUDA 教程中的层数参数不能机械迁移到另一种后端;后面 Bench 表里的 ngl=99 是其输出字段,不代表你需要向 CLI 或 Server 补上这组参数。
12. Bench:区分“读输入的速度”和“吐字的速度”
CLI 成功并退出后,运行同一模型的基准测试:
BASE="$HOME/rk_llama_npu_test"
BIN="$BASE/rk-llama.cpp/build/bin"
MODEL="$BASE/models/Qwen3.5-2B.q8_0.gguf"
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export RKNPU_CORES="0,1,2"
unset RKNPU_HYBRID
ulimit -n 65536
set -o pipefail
taskset -c 4-7 "$BIN/llama-bench" \
-m "$MODEL" \
-t 4 \
-p 128 \
-n 32 2>&1 | tee "$BASE/bench-q8_0-pp128-tg32.log"
pp128 是 Prompt Processing,即处理 128 个输入 Token 的速度;tg32 是 Text Generation,即逐个生成 32 个 Token 的速度;t/s 是 tokens per second。前者影响长输入预处理和首 Token 等待,后者更接近连续输出时的体验,二者不能互换。
本文已有 Q6_K 实测结果
| 模型 | Backend | 构建 | CPU 线程 | Prompt Processing | Text Generation |
|---|---|---|---|---|---|
| Qwen3.5-2B Q6_K | RKNPU | 81eff6a | 4 | pp128:42.06 ± 0.84 t/s | tg32:6.85 ± 0.14 t/s |
原输出同时记录 size=1.44 GiB、params=1.88 B、ngl=99。这些是 Bench 对模型内容的输出口径,与下载页整文件十进制 GB 的口径不同,不应把两个大小字段强行解释为同一种计量。
以上是本文测试环境下的一次实测记录,不是官方性能承诺,也不代表所有 RK3588 板卡会得到相同数字。42.06 t/s 不能写成“每秒输出 42 个 Token”。Q8_0 是本文默认部署建议,但本文没有给出它的实测速度,不能把 Q6_K 结果挪给 Q8_0。
要对照 Q6_K,在板端下载并校验:
MODEL_DIR="$HOME/rk_llama_npu_test/models"
wget -c \
-O "$MODEL_DIR/Qwen3.5-2B.q6_k.gguf" \
'https://huggingface.co/AaryanK/Qwen3.5-2B-GGUF/resolve/3b3b8686d74f2ccfff1bcf36f80e114aff1b7881/Qwen3.5-2B.q6_k.gguf?download=true'
cd "$MODEL_DIR"
printf '%s %s\n' \
'92e39c352d037f0fe7517e6c8b7c2d3e950ca076c9ab46c7ff38ec6a2e7d9db8' \
'Qwen3.5-2B.q6_k.gguf' | sha256sum -c -
校验通过后,完整执行:
BASE="$HOME/rk_llama_npu_test"
BIN="$BASE/rk-llama.cpp/build/bin"
MODEL="$BASE/models/Qwen3.5-2B.q6_k.gguf"
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export RKNPU_CORES="0,1,2"
unset RKNPU_HYBRID
ulimit -n 65536
set -o pipefail
taskset -c 4-7 "$BIN/llama-bench" \
-m "$MODEL" -t 4 -p 128 -n 32 \
2>&1 | tee "$BASE/bench-q6_k-pp128-tg32.log"
不要在 Server 运行时另外启动 Bench。记录性能时同时保存源码提交、模型散列、NPU 驱动及实际 Runtime、CPU/NPU 频率、散热条件、Context、Prompt 长度、Batch/UBatch、可用内存和后台进程。原实测记录没有给出这些项目的所有数值,本文不补造;因此历史跑分可作为参考,不能用于严格回归判定。
13. 服务化:完整 start.sh 与 stop.sh
两份脚本均放在 $HOME/rk_llama_npu_test,而不是源码子目录。start.sh 自动定位部署根目录,检查模型、NPU 权限、端口和文件描述符,记录真实 Server PID,并等待 /health。stop.sh 同时检查 PID 文件和 /proc 中的真实进程,停止后验证端口释放。
下面是沿用实测参数和启停逻辑的 1.0.1 文章审核版:修正 CACHE_REUSE=0、PREDICT=-1 的注释;避免 LD_LIBRARY_PATH 出现尾部空路径项;为后台进程明确指定 /dev/null 标准输入。没有把这些编辑修订表述为一次新的板端实测。
start.sh 完整代码
执行 cd "$HOME/rk_llama_npu_test",用 nano start.sh 创建文件,粘贴下列全部内容。默认保持实测脚本的 16K Context;只做普通聊天时,可在跑通后按第 19 节改成 8K。
#!/usr/bin/env bash
# RK3588 rk-llama.cpp 启动脚本
# 版本:1.0.1(2026-09-09,文章审核版;部署参数沿用实测基线)
# 编码:UTF-8;换行:Linux LF
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
set -uo pipefail
umask 077
# ================= 用户配置 =================
# 脚本应放在部署根目录,目录下包含 rk-llama.cpp/ 和 models/。
BASE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# 更换模型时修改此处。
MODEL="$BASE/models/Qwen3.5-2B.q8_0.gguf"
HOST="0.0.0.0" # 监听所有网卡
PORT="8080" # Web UI/API 端口
THREADS="4" # 生成阶段 CPU 线程数
THREADS_BATCH="4" # 提示词处理 CPU 线程数
CPU_RANGE="4-7" # RK3588 四颗 Cortex-A76 大核
CPU_STRICT="1" # 1=严格限制在 CPU_RANGE
CPU_PRIORITY="1" # 0=普通,1=中等;普通用户不建议 2/3
CPU_POLL="100" # 0~100;越高延迟越低,但空闲功耗越高
USE_TASKSET="1" # 1=在操作系统层绑定 CPU_RANGE
CONTEXT_SIZE="16384" # 上下文长度,单位 Token
PREDICT="-1" # 服务端不设固定生成上限;调用方仍应传 max_tokens
BATCH_SIZE="2048" # 逻辑批大小,单位 Token
UBATCH_SIZE="512" # 物理批大小,单位 Token
PARALLEL="1" # 并发槽位;8GB 单用户建议 1
CACHE_RAM="512" # 提示词缓存上限,单位 MiB;0=关闭
CACHE_REUSE="0" # 0=关闭 KV shifting 块复用;不等于关闭前缀缓存
CACHE_TYPE_K="f16" # KV Cache 类型
CACHE_TYPE_V="f16"
FLASH_ATTN="auto" # auto/on/off
REASONING="off" # 是否输出思考过程
REASONING_BUDGET="0"
NPU_CORES="0,1,2" # RK3588 三颗 NPU 核心
NPU_HYBRID="" # 留空=根据 GGUF 类型自动选择 NPU 管线
LOCK_NPU_FREQ="0" # 0=自动调频,1=尝试锁频
NPU_FREQ="1000000000" # Hz;1000000000=1000MHz
ENABLE_MMAP="1"
ENABLE_MLOCK="0" # 8GB 无 Swap 设备建议保持 0
ENABLE_WARMUP="1"
ENABLE_METRICS="1"
NOFILE_LIMIT="65536" # RKNPU2 需要较高的文件描述符上限
START_TIMEOUT="120" # 等待 /health 就绪的最长秒数
# ================= 固定路径 =================
BIN="$BASE/rk-llama.cpp/build/bin"
SERVER="$BIN/llama-server"
LOG="$BASE/qwen35_server_${PORT}.log"
PIDFILE="$BASE/qwen35_server_${PORT}.pid"
LOCKFILE="$BASE/.qwen35_server_${PORT}.lock"
NPU_DEVICE="/dev/dri/by-path/platform-fdab0000.npu-render"
die() {
echo "错误:$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1"
}
# 读取 /proc/<PID>/cmdline 的原始参数,精确核对程序路径和端口。
# 这可以防止 PID 复用,也不会误匹配其他目录或其他端口的服务。
is_target_pid() {
local pid="$1"
local args=()
local i
[[ "$pid" =~ ^[0-9]+$ ]] || return 1
[ -r "/proc/$pid/cmdline" ] || return 1
mapfile -d '' -t args < "/proc/$pid/cmdline" 2>/dev/null || return 1
[ "${#args[@]}" -gt 0 ] || return 1
[ "$(readlink -m -- "${args[0]}")" = "$(readlink -m -- "$SERVER")" ] ||
return 1
for ((i=1; i<${#args[@]}; i++)); do
if [ "${args[$i]}" = "--port" ] &&
[ "${args[$((i+1))]:-}" = "$PORT" ]; then
return 0
fi
[ "${args[$i]}" = "--port=$PORT" ] && return 0
done
return 1
}
find_target_pids() {
local proc
local pid
for proc in /proc/[0-9]*; do
pid="${proc##*/}"
is_target_pid "$pid" && echo "$pid"
done
}
port_is_listening() {
ss -H -ltn "sport = :$PORT" 2>/dev/null | grep -q .
}
terminate_failed_start() {
local pids
local remaining
local pid
local i
pids=$(printf '%s\n%s\n' "${SERVER_PID:-}" "$(find_target_pids)" |
awk '/^[0-9]+$/ && !seen[$0]++')
while read -r pid; do
if [ -n "$pid" ]; then
kill "$pid" 2>/dev/null || true
fi
done <<< "$pids"
if [[ "${LAUNCH_PID:-}" =~ ^[0-9]+$ ]]; then
kill "$LAUNCH_PID" 2>/dev/null || true
fi
for ((i=0; i<10; i++)); do
remaining=$(find_target_pids)
[ -z "$remaining" ] && break
sleep 1
done
remaining=$(find_target_pids)
while read -r pid; do
if [ -n "$pid" ]; then
kill -9 "$pid" 2>/dev/null || true
fi
done <<< "$remaining"
rm -f "$PIDFILE" "$PIDFILE.tmp"
}
# ================= 启动前检查 =================
for cmd in awk curl flock grep nohup readlink ss; do
require_command "$cmd"
done
[ "$USE_TASKSET" = "0" ] || require_command taskset
if ! [[ "$PORT" =~ ^[0-9]+$ ]] ||
! ((10#$PORT >= 1 && 10#$PORT <= 65535)); then
die "PORT 必须为 1~65535"
fi
if ! [[ "$START_TIMEOUT" =~ ^[0-9]+$ ]] ||
! ((10#$START_TIMEOUT >= 1)); then
die "START_TIMEOUT 必须为正整数"
fi
if [ "$LOCK_NPU_FREQ" = "1" ]; then
if ! [[ "$NPU_FREQ" =~ ^[0-9]+$ ]] ||
! ((10#$NPU_FREQ > 0)); then
die "启用锁频时,NPU_FREQ 必须为正整数 Hz"
fi
NPU_FREQ=$((10#$NPU_FREQ))
fi
PORT=$((10#$PORT))
START_TIMEOUT=$((10#$START_TIMEOUT))
[ -x "$SERVER" ] || die "找不到可执行程序:$SERVER"
[ -f "$MODEL" ] || die "找不到模型:$MODEL"
[ -e "$NPU_DEVICE" ] || die "找不到 NPU 设备:$NPU_DEVICE"
if [ ! -r "$NPU_DEVICE" ] || [ ! -w "$NPU_DEVICE" ]; then
die "当前用户无权访问 NPU,请检查 render 组权限"
fi
# 防止两个 start.sh/stop.sh 同时操作同一服务。
exec 9>"$LOCKFILE"
flock -n 9 || die "另一个启动或停止操作正在进行"
EXISTING_PIDS=$(find_target_pids)
[ -z "$EXISTING_PIDS" ] ||
die "服务已经运行,PID:$(echo "$EXISTING_PIDS" | tr '\n' ' ')"
if port_is_listening; then
ss -H -ltnp "sport = :$PORT" 2>/dev/null || true
die "端口 $PORT 已被其他程序占用"
fi
if ! ulimit -n "$NOFILE_LIMIT"; then
die "无法把文件描述符限制设置为 $NOFILE_LIMIT"
fi
echo "文件描述符限制:$(ulimit -n)"
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export RKNPU_CORES="$NPU_CORES"
if [ -n "$NPU_HYBRID" ]; then
export RKNPU_HYBRID="$NPU_HYBRID"
else
unset RKNPU_HYBRID 2>/dev/null || true
fi
# 锁频是可选优化。没有免密码 sudo 时自动跳过,不影响启动。
if [ "$LOCK_NPU_FREQ" = "1" ]; then
NPU_DEVFREQ="/sys/class/devfreq/fdab0000.npu"
if [ -w "$NPU_DEVFREQ/min_freq" ] &&
[ -w "$NPU_DEVFREQ/max_freq" ]; then
if echo "$NPU_FREQ" > "$NPU_DEVFREQ/min_freq" &&
echo "$NPU_FREQ" > "$NPU_DEVFREQ/max_freq"; then
echo "NPU 频率已锁定:$((NPU_FREQ / 1000000))MHz"
else
echo "警告:NPU 锁频失败,继续使用系统调频"
fi
elif sudo -n true 2>/dev/null; then
if echo "$NPU_FREQ" |
sudo -n tee "$NPU_DEVFREQ/min_freq" >/dev/null &&
echo "$NPU_FREQ" |
sudo -n tee "$NPU_DEVFREQ/max_freq" >/dev/null; then
echo "NPU 频率已锁定:$((NPU_FREQ / 1000000))MHz"
else
echo "警告:NPU 锁频失败,继续使用系统调频"
fi
else
echo "警告:无可用 sudo 权限,跳过 NPU 锁频"
fi
fi
# ================= 组装参数并启动 =================
ARGS=(
-m "$MODEL"
--host "$HOST"
--port "$PORT"
-t "$THREADS"
-tb "$THREADS_BATCH"
--cpu-range "$CPU_RANGE"
--cpu-range-batch "$CPU_RANGE"
--cpu-strict "$CPU_STRICT"
--cpu-strict-batch "$CPU_STRICT"
--prio "$CPU_PRIORITY"
--prio-batch "$CPU_PRIORITY"
--poll "$CPU_POLL"
--poll-batch 1
-c "$CONTEXT_SIZE"
-n "$PREDICT"
-b "$BATCH_SIZE"
-ub "$UBATCH_SIZE"
--parallel "$PARALLEL"
--cache-ram "$CACHE_RAM"
--cache-reuse "$CACHE_REUSE"
--cache-type-k "$CACHE_TYPE_K"
--cache-type-v "$CACHE_TYPE_V"
--flash-attn "$FLASH_ATTN"
--reasoning "$REASONING"
--reasoning-budget "$REASONING_BUDGET"
)
if [ "$CACHE_RAM" = "0" ]; then
ARGS+=(--no-cache-prompt)
else
ARGS+=(--cache-prompt)
fi
if [ "$ENABLE_MMAP" = "1" ]; then
ARGS+=(--mmap)
else
ARGS+=(--no-mmap)
fi
[ "$ENABLE_MLOCK" = "1" ] && ARGS+=(--mlock)
[ "$ENABLE_WARMUP" = "0" ] && ARGS+=(--no-warmup)
[ "$ENABLE_METRICS" = "1" ] && ARGS+=(--metrics)
LAUNCH_CMD=("$SERVER")
if [ "$USE_TASKSET" = "1" ]; then
LAUNCH_CMD=(taskset -c "$CPU_RANGE" "$SERVER")
fi
# 关闭子进程继承的锁文件描述符,避免脚本退出后仍占用锁。
: > "$LOG" || die "无法写入日志:$LOG"
chmod 600 "$LOG" 2>/dev/null || true
LAUNCH_PID=""
SERVER_PID=""
trap 'echo "启动被中断"; terminate_failed_start; exit 130' INT TERM HUP
nohup "${LAUNCH_CMD[@]}" "${ARGS[@]}" < /dev/null > "$LOG" 2>&1 9>&- &
LAUNCH_PID=$!
# $! 可能是 taskset 等包装进程,因此从 /proc 定位真实服务 PID。
for ((i=0; i<100; i++)); do
if is_target_pid "$LAUNCH_PID"; then
SERVER_PID="$LAUNCH_PID"
break
fi
SERVER_PID=$(find_target_pids | head -n 1)
[ -n "$SERVER_PID" ] && break
sleep 0.1
done
if [ -z "$SERVER_PID" ]; then
terminate_failed_start
tail -50 "$LOG" 2>/dev/null || true
die "没有找到真实 llama-server 进程"
fi
if ! printf '%s\n' "$SERVER_PID" > "$PIDFILE.tmp" ||
! mv -f "$PIDFILE.tmp" "$PIDFILE"; then
terminate_failed_start
die "无法写入 PID 文件:$PIDFILE"
fi
# 只有 /health 返回成功后才报告启动完成;进程提前退出则立即报错。
HEALTH_HOST="$HOST"
[ "$HOST" = "0.0.0.0" ] && HEALTH_HOST="127.0.0.1"
READY="0"
DEADLINE=$((SECONDS + 10#$START_TIMEOUT))
while ((SECONDS < DEADLINE)); do
if ! is_target_pid "$SERVER_PID"; then
rm -f "$PIDFILE"
tail -50 "$LOG" 2>/dev/null || true
die "llama-server 在启动期间退出"
fi
if curl -fsS --connect-timeout 1 --max-time 2 \
"http://${HEALTH_HOST}:${PORT}/health" >/dev/null 2>&1; then
READY="1"
break
fi
sleep 1
done
if [ "$READY" != "1" ]; then
terminate_failed_start
tail -50 "$LOG" 2>/dev/null || true
die "等待服务就绪超时(${START_TIMEOUT}秒)"
fi
trap - INT TERM HUP
echo "服务已就绪,PID:$SERVER_PID"
echo "模型:$MODEL"
echo "监听地址:http://${HOST}:${PORT}/"
echo "日志:$LOG"
stop.sh 完整代码
在同一部署根目录用 nano stop.sh 创建文件。两份脚本的 PORT 必须相同。
#!/usr/bin/env bash
# RK3588 rk-llama.cpp 停止脚本
# 版本:1.0.1(2026-09-09,文章审核版;停止逻辑沿用实测基线)
# 编码:UTF-8;换行:Linux LF
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
set -uo pipefail
umask 077
# 脚本应与 start.sh 放在同一部署根目录。
BASE="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
SERVER="$BASE/rk-llama.cpp/build/bin/llama-server"
PORT="8080" # 必须与 start.sh 一致
STOP_TIMEOUT="20" # 等待正常退出的最长秒数
PIDFILE="$BASE/qwen35_server_${PORT}.pid"
LOCKFILE="$BASE/.qwen35_server_${PORT}.lock"
die() {
echo "错误:$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1"
}
# 精确匹配程序路径和 --port,避免 PID 复用或误杀其他服务。
is_target_pid() {
local pid="$1"
local args=()
local i
[[ "$pid" =~ ^[0-9]+$ ]] || return 1
[ -r "/proc/$pid/cmdline" ] || return 1
mapfile -d '' -t args < "/proc/$pid/cmdline" 2>/dev/null || return 1
[ "${#args[@]}" -gt 0 ] || return 1
[ "$(readlink -m -- "${args[0]}")" = "$(readlink -m -- "$SERVER")" ] ||
return 1
for ((i=1; i<${#args[@]}; i++)); do
if [ "${args[$i]}" = "--port" ] &&
[ "${args[$((i+1))]:-}" = "$PORT" ]; then
return 0
fi
[ "${args[$i]}" = "--port=$PORT" ] && return 0
done
return 1
}
find_target_pids() {
local proc
local pid
for proc in /proc/[0-9]*; do
pid="${proc##*/}"
is_target_pid "$pid" && echo "$pid"
done
}
port_is_listening() {
ss -H -ltn "sport = :$PORT" 2>/dev/null | grep -q .
}
for cmd in awk flock grep readlink ss; do
require_command "$cmd"
done
if ! [[ "$PORT" =~ ^[0-9]+$ ]] ||
! ((10#$PORT >= 1 && 10#$PORT <= 65535)); then
die "PORT 必须为 1~65535"
fi
if ! [[ "$STOP_TIMEOUT" =~ ^[0-9]+$ ]] ||
! ((10#$STOP_TIMEOUT >= 0)); then
die "STOP_TIMEOUT 必须为非负整数"
fi
PORT=$((10#$PORT))
STOP_TIMEOUT=$((10#$STOP_TIMEOUT))
# 与 start.sh 使用同一把锁,避免启停操作相互竞争。
exec 9>"$LOCKFILE"
flock -n 9 || die "另一个启动或停止操作正在进行"
TARGET_PIDS=""
if [ -f "$PIDFILE" ]; then
SAVED_PID=$(cat "$PIDFILE" 2>/dev/null || true)
is_target_pid "$SAVED_PID" && TARGET_PIDS="$SAVED_PID"
fi
# 始终扫描 /proc:PID 文件缺失、过期或记录了包装进程时仍能停止。
SCANNED_PIDS=$(find_target_pids)
TARGET_PIDS=$(printf '%s\n%s\n' "$TARGET_PIDS" "$SCANNED_PIDS" |
awk '/^[0-9]+$/ && !seen[$0]++')
if [ -z "$TARGET_PIDS" ]; then
rm -f "$PIDFILE"
if port_is_listening; then
ss -H -ltnp "sport = :$PORT" 2>/dev/null || true
die "未找到目标 llama-server,但端口 $PORT 仍被占用"
fi
echo "服务未运行,端口 $PORT 已释放"
exit 0
fi
echo "正在停止 llama-server:$(echo "$TARGET_PIDS" | tr '\n' ' ')"
while read -r pid; do
if [ -n "$pid" ]; then
kill "$pid" 2>/dev/null || true
fi
done <<< "$TARGET_PIDS"
for ((i=0; i<STOP_TIMEOUT; i++)); do
REMAINING=$(find_target_pids)
[ -z "$REMAINING" ] && break
sleep 1
done
REMAINING=$(find_target_pids)
if [ -n "$REMAINING" ]; then
echo "正常停止超时,强制停止:$(echo "$REMAINING" | tr '\n' ' ')"
while read -r pid; do
if [ -n "$pid" ]; then
kill -9 "$pid" 2>/dev/null || true
fi
done <<< "$REMAINING"
sleep 1
fi
FINAL_PIDS=$(find_target_pids)
[ -z "$FINAL_PIDS" ] ||
die "停止失败,仍存在进程:$(echo "$FINAL_PIDS" | tr '\n' ' ')"
rm -f "$PIDFILE"
if port_is_listening; then
ss -H -ltnp "sport = :$PORT" 2>/dev/null || true
die "目标进程已退出,但端口 $PORT 仍被占用"
fi
echo "服务已停止,端口 $PORT 已释放"
14. 启动参数分别控制什么
先理解参数,再修改脚本开头的配置区。下面数值是本文沿用的基线配置或明确说明的建议,不是适用于所有镜像的最优参数。
| 配置 | 基线值 | 含义与调整边界 |
|---|---|---|
BASE | 自动定位 | 按脚本自身目录定位源码、模型和日志,不依赖执行命令时所在目录 |
MODEL | Q8_0 文件 | 更换前先停止服务,路径和大小写必须完全一致 |
HOST / PORT | 0.0.0.0 / 8080 | IPv4 所有接口监听;端口修改时必须同步 start/stop |
THREADS / THREADS_BATCH | 4 / 4 | 生成阶段和提示词处理阶段的 CPU 线程数 |
CPU_RANGE / USE_TASKSET | 4-7 / 1 | 进程亲和性;程序内部 CPU 参数与 OS 绑定共同作用 |
CPU_STRICT | 1 | 严格限制程序工作线程的 CPU 范围 |
CPU_PRIORITY | 1 | 优先级请求;是否生效受普通用户权限约束,先看启动日志 |
CPU_POLL | 100 | 较积极的线程轮询;可能减少等待,也会增加 CPU 占用与能耗,需实测 |
CONTEXT_SIZE | 16384 | Context,服务可用上下文规模,单位 Token;普通聊天可降到 8192 |
PREDICT | -1 | 服务端不设固定生成 Token 上限;请求仍应设置 max_tokens,EOS 等条件也能终止输出 |
BATCH_SIZE | 2048 | 一次逻辑处理允许的 Token 批大小,不是并发用户数 |
UBATCH_SIZE | 512 | 物理计算微批,通常是拆开逻辑批后的实际计算粒度 |
PARALLEL | 1 | 同一服务内会话槽位数;8GB 先采用单槽,增加后需重新核对每槽上下文与内存 |
CACHE_RAM | 512 MiB | RAM 中提示词状态缓存预算,不是总内存上限 |
CACHE_REUSE | 0 | 关闭基于 KV shifting 的块复用;普通相同前缀复用由 prompt cache 机制控制 |
CACHE_TYPE_K / CACHE_TYPE_V | f16 / f16 | K、V 缓存的存储类型;不代表权重也变成 FP16 |
FLASH_ATTN | auto | 让程序按可用实现选择 Flash Attention;不代表所有注意力都在 NPU 执行 |
REASONING / REASONING_BUDGET | off / 0 | 沿用首次验证的非思考配置,升级时检查该版本帮助 |
NPU_CORES | 0,1,2 | 脚本配置名,导出为真正环境变量 RKNPU_CORES |
NPU_HYBRID | 空字符串 | 不导出自定义 RKNPU_HYBRID,保持自动管线 |
LOCK_NPU_FREQ / NPU_FREQ | 0 / 1000000000 Hz | 默认不锁频;1GHz 是可选设置值,不是已验证运行频率 |
ENABLE_MMAP | 1 | 允许文件内存映射;不代表模型或 NPU 缓冲区零内存占用 |
ENABLE_MLOCK | 0 | 不锁住全部可锁页;开启会受内存及 memlock 限制,不建议 8GB 默认开启 |
ENABLE_WARMUP | 1 | 使用预热,尽早执行部分初始化;不保证覆盖所有首次请求的形状和分配 |
ENABLE_METRICS | 1 | 开启指标端点;局域网管理使用,需纳入访问控制 |
NOFILE_LIMIT | 65536 | 设置后由服务子进程继承;设置失败即停止启动 |
START_TIMEOUT / STOP_TIMEOUT | 120 / 20 秒 | 等待健康就绪与正常退出;超时看日志,再决定是否调整 |
原实测脚本把 CACHE_REUSE=0 注释为“自动”。根据同一实测提交的 Server 文档,它实际表示禁用这项 KV shifting 块复用,本文已修正注释,数值保持 0。原脚本对 PREDICT=-1 的解释也补充了调用方仍应传入生成上限的条件。实测版本参数定义、Server 文档
脚本默认覆盖同端口的上次日志。准备对比试验前应先另存日志;日志可能包含提示词或模型信息,umask 077 把新文件权限限制在当前用户范围。flock 用于避免同目录、同端口启停竞争,它不是全系统 NPU 互斥锁,不能阻止另一个目录或账号启动其他模型。
15. 启动服务:编码、真实进程、健康状态都要过关
Windows 编辑器保存脚本时选择 UTF-8 和 Linux LF。板端执行:
cd "$HOME/rk_llama_npu_test"
sed -i 's/\r$//' start.sh stop.sh
sed -i '1s/^\xEF\xBB\xBF//' start.sh stop.sh
file start.sh stop.sh
bash -n start.sh
bash -n stop.sh
chmod +x start.sh stop.sh
./start.sh
第一条 sed 去除 CRLF 中的行尾 CR,第二条去除可能影响 shebang 的 UTF-8 BOM。它们不会把已经损坏的中文恢复成正确字符,也不能把任意 GBK 文件自动转换成 UTF-8。bash -n 无输出表示语法通过,不能证明模型参数、驱动和设备都正常。
start.sh 只有发现真实进程并收到 /health 成功响应,才报告“服务已就绪”。首次加载可能较慢,应从日志区分正在转换权重、等待内存和真实异常。
pgrep -ax llama-server
ss -ltnp | grep ':8080'
tail -f "$HOME/rk_llama_npu_test/qwen35_server_8080.log"
日志通常包含 RKNPU model buffer size、RKNPU compute buffer size、模型加载完成与服务监听信息。Ctrl+C 退出 tail -f 只停止查看日志,不停止后台模型。
健康检查
curl http://127.0.0.1:8080/health
成功响应:
{"status":"ok"}
加载期间可能返回 HTTP 503。健康接口证明服务的加载状态,不能代替一条实际生成请求;即使已就绪,也可能在首次复杂请求中触发新的内存分配。
Web UI
从电脑或手机浏览器访问开发板局域网地址,形式为 http://开发板实际IP:8080/。例如板卡地址若是 192.168.3.100,访问的就是 示例局域网地址。这个示例不是互联网服务地址。
0.0.0.0 表示监听所有 IPv4 接口,不是应该填写到另一台电脑浏览器里的目标 IP。先发一个短文本问题,再看服务日志和 NPU 负载。
若板端 /health 成功而其他电脑打不开网页,检查监听地址、防火墙、IP 和网络隔离。测试保持在可信局域网;不要直接将 8080 映射到公网。生产接入应配置反向代理、TLS、认证、访问控制与防火墙,或通过 VPN 访问。仅设置 0.0.0.0 不提供任何身份验证。
16. OpenAI 兼容 API:把开发板接入应用
图 6:PC、Web、Python 或 RAG 应用向本地 llama-server 发请求,服务再调用 rk-llama.cpp 与 RKNPU2;图中不涉及云端模型。
接口兼容的意义是客户端可以沿用常见的聊天请求结构,并不代表全部 OpenAI 产品接口或功能都被实现。先确认模型信息,再请求聊天:实测版本 API 文档
curl -fsS http://127.0.0.1:8080/v1/models
curl -sS --fail-with-body http://127.0.0.1:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "Qwen3.5-2B.q8_0.gguf",
"messages": [
{"role": "system", "content": "你是一个简洁的中文助手。"},
{"role": "user", "content": "请用两句话介绍RK3588。"}
],
"max_tokens": 128,
"temperature": 0.2,
"stream": false
}'
上例沿用实测请求中的模型名。客户端如严格核对模型 ID,应使用 /v1/models 的 data[0].id;换模型后也应同步修改。messages 是本次请求的消息及历史,max_tokens 限制此次输出,temperature 控制采样随机性,stream=false 等待完整响应。temperature=0.2 是简短接口验证配置,不是 Qwen 官方的通用质量最优建议。
以下 Python 使用标准库直接调用同一个、已经核实结构的 HTTP 接口,无需安装 SDK。在开发板运行时保持回环地址;在其他电脑运行时把 BASE_URL 改成板卡地址:
import json
import urllib.error
import urllib.request
BASE_URL = "http://127.0.0.1:8080"
try:
with urllib.request.urlopen(BASE_URL + "/v1/models", timeout=10) as response:
model_id = json.load(response)["data"][0]["id"]
payload = {
"model": model_id,
"messages": [
{"role": "system", "content": "你是一个简洁的中文助手。"},
{"role": "user", "content": "请用两句话介绍RK3588。"},
],
"max_tokens": 128,
"temperature": 0.2,
"stream": False,
}
request = urllib.request.Request(
BASE_URL + "/v1/chat/completions",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=180) as response:
result = json.load(response)
print(result["choices"][0]["message"]["content"])
except urllib.error.HTTPError as error:
print("HTTP", error.code, error.read().decode("utf-8", errors="replace"))
raise
这是按公开接口编写的调用示例,不作为新增板端实测记录。已有 OpenAI 兼容客户端通常填写 API Base URL http://开发板实际IP:8080/v1。示例服务没有启用鉴权;若以后在 Server 或代理层配置认证,客户端须同步携带对应凭据。
17. 证明请求确实使用了 NPU:四层证据链
图 7:设备注册、运行日志、请求期间的三核负载与 Bench 后端信息相互印证。波形为示意,不是新增负载实测曲线。
证据一:设备注册
BASE="$HOME/rk_llama_npu_test"
BIN="$BASE/rk-llama.cpp/build/bin"
export LD_LIBRARY_PATH="$BIN${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
"$BIN/llama-cli" --list-devices
应包含 RKNPU: Rockchip NPU。这一步最好在启动服务前完成,不加载第二个模型。
证据二:当前服务的日志
grep -Ei 'RKNPU|model buffer|compute buffer' \
"$HOME/rk_llama_npu_test/qwen35_server_8080.log"
查找 RKNPU model buffer、RKNPU compute buffer 等输出,并确认该日志对应当前运行的进程和刚发出的请求。日志说明后端建立了相关模型、计算缓冲,但仅有初始化信息仍不等于每次请求都成功完成。
证据三:请求期间的三核实际负载
sudo watch -n 1 cat /sys/kernel/debug/rknpu/load
在另一个终端或网页里发起一条足够长的文本请求。观察 Core0、Core1、Core2,而不是在空闲时读一次 0% 就下结论。短请求可能被 1 秒采样错过;采样时序、串行等待和调度都可能使各核负载不一致。
证据四:llama-bench 的 backend
停止 Server,确认进程退出后,再按第 12 节运行 Bench。输出 backend=RKNPU,并完成 pp 和 tg 两种测试。它提供可重复的计算场景;连同负载和日志,能形成比单张 NPU 百分比截图可靠得多的证据。
另外核对“生效配置”,别只看脚本
服务运行时在板端执行:
PID=$(cat "$HOME/rk_llama_npu_test/qwen35_server_8080.pid")
ps -p "$PID" -o pid,ppid,stat,etime,args
tr '\0' '\n' < "/proc/$PID/environ" | grep '^RKNPU'
taskset -pc "$PID"
grep -i 'open files' "/proc/$PID/limits"
ls -1 "/proc/$PID/fd" | wc -l
grep 'librknnrt.so' "/proc/$PID/maps" | head -n 3
应确认 RKNPU_CORES=0,1,2、CPU affinity 为 4-7、文件描述符限制达到 65536。fd 目录计数是瞬时打开描述符数量,不是上限;运行中会变化。最后一条用于查看实际映射的 Runtime 路径,必要时记录该文件 SHA256。
这些环境变量和亲和性只证明设置已传入,不代替负载证据。若 PID 文件失效,先用 pgrep -ax llama-server 和 ps 确认真实进程,不能对一个过期 PID 的结果做性能判断。
18. 停止服务:PID 文件消失,不等于服务消失
在某些启动链里,$! 指向的是 wrapper,而真正 Server 已成为它的子进程。只杀 wrapper,会留下继续占用模型内存和端口的服务进程。需要说明,GNU nohup 和 taskset 常见实现会直接 exec 目标程序,并不是“使用这两个命令就一定多一个进程”;要以实际进程树为准。
上述新版脚本保留了实测排障改进:启动时读取 /proc/PID/cmdline,同时核对程序路径和 --port;停止时不只信任 PID 文件,还扫描目标进程。正常 TERM 等待超时后才对仍匹配的目标强制停止,并再次检查进程和端口。
cd "$HOME/rk_llama_npu_test"
./stop.sh
pgrep -ax llama-server || echo '没有 llama-server 进程'
ss -ltnp | grep ':8080' || echo '8080 端口已释放'
free -h
pgrep 会显示同名的其他服务,需结合路径和端口判断。free -h 中文件页缓存仍可能保留,不能仅凭 used 没有立刻回到原数值就断定泄漏。真正的停止判据是目标进程消失、目标端口释放;内存用于辅助确认。
如仍有残留:
ps -eo pid,ppid,pgid,sid,stat,rss,etime,cmd | grep '[l]lama-server'
ss -H -ltnp 'sport = :8080'
检查是不是换了部署目录、修改了 start/stop 其中一份端口、或者由另一个用户启动。不要用无范围的 pkill -f 把其他任务一起停止。切换模型的顺序始终是:停止 → 验证退出 → 修改 MODEL → 启动。
19. 性能优化:先缩小变量,再放大负载
NPU 不必始终 100%
生成每个 Token 需要先准备输入,再执行模型各层计算、读写缓存状态,并采样得到下一个 Token。它不是一次长时间不间断的纯矩阵乘法:
CPU 准备与调度
→ NPU 支持的矩阵计算
→ CPU 部分算子与采样
→ 下一 Token
KV Cache 的读取和更新贯穿前向计算,图示并非真实算子级时间表。CPU/NPU 同步、内存访问和串行依赖都会留下间隙,所以负载可能呈脉冲、间歇或不同核心交替。未达到 100% 不能单独证明“没有完整调用 NPU”。
更有用的指标是:Prompt Processing t/s、Generation t/s、首 Token 延迟、完整回答耗时及连续请求稳定性。首 Token 延迟还受排队、Prompt 长度和是否命中缓存影响,不能只用 Bench 的 pp 速度替代。
8GB 应该配置多大 Context
Context 是模型一次处理所能使用的 Token 窗口,包含系统提示、历史、检索内容、用户消息和模板开销,还应为输出留出余量。模型卡给出的 262144 Token 原生上下文,是模型能力上限信息,不是 8GB 开发板的默认配置建议。Qwen3.5-2B 官方模型卡
估计资源时需要考虑:
模型权重及后端转换后的表示
+ KV Cache / 循环状态
+ Runtime 工作缓冲区
+ Prompt Cache
+ 操作系统和其他服务
= 共同占用系统 RAM
KV Cache 保存注意力计算中可复用的 Key/Value,避免每个 Token 都重算所有历史。Qwen3.5-2B 使用 Gated DeltaNet 与注意力混合结构,还存在相应状态,因此不宜直接套用“所有层都是普通全注意力”的 KV 内存公式。实际峰值应以这个模型、这个后端的日志和运行测量为准。
| 使用场景 | 建议起点 | 注意事项 |
|---|---|---|
| 普通聊天 | 8192 | 先控制历史和输出长度;从实测脚本的 16K 降低时同步记录设置 |
| 长对话 / 轻量 RAG | 16384 | 本文脚本基线值,仍需用实际长输入验证峰值内存 |
| 更长上下文试验 | 32768 | 逐级提高,观察长提示词处理、可用内存和稳定性,不作为默认承诺 |
这些是建议配置,不代表 8GB 上任意量化、任意并发都保证可用。PARALLEL 改变后还应检查每槽上下文,不应把多个槽位当作免费增加容量。查看日志中的实际值:
grep -E 'n_ctx_train|n_ctx +=|n_ctx_seq|KV|buffer size' \
"$HOME/rk_llama_npu_test/qwen35_server_8080.log"
free -h
vmstat 1
Batch 与 UBatch
BATCH_SIZE=2048 是逻辑批大小,UBATCH_SIZE=512 是物理微批大小,二者主要影响输入处理时计算任务的组织和缓冲需求。它们不是模型大小、Context,也不是 2048 个用户同时聊天。
先保持 2048/512。长输入可建议测试 UBatch 1024,但需重新观察内存峰值、pp 和稳定性;不要只看 pp 上升就忽略 tg、首 Token 延迟或崩溃。改变微批也不保证会提高单个新 Token 的生成速度。
CACHE_RAM 能节约哪些重复工作
CACHE_RAM=512 为提示词状态缓存提供 512MiB 预算。重复 System Prompt、相同前缀、同一文档连续问答、相同历史上下文,可能复用先前计算,主要减少重复输入处理。
它不是单 Token 生成速度加速器,也不是磁盘缓存;不命中复用条件时收益有限。脚本设为 0 时同时传入 --no-cache-prompt,因而关闭了脚本控制的这两项缓存设置;不要把这一组合行为误认为上游 --cache-ram 0 单个参数的所有含义。
原实测教程还给出普通聊天 Context=8192、CACHE_RAM=1024 的可试配置。它适合重复前缀多且剩余内存足够时比较;本文默认继续保留 512MiB,不将更大缓存当作必然更快。
CPU 绑定、频率与散热
先确认 CPU4~7、四线程、NPU0~2 和 PARALLEL=1,然后比较量化。系统中的 CPU 编号可结合 lscpu -e、各 CPU 的频率策略和厂商设备树核对;Arm 核心型号本身不会规定 Linux 把它编号为几。
NPU 锁频默认关闭。仅在确认板卡支持的频点和温控策略后考虑,先只读检查:
cat /sys/class/devfreq/fdab0000.npu/available_frequencies
cat /sys/class/devfreq/fdab0000.npu/cur_freq
脚本中的 1GHz 不代表每块板都支持,也不是本文已记录的实测频率。锁频是可选试验,权限不足会跳过;该脚本不会自动恢复手工改变的 min/max 频点,试验前应记录原值。保持自动调频与有效散热通常更适合先建立稳定基线。
每次只改一个参数,停止旧模型后再测,保留日志:
grep -E 'prompt eval time|eval time' \
"$HOME/rk_llama_npu_test/qwen35_server_8080.log" | tail -20
20. RK3588 本地知识库:让检索减轻上下文负担
RAG,即检索增强生成,是先找相关材料,再把材料交给模型回答。它能减少“把整篇 PDF 塞给模型”的输入负担,但不会消除模型的知识边界或幻觉。
PDF / 文档
→ 解析与清洗
→ Chunk 分段
→ Embedding 向量化
→ Vector DB 建索引
用户问题 → 向量检索 → Top-K 相关片段
↓
Qwen3.5-2B 文本服务
↓
带来源的回答
将解析和向量化尽量放在离线建库阶段,保留文件名、页码或段落来源。查询时先检索,再按 Token 预算限制片段总长度,为问题、系统提示和输出预留空间;先优化召回与切块,不要把 Context 直接拉到 256K。
生成模型不会因为能聊天就自动成为合适的 Embedding 模型。向量化组件需要单独选型和验证;若另用 NPU 模型做向量化,必须服从下节的多进程限制。初期可以把 Embedding 放在 CPU 或另一台设备,RK3588 维持单个生成服务。这是工程架构建议,不是一份完成验证的 RAG 配置。
21. 常见问题与故障排查
图 8:从最外层连接和服务状态逐步向模型与计算后端收敛;每一层都先用可观察证据定位。
| 现象 | 优先怀疑 | 检查方法 | 处理方式 |
|---|---|---|---|
| Processing 0%,NPU 也是 0% | 句柄耗尽,或请求根本未进入计算 | 搜索 Too many open files、Compute error、failed to convert handle,检查 /proc/PID/limits | 确认 65536 真正生效;修正后停止并重新启动,不只刷新网页 |
| /health 正常,第一次提问进程退出 | 请求阶段分配失败、损坏模型、错误管线、内存不足 | tail -200、SHA256、open files、内核 OOM 日志 | 恢复自动管线和小上下文;处理句柄与内存问题,再 CLI 验证 |
| 找不到模型 | 路径、大小写或上传位置不一致 | ls -lh models、查看 MODEL= | 统一绝对路径与文件名,重新校验文件 |
| 8080 已占用 | 旧 Server 或其他服务 | ss -H -ltnp 'sport = :8080' | 先确认进程归属;目标服务用匹配的 stop.sh,其他服务按其管理方式处理 |
| 别的电脑打不开 Web UI | 监听回环地址、防火墙、错误 IP、网络隔离 | 板端 /health、ss、ufw status | 改为预期监听地址,仅放行可信客户端,并确认网络可达 |
| --list-devices 没有 RKNPU | CMake 开关、动态库路径、Runtime 或权限错误 | CMakeCache、libggml-rknpu2.so、ldd、设备权限 | 修复缺失条件后重新编译或运行,勿仅凭 ldconfig 结论 |
| 中文乱码、bad interpreter、命令带奇怪字符 | 文件编码、BOM、CRLF 或终端字符集 | file、bash -n、编辑器编码设置 | 保存 UTF-8/LF,清除 BOM;终端用 UTF-8,乱码内容需重新保存 |
| Images require a vision-capable model | 本文文本路径未启用视觉组件 | 查看当前模型能力及启动参数 | 仅发文本;需要视觉时另行验证模型、视觉编码组件及对应支持 |
| Request exceeds context size | 历史、文档、模板和输出预算超限 | 日志中的实际 Context,检查请求 Token 规模 | 清理历史、压缩或检索内容;内存允许再逐步增加 Context |
| 多模型同时请求后卡死或 IOMMU 异常 | 独立进程同时操作 NPU,触及驱动/域限制 | 进程列表、内核日志、是否同时执行 CLI/Bench/Server | 保持一次一个 NPU 推理进程;先停止并验证旧进程 |
| 恢复开发板后网页仍有旧会话 | 访问端浏览器 IndexedDB | 同地址用无痕窗口对照 | 删除该站点会话或站点数据;不是继续删除模型目录 |
| stop.sh 后仍占大量内存 | wrapper PID、不同路径/端口、其他服务,或仅页缓存保留 | pgrep -ax、完整进程树、端口、free -h | 用完整停止脚本定位目标,检查真实进程和端口,不只删除 PID 文件 |
Processing 0% 与首次崩溃的恢复顺序
先取证:
LOG="$HOME/rk_llama_npu_test/qwen35_server_8080.log"
grep -E 'Too many open files|Compute error|failed to convert handle|failed to malloc npu memory' "$LOG"
tail -200 "$LOG"
sudo dmesg -T | grep -Ei 'rknpu|rknn|iommu|out of memory|oom|killed process' | tail -80
Too many open files 说明描述符资源耗尽,不能由这句话反推出上限一定是 1024;1024 是原实测中遇到的限制,其他会话值可能不同。以真实进程的 limits 为准。
若硬限制不足,可由管理员为实际登录用户配置 PAM limits,例如在板端创建配置,用户名从当前登录用户取得:
printf '%s soft nofile 65536\n%s hard nofile 65536\n' "$USER" "$USER" \
| sudo tee /etc/security/limits.d/90-rknpu-nofile.conf
这适用于启用 PAM limits 的登录会话;重新登录后检查 ulimit -Sn、ulimit -Hn。若后续改为 systemd 服务,应在服务单元中设置 LimitNOFILE=65536,不能假设 SSH 会话配置会被服务继承。
修正后先停止旧进程,恢复 NPU_HYBRID="",用校验通过的模型和小 Context 重新验证。原实测经验是:若句柄耗尽后已出现大量 RKNN 分配失败、随后首次推理仍段错误,仅重启进程可能无法恢复。保存日志、停止服务后,再考虑:
sudo reboot
重启用于恢复可能异常的驱动运行状态,不是替代根因修复。再次上线仍需确认限制、权限、散列和单进程条件。
网页不可达:从回环地址到局域网
curl -fsS http://127.0.0.1:8080/health
ss -H -ltnp 'sport = :8080'
ip -br addr
sudo ufw status
若没有安装 UFW,ufw 命令缺失不表示防火墙已放行,应按该镜像实际使用的防火墙检查。UFW 已启用且需要放行某台客户端时,可用受限规则:
read -r -p '输入允许访问的客户端 IPv4 地址:' CLIENT_IP
sudo ufw allow from "$CLIENT_IP" to any port 8080 proto tcp
原实测教程给出全来源放行 8080 的规则;这里改成显式输入可信客户端地址,以配合局域网部署边界。不要通过关闭整个防火墙来排除所有问题。
多个独立 NPU 模型进程的风险
实测及核验版 RKNPU2 README 均说明:每个 IOMMU domain 存在 4GB 限制,并警告在分配自定义 domain 后,多个独立进程同时访问 NPU,可能导致内核崩溃或系统冻结。这个限制不能等同于“整块 NPU 总共只能使用 4GB”或“整个模型进程只能使用 4GB”。后端多实例说明
对新部署最可靠的操作约束是:只运行一个模型推理进程;Bench 与 Server 不重叠;切换模型先停止并检查。给两个进程选择不同 NPU Core,或把端口改成两个值,都不是已经证明有效的隔离措施。
浏览器为什么还记得旧会话
实测版本 Web UI 使用 IndexedDB,数据库名为 LlamacppWebui。浏览器按照协议、主机和端口区分站点数据;恢复板卡系统,通常不会删除电脑或手机浏览器中的会话。对应版本数据库实现
先用同地址的无痕窗口验证。需要清理时,使用 Web UI 删除会话,或清除该站点数据;Chrome/Edge 也可在开发者工具的 Application → IndexedDB 删除 LlamacppWebui。清除前保留需要的记录,这会删除浏览器侧历史。升级后的数据库结构可能变化,应以对应 Web UI 实现为准。
22. 已完成编译后的最短启动流程
前提是已经按全文完成依赖、权限、编译、模型校验与脚本创建:
cd "$HOME/rk_llama_npu_test"
bash -n start.sh
bash -n stop.sh
chmod +x start.sh stop.sh
./start.sh
curl http://127.0.0.1:8080/health
tail -f qwen35_server_8080.log
浏览器访问 http://开发板实际IP:8080/。退出日志查看后停止:
./stop.sh
pgrep -ax llama-server || echo '没有 llama-server 进程'
ss -ltnp | grep ':8080' || echo '8080 端口已释放'
参考资料
以下链接用于追溯技术事实,核验日期为 2026-09-09;标明固定提交的资料优先用于复现。
| 名称 | 链接与用途 |
|---|---|
| rk-llama.cpp 仓库 | 项目首页:核实与 llama.cpp 的派生关系 |
| rknpu2 分支 | 当前分支:查看持续维护状态 |
| 实测版 RKNPU2 README | 固定提交说明:构建、默认管线、核心选择和多进程限制 |
| 当前版 RKNPU2 README | 分支说明:与实测版本作差异对照 |
| 当前量化更新 | 50e7865 提交:核实 per-channel 更新 |
| Server 参数和接口 | 固定版本文档:健康状态、缓存、模型列表、聊天接口 |
| 参数解析与链接库 | arg.cpp、后端 CMake:核对脚本开关与 Runtime 链接 |
| Qwen 官方模型 | Qwen3.5-2B 模型卡:模型架构、视觉组件与上下文能力 |
| 本文 GGUF 文件 | 固定版本文件列表:文件名、体积和 SHA256 |
| Rockchip 官方工具链 | RKNN-Toolkit2、RKLLM:区分 Runtime 与模型转换路线 |
| llama.cpp | 上游项目:理解 GGUF、ggml、工具和后端生态 |
| RK3588 与 A76 | Rockchip 介绍、Arm Cortex-A76:CPU、GPU、NPU 与 CPU 核心背景 |
| Linux 与 Bash | DRM render nodes、Bash 手册:设备权限和进程资源限制 |
| Web UI 数据存储 | 固定版本 database.service.ts:确认浏览器 IndexedDB 会话存储 |