5.8 KiB · text History c20d4f7

rknn-describe-server

A small HTTP server that runs on the Rockchip board (RK3588 / RK3576 — e.g. r5bp-0) and exposes the airockchip/rknn-llm multimodal demo over a simple multipart endpoint that the DMS describe worker can call.

The DMS Rust worker stays on x86; this server is the only thing that needs to live on the NPU host.

Wire protocol

POST /describe — multipart/form-data:

field required notes
image yes binary image (jpg/png/webp/…)
prompt no overrides default prompt
model no name registered in models.yaml (default: first entry)
max_new_tokens no integer, default 256

Response:

{ "description": "A red barn at dusk with a tractor in front.",
  "model": "qwen2-vl-2b",
  "tokens": 27 }

GET /healthz — 200 ok once the model is loaded.

Approach

The reference implementation here just shells out to the upstream multimodal_demo binary per request. That keeps the integration minimal but reloads the model every call — fine for a smoke test, far too slow for production.

For sustained throughput, replace subprocess with a long-lived ctypes wrapper around librkllmrt.so (see rkllm.h in the upstream repo) that keeps the language model warm across requests. The request/response shape above does not need to change.

Setup on an RK3588 board

librknnrt.so and librkllmrt.so both ship inside the upstream repo (3rdparty/librknnrt/, rkllm-runtime/) and get installed next to the demo binary, so no system-wide RKNN runtime is required.

The bring-up below is what we ran on r5bp-0 and should reproduce on any RK3588 board with the same OS image.

sudo apt install -y git cmake g++ python3-venv python3-pip

# 1. clone the upstream toolkit and build the multimodal demo (the cmake
#    build-linux.sh assumes a cross-compiler — on-device just call cmake
#    directly with the system toolchain).
mkdir -p ~/dms-vlm && cd ~/dms-vlm
git clone --depth 1 https://github.com/airockchip/rknn-llm.git
cd rknn-llm
# rkllm's default CPU mask assumes 8 online cores. Required on boards with
# fewer (the CM5 at 172.16.15.136 has 6: 4x A55 + 2x A76) -- without it
# rkllm_init dies with "Mismatch between enabled CPUs mask and expected count".
patch -p1 < /path/to/dms/tools/rknn-describe-server/patches/0001-demo-cpu-mask-env.patch
cd examples/multimodal_model_demo/deploy
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release \
         -DCMAKE_SYSTEM_NAME=Linux \
         -DCMAKE_SYSTEM_PROCESSOR=aarch64
make -j8 install
# ⇒ ./install/demo_Linux_aarch64/{demo,imgenc,lib/}

# 2. fetch a converted VLM. The Qwen2-VL-2B build below totals ~3.3 GiB.
mkdir -p ~/dms-vlm/models/qwen2-vl-2b && cd ~/dms-vlm/models/qwen2-vl-2b
wget -O Qwen2-VL-2B-Instruct.rkllm \
  https://huggingface.co/thanhtantran/Qwen2-VL-2B-RKLLM/resolve/main/Qwen2-VL-2B-Instruct.rkllm
wget -O Qwen2-VL-2B-Instruct.rknn \
  https://huggingface.co/thanhtantran/Qwen2-VL-2B-RKLLM/resolve/main/Qwen2-VL-2B-Instruct.rknn

# 3. drop server.py + models.yaml into ~/dms-vlm, create a venv, install deps
cd ~/dms-vlm
python3 -m venv venv
./venv/bin/pip install fastapi "uvicorn[standard]" python-multipart pyyaml pillow
# optional: HEIC/AVIF uploads (server.py registers it if importable)
./venv/bin/pip install pillow-heif

# 4. install the user-level systemd unit and enable linger so it keeps
#    running across reboots without an active login session.
mkdir -p ~/.config/systemd/user
install -m 644 dms-vlm.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now dms-vlm.service
sudo loginctl enable-linger "$USER"

Smoke test from another host:

curl -s http://172.16.15.136:8080/healthz
curl -F image=@/path/to/sample.jpg http://172.16.15.136:8080/describe

First request takes ~10–15 s because the demo binary reloads the model every call. That’s the thing to fix when going to production — replace subprocess with a long-lived ctypes wrapper around librkllmrt.so so the LLM stays warm. The wire contract above does not change.

models.yaml — paths must point at the files as downloaded in step 2; the Qwen2-VL build ships one .rknn (vision) and one .rkllm (LLM), not vision.rknn / llm.rkllm:

qwen2-vl-2b:
  vision: /home/gmorell/dms-vlm/models/qwen2-vl-2b/Qwen2-VL-2B-Instruct.rknn
  llm:    /home/gmorell/dms-vlm/models/qwen2-vl-2b/Qwen2-VL-2B-Instruct.rkllm
  max_context_len: 2048
  rknn_core_num: 3
  platform: rk3588          # rk3588 | rk3576 | rk3562 | rv1126b
  enabled_cpus_num: 4       # must be >= rknn_core_num and match the mask popcount
  enabled_cpus_mask: "0x3C" # 6-core CM5: both A76 (cpu4,5) + two A55 (cpu2,3)
  img_start: "<|vision_start|>"
  img_end:   "<|vision_end|>"
  img_content: "<|image_pad|>"

platform is mandatory — the demo takes it as argv[7] and refuses to start without a value it recognises. enabled_cpus_num / enabled_cpus_mask are only honoured by a demo built with patches/0001-demo-cpu-mask-env.patch; omit them on a stock 8-core board.

Known limitation: short descriptions

The thanhtantran/Qwen2-VL-2B-RKLLM W8A8 conversion emits EOS after a single clause, e.g. "The image depicts an astronaut on the moon,". This is the model, not the wiring: raising max_new_tokens from 256 to 512 returns byte-identical output, and enabling the demo’s chat template changes nothing. Try a different conversion or a larger VLM if fuller captions are needed.

The DMS worker then talks to http://r5bp-0:8080/describe — see workers.yml for the env-var wiring.