RK3588 使用 rk-llama.cpp + RKNPU2 部署 Qwen3.5:从零开始的 NPU 本地大模型实战

图 1:本文以 RK3588 上的文本推理为目标。板卡为概念示意,CPU 与三核 NPU 均属于 RK3588 SoC。

写在前面:跑起来只是起点,还要知道它跑在哪里

一块 8GB 的 RK3588 开发板,可以把 Qwen3.5-2B 做成局域网里的模型服务:浏览器直接聊天,程序通过 /v1/chat/completions 调用,部分计算交给板载 NPU。真正需要解决的是一整条链路:设备权限是否正确、程序是否编译了 NPU 后端、GGUF 量化是否匹配硬件管线、第一次推理是否稳定,以及停止服务后内存和端口是否确实释放。

本文完成的部署路径是:

text 复制代码
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.cpprknpu2 分支
实测提交 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-clillama-benchllama-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 和用户名需按实际设备输入,避免把尖括号占位符误当重定向符号:

powershell 复制代码
$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.exetar,源码下载还需 Git;缺少 Git 时使用 Git 官方下载页

5. 检查系统、NPU、Runtime 和用户权限

系统与资源

bash 复制代码
cat /etc/os-release
uname -a
uname -m
free -h
df -h /

核对 aarch64,以及 free -h 中的 available。不要仅看 free 列:Linux 会用空闲内存作文件缓存。RAM 紧张时,模型能加载并不保证长提示词或首次推理也能成功。

设备、驱动和运行库

bash 复制代码
ls -l /dev/dri/by-path/platform-fdab0000.npu-render
sudo cat /sys/kernel/debug/rknpu/version
ls -l /usr/lib/librknnrt.so

本文实测设备链接及驱动输出为:

text 复制代码
platform-fdab0000.npu-render -> ../renderD129
RKNPU driver: v0.9.8

renderD129 是本文镜像的实际编号,其他镜像应以 by-path 指向和设备归属为准。若 debugfs 路径不存在,可先检查是否挂载:

bash 复制代码
findmnt -t debugfs

仅在确认系统支持 debugfs、但尚未挂载时执行:

bash 复制代码
sudo mount -t debugfs debugfs /sys/kernel/debug

挂载不会凭空安装驱动。设备节点缺失、驱动未加载、Runtime 缺失,需要回到厂商镜像或 SDK 处理,不能靠反复启动模型修复。

为什么优先检查 render 组

bash 复制代码
id
ls -l /dev/dri/renderD129

实测节点类似:

text 复制代码
crw-rw---- 1 root render ... /dev/dri/renderD129

root:render 表示所有者为 root、所属组为 render;0660 表示所有者和所属组有读写权限,其他用户没有。这个权限模型下,普通用户应加入 render,仅加入 video 不足以保证可访问此节点。不同镜像也可能使用 ACL 或不同组名,实际权限以节点为准。Linux DRM render node 文档

bash 复制代码
sudo usermod -aG render "$USER"

注销 SSH 并重新登录,再运行 id。旧会话不会自动获得新的附加组。后续以普通用户运行模型,仅对驱动状态、系统配置等操作使用 sudo

ldconfig 不能单独证明 NPU 是否可用

bash 复制代码
ldconfig -p | grep rknn

这个命令查询的是动态链接器缓存。没有结果不等于 librknnrt.so 不存在,更不等于 NPU 损坏;有结果也不等于后端已加载。判断必须联合使用:实际库文件 → 设备及权限 → --list-devices → 真实推理

还有一个版本细节:实测提交的后端 CMake 链接了源码目录内 ggml/src/ggml-rknpu2/libs/librknnrt.so。检查系统库是环境基线检查,进程最终加载哪份库还应看 ldd/proc/PID/maps,不要把系统文件存在误写成"运行时必定用了它"。该提交的后端构建文件

6. 安装依赖与建立部署目录

bash 复制代码
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 提供 tasksetflock 等工具,iproute2 提供 ssprocps 提供进程检查工具。原实测镜像已经具备部分依赖;这里补列 libssl-deviproute2procpsnano,使新镜像的依赖更明确。启用 HTTPS 的构建需要相应 OpenSSL 开发文件,不应遇错就随意关闭功能。

部署目录最终为:

text 复制代码
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

下面用于新部署目录。先下载分支,再额外取回实测提交并切换到它:

bash 复制代码
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,可获取完整分支历史再切换:

bash 复制代码
git fetch --unshallow origin rknpu2
git checkout --detach 81eff6a45c6abbb99e26907ef7fe8f7bf3ed2c62

后一组仅用于仍为浅仓库且前一条 fetch 失败的情况。已有源码目录应先检查修改状态,不要对仍在使用的目录重复 clone 或强制覆盖。

方式 B:电脑下载、压缩,再 SCP 上传

开发板访问 GitHub 不稳定时,在电脑 PowerShell 的一个新源码下载目录执行。下列连接变量沿用第 4 节:

powershell 复制代码
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 编译好的二进制。开发板执行:

bash 复制代码
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 已注册

bash 复制代码
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 计算后端动态库

现在检查设备注册:

bash 复制代码
export LD_LIBRARY_PATH="$REPO/build/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
"$REPO/build/bin/llama-cli" --list-devices

关键输出必须包含:

text 复制代码
RKNPU: Rockchip NPU

这只是第一层证据,说明程序能够发现该后端,尚不能证明当前模型的请求已经使用 NPU。只有程序启动成功、出现欢迎信息,都不足以代替它。

如果没有 RKNPU,按顺序检查:

bash 复制代码
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_STANDARDW4A4_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 的公开元数据与实测记录一致:

text 复制代码
文件名:Qwen3.5-2B.q8_0.gguf
大小:2012012480 bytes
SHA256:44bead7f88cb7f904799449fecae257f299f68e948a27858402062ecd9f7be88

固定版本的 Q8_0 文件信息 中的 SHA256 是完整文件散列;不要把页面上的 Xet hash 当作 SHA256。

推荐:电脑下载后上传

电脑 PowerShell 执行下载。-L 跟随跳转,--fail 遇到 HTTP 错误返回失败,--retry 处理短暂网络故障,-C - 从已有文件位置续传:

powershell 复制代码
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 变量:

powershell 复制代码
ssh -p $BoardPort $BoardTarget "mkdir -p '$BoardBase/models'"
scp -P $BoardPort .\Qwen3.5-2B.q8_0.gguf "${BoardTarget}:${BoardBase}/models/"

上传后在开发板运行:

bash 复制代码
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 看起来"差不多大"继续。

可选:开发板直接下载

bash 复制代码
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 推理:把错误限制在最少的层里

先退出其他模型实例,再运行:

bash 复制代码
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 不是无关紧要的调优项。后端快速入门

若设置失败,先看:

bash 复制代码
ulimit -Sn
ulimit -Hn

普通用户不能把软限制提高到硬限制以上。此时应由管理员调整登录会话限制,再重新登录;不要在失败后继续运行模型,也不要以为另一个 SSH 窗口里执行过 ulimit 就会改变已启动的服务。

不要照搬 --device RKNPU -ngl 99 本文实测命令依赖该分支的自动调度,没有额外强制 offload 参数。CUDA 教程中的层数参数不能机械迁移到另一种后端;后面 Bench 表里的 ngl=99 是其输出字段,不代表你需要向 CLI 或 Server 补上这组参数。

12. Bench:区分"读输入的速度"和"吐字的速度"

CLI 成功并退出后,运行同一模型的基准测试:

bash 复制代码
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 GiBparams=1.88 Bngl=99。这些是 Bench 对模型内容的输出口径,与下载页整文件十进制 GB 的口径不同,不应把两个大小字段强行解释为同一种计量。

以上是本文测试环境下的一次实测记录,不是官方性能承诺,也不代表所有 RK3588 板卡会得到相同数字。 42.06 t/s 不能写成"每秒输出 42 个 Token"。Q8_0 是本文默认部署建议,但本文没有给出它的实测速度,不能把 Q6_K 结果挪给 Q8_0。

要对照 Q6_K,在板端下载并校验:

bash 复制代码
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 -

校验通过后,完整执行:

bash 复制代码
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.shstop.sh

两份脚本均放在 $HOME/rk_llama_npu_test,而不是源码子目录。start.sh 自动定位部署根目录,检查模型、NPU 权限、端口和文件描述符,记录真实 Server PID,并等待 /healthstop.sh 同时检查 PID 文件和 /proc 中的真实进程,停止后验证端口释放。

下面是沿用实测参数和启停逻辑的 1.0.1 文章审核版 :修正 CACHE_REUSE=0PREDICT=-1 的注释;避免 LD_LIBRARY_PATH 出现尾部空路径项;为后台进程明确指定 /dev/null 标准输入。没有把这些编辑修订表述为一次新的板端实测。

start.sh 完整代码

执行 cd "$HOME/rk_llama_npu_test",用 nano start.sh 创建文件,粘贴下列全部内容。默认保持实测脚本的 16K Context;只做普通聊天时,可在跑通后按第 19 节改成 8K。

bash 复制代码
#!/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 必须相同。

bash 复制代码
#!/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。板端执行:

bash 复制代码
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 成功响应,才报告"服务已就绪"。首次加载可能较慢,应从日志区分正在转换权重、等待内存和真实异常。

bash 复制代码
pgrep -ax llama-server
ss -ltnp | grep ':8080'
tail -f "$HOME/rk_llama_npu_test/qwen35_server_8080.log"

日志通常包含 RKNPU model buffer sizeRKNPU compute buffer size、模型加载完成与服务监听信息。Ctrl+C 退出 tail -f 只停止查看日志,不停止后台模型。

健康检查

bash 复制代码
curl http://127.0.0.1:8080/health

成功响应:

json 复制代码
{"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 文档

bash 复制代码
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/modelsdata[0].id;换模型后也应同步修改。messages 是本次请求的消息及历史,max_tokens 限制此次输出,temperature 控制采样随机性,stream=false 等待完整响应。temperature=0.2 是简短接口验证配置,不是 Qwen 官方的通用质量最优建议。

以下 Python 使用标准库直接调用同一个、已经核实结构的 HTTP 接口,无需安装 SDK。在开发板运行时保持回环地址;在其他电脑运行时把 BASE_URL 改成板卡地址:

python 复制代码
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 后端信息相互印证。波形为示意,不是新增负载实测曲线。

证据一:设备注册

bash 复制代码
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。这一步最好在启动服务前完成,不加载第二个模型。

证据二:当前服务的日志

bash 复制代码
grep -Ei 'RKNPU|model buffer|compute buffer' \
  "$HOME/rk_llama_npu_test/qwen35_server_8080.log"

查找 RKNPU model bufferRKNPU compute buffer 等输出,并确认该日志对应当前运行的进程和刚发出的请求。日志说明后端建立了相关模型、计算缓冲,但仅有初始化信息仍不等于每次请求都成功完成。

证据三:请求期间的三核实际负载

bash 复制代码
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 百分比截图可靠得多的证据。

另外核对"生效配置",别只看脚本

服务运行时在板端执行:

bash 复制代码
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、文件描述符限制达到 65536fd 目录计数是瞬时打开描述符数量,不是上限;运行中会变化。最后一条用于查看实际映射的 Runtime 路径,必要时记录该文件 SHA256。

这些环境变量和亲和性只证明设置已传入,不代替负载证据。若 PID 文件失效,先用 pgrep -ax llama-serverps 确认真实进程,不能对一个过期 PID 的结果做性能判断。

18. 停止服务:PID 文件消失,不等于服务消失

在某些启动链里,$! 指向的是 wrapper,而真正 Server 已成为它的子进程。只杀 wrapper,会留下继续占用模型内存和端口的服务进程。需要说明,GNU nohuptaskset 常见实现会直接 exec 目标程序,并不是"使用这两个命令就一定多一个进程";要以实际进程树为准。

上述新版脚本保留了实测排障改进:启动时读取 /proc/PID/cmdline,同时核对程序路径和 --port;停止时不只信任 PID 文件,还扫描目标进程。正常 TERM 等待超时后才对仍匹配的目标强制停止,并再次检查进程和端口。

bash 复制代码
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 没有立刻回到原数值就断定泄漏。真正的停止判据是目标进程消失、目标端口释放;内存用于辅助确认。

如仍有残留:

bash 复制代码
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。它不是一次长时间不间断的纯矩阵乘法:

text 复制代码
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 官方模型卡

估计资源时需要考虑:

text 复制代码
模型权重及后端转换后的表示
  + KV Cache / 循环状态
  + Runtime 工作缓冲区
  + Prompt Cache
  + 操作系统和其他服务
  = 共同占用系统 RAM

KV Cache 保存注意力计算中可复用的 Key/Value,避免每个 Token 都重算所有历史。Qwen3.5-2B 使用 Gated DeltaNet 与注意力混合结构,还存在相应状态,因此不宜直接套用"所有层都是普通全注意力"的 KV 内存公式。实际峰值应以这个模型、这个后端的日志和运行测量为准。

使用场景 建议起点 注意事项
普通聊天 8192 先控制历史和输出长度;从实测脚本的 16K 降低时同步记录设置
长对话 / 轻量 RAG 16384 本文脚本基线值,仍需用实际长输入验证峰值内存
更长上下文试验 32768 逐级提高,观察长提示词处理、可用内存和稳定性,不作为默认承诺

这些是建议配置,不代表 8GB 上任意量化、任意并发都保证可用。PARALLEL 改变后还应检查每槽上下文,不应把多个槽位当作免费增加容量。查看日志中的实际值:

bash 复制代码
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=8192CACHE_RAM=1024 的可试配置。它适合重复前缀多且剩余内存足够时比较;本文默认继续保留 512MiB,不将更大缓存当作必然更快。

CPU 绑定、频率与散热

先确认 CPU4~7、四线程、NPU0~2 和 PARALLEL=1,然后比较量化。系统中的 CPU 编号可结合 lscpu -e、各 CPU 的频率策略和厂商设备树核对;Arm 核心型号本身不会规定 Linux 把它编号为几。

NPU 锁频默认关闭。仅在确认板卡支持的频点和温控策略后考虑,先只读检查:

bash 复制代码
cat /sys/class/devfreq/fdab0000.npu/available_frequencies
cat /sys/class/devfreq/fdab0000.npu/cur_freq

脚本中的 1GHz 不代表每块板都支持,也不是本文已记录的实测频率。锁频是可选试验,权限不足会跳过;该脚本不会自动恢复手工改变的 min/max 频点,试验前应记录原值。保持自动调频与有效散热通常更适合先建立稳定基线。

每次只改一个参数,停止旧模型后再测,保留日志:

bash 复制代码
grep -E 'prompt eval time|eval time' \
  "$HOME/rk_llama_npu_test/qwen35_server_8080.log" | tail -20

20. RK3588 本地知识库:让检索减轻上下文负担

RAG,即检索增强生成,是先找相关材料,再把材料交给模型回答。它能减少"把整篇 PDF 塞给模型"的输入负担,但不会消除模型的知识边界或幻觉。

text 复制代码
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 filesCompute errorfailed 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、网络隔离 板端 /healthssufw status 改为预期监听地址,仅放行可信客户端,并确认网络可达
--list-devices 没有 RKNPU CMake 开关、动态库路径、Runtime 或权限错误 CMakeCache、libggml-rknpu2.soldd、设备权限 修复缺失条件后重新编译或运行,勿仅凭 ldconfig 结论
中文乱码、bad interpreter、命令带奇怪字符 文件编码、BOM、CRLF 或终端字符集 filebash -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% 与首次崩溃的恢复顺序

先取证:

bash 复制代码
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,例如在板端创建配置,用户名从当前登录用户取得:

bash 复制代码
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 -Snulimit -Hn。若后续改为 systemd 服务,应在服务单元中设置 LimitNOFILE=65536,不能假设 SSH 会话配置会被服务继承。

修正后先停止旧进程,恢复 NPU_HYBRID="",用校验通过的模型和小 Context 重新验证。原实测经验是:若句柄耗尽后已出现大量 RKNN 分配失败、随后首次推理仍段错误,仅重启进程可能无法恢复。保存日志、停止服务后,再考虑:

bash 复制代码
sudo reboot

重启用于恢复可能异常的驱动运行状态,不是替代根因修复。再次上线仍需确认限制、权限、散列和单进程条件。

网页不可达:从回环地址到局域网

bash 复制代码
curl -fsS http://127.0.0.1:8080/health
ss -H -ltnp 'sport = :8080'
ip -br addr
sudo ufw status

若没有安装 UFW,ufw 命令缺失不表示防火墙已放行,应按该镜像实际使用的防火墙检查。UFW 已启用且需要放行某台客户端时,可用受限规则:

bash 复制代码
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. 已完成编译后的最短启动流程

前提是已经按全文完成依赖、权限、编译、模型校验与脚本创建:

bash 复制代码
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/。退出日志查看后停止:

bash 复制代码
./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-Toolkit2RKLLM:区分 Runtime 与模型转换路线
llama.cpp 上游项目:理解 GGUF、ggml、工具和后端生态
RK3588 与 A76 Rockchip 介绍Arm Cortex-A76:CPU、GPU、NPU 与 CPU 核心背景
Linux 与 Bash DRM render nodesBash 手册:设备权限和进程资源限制
Web UI 数据存储 固定版本 database.service.ts:确认浏览器 IndexedDB 会话存储
相关推荐
殷紫川1 小时前
ZCode 把整个 Git 仓库加密上传到了阿里云 OSS:一次客户端逆向的完整复盘
llm·aigc
dong_junshuai1 小时前
每天一个开源项目#103 BrowserSkill:4.7K星,Agent接管真实浏览器
开源·github·agent
dong_junshuai1 小时前
每天一个开源项目#102 六阶段代码复核流程范式
开源·github·agent
商业看点解说1 小时前
哪些 AI 创业公司和生成式 AI 产品适合使用 Amazon Bedrock?
人工智能
智驭未来掌门人1 小时前
大模型网关集成 MCP 与 CLI 的调用指南(自动分配密钥工具)
人工智能
snakeshe10101 小时前
AI 应用开发Day4(下):Embedding——从关键词匹配走向语义检索
人工智能
秦先生在广东1 小时前
Tencent BrowserSkill:让 AI Agent 无缝接入真实浏览器会话
人工智能
Allen正心正念20251 小时前
Agent框架—LangGraph智能体框架设计要点和踩坑预防指南
人工智能
高升说2 小时前
无人机机载视觉避障硬件链路拆解:从成像到制动的五层设计
人工智能