Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Notable changes to vla.cpp. Format loosely follows [Keep a Changelog](https://ke

### Added

- **Hexagon NPU on the Qualcomm Dragonwing IQ-10 (Linux).**
`scripts/build_linux_hexagon.sh` cross-builds vla.cpp with `GGML_HEXAGON=ON`
in Qualcomm's toolchain container, `vla-server` included. On the IQ-10 (v81,
llama.cpp `b11223`) SmolVLA, π0.5, Evo-1 and GR00T N1.5 / N1.6 / N1.7 run
and were checked against the CPU build on fixed inputs; GR00T N1.7 is above
the 2.9e-3 bar. Concats the NPU refuses on buffers above 4 GB of DSP address
space run on the CPU. See `docs/backend/hexagon.md` for the build, the skel
install and the numbers.
- **ROCm/HIP backend for VLA inference.** `-DGGML_HIP=ON` selects ggml's HIP
backend without compiling vla.cpp's CUDA-only kernels. On Linux `gfx1200`
(Radeon RX 9060 XT) with ROCm 7.14 and llama.cpp `b11223`, every arch except
Expand Down
89 changes: 81 additions & 8 deletions docs/backend/hexagon.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
What running `vla.cpp` on Qualcomm's Hexagon NPU takes, and what it turned out
to take once it ran.

> **Status: running on Windows on Arm, not yet on a Linux board.** vla.cpp builds
> with `GGML_HEXAGON=ON` and runs eleven checkpoints on a Snapdragon X laptop's
> v73 NPU, each checked against a CPU reference. The build, the measurements and
> the upstream bugs found along the way are in
> [hexagon-windows.md](hexagon-windows.md). What follows is the backend in
> general: the parts that hold on any Snapdragon part, and the Linux / IQ-9 notes
> from before anything ran, which are still unverified.
> **Status: running on Windows on Arm and on a Dragonwing IQ-10 under Linux.**
> vla.cpp builds with `GGML_HEXAGON=ON` and runs eleven checkpoints on a
> Snapdragon X laptop's v73 NPU and six on an IQ-10's v81 NPU under Linux, each
> checked against a CPU reference. The laptop build, its measurements and the
> upstream bugs found along the way are in
> [hexagon-windows.md](hexagon-windows.md); the Linux board is in [Linux
> (IQ-10)](#linux-iq-10). What follows is the backend in general: the parts that
> hold on any Snapdragon part, and the IQ-9 and multi-NPU notes from before
> anything ran, which are still unverified.
>
> The earlier version of this file was written without hardware and projected
> 0.8-2.0 s per SmolVLA action chunk on a v79 part. On a v73 part it measured
Expand Down Expand Up @@ -115,6 +117,12 @@ was right: a missing op fails at load, and these only show in the actions.
[hexagon-windows.md](hexagon-windows.md#hexagon-issues-found-and-worked-around)
has the numbers and how each was bisected. They belong upstream.

One more op is claimed and then refused at run time. On buffers mapped above
4 GB of DSP address space, which the IQ-10 under Linux hands out, the NPU runs
`CONCAT` only as the dim-1 contiguous DMA copy and answers any other concat with
`NO_SUPPORT`, and the host aborts on the first predict. The wrapper sends those
concats to the CPU.

## What changed in `vla.cpp`

The shape of the diff was predicted correctly, apart from the scheduler:
Expand Down Expand Up @@ -190,7 +198,8 @@ docker run -it --rm -u $(id -u):$(id -g) --volume $(pwd):/workspace \
```

On the device, both library paths must be set - `ADSP_LIBRARY_PATH` is how the
FastRPC loader finds the HTP skel:
FastRPC loader finds the HTP skel (not on every board: the IQ-10 ignores it, see
[Linux (IQ-10)](#linux-iq-10)):

```bash
export LD_LIBRARY_PATH=./lib
Expand All @@ -210,6 +219,70 @@ SSH or ADB from the host:
llama-completion -m gemma-2b-it-Q4_0.gguf -f prompt.txt --split-mode tensor
```

## Linux (IQ-10)

Run on a Qualcomm Dragonwing IQ-10 board (IQX EVK, QCS8797) under Linux
(Ubuntu 24.04): Hexagon v81, four CDSP domains, llama.cpp `b11223`.

### Building

The Hexagon SDK has no arm64 Linux build, so vla.cpp is cross-built on an
x86-64 host, in the same toolchain container as llama.cpp above:

```bash
scripts/build_linux_hexagon.sh # with vla-server
scripts/build_linux_hexagon.sh --no-server # without it
```

The package lands in `build-lnx-htp/pkg` (`bin/`, `lib/`). For the server the
script installs Debian's arm64 protobuf and ZeroMQ dev packages in the
container; the board needs the matching runtime libraries
(`libprotobuf32t64`, `libzmq5`). See `scripts/build_linux_hexagon.sh --help`
for the options.

### Installing the skel

On this board FastRPC ignores `ADSP_LIBRARY_PATH`. It loads
`libggml-htp-v81.so` from `/vendor/dsp/cdspN/`, one copy per CDSP domain, and
falls back to `/usr/share/fastrpc/`. It also loads only skels processed by the
board vendor's tooling: an unprocessed one fails with `failed to open session
... error 0x80000406`. Set `HTP_SKEL_POST` to that tool when building, then
install the skel on the board as root:

```bash
for d in /vendor/dsp/cdsp*; do cp pkg/lib/libggml-htp-v81.so "$d"/; done
```

Reinstall it whenever the build changes. A skel from another llama.cpp build,
such as one left in `/usr/share/fastrpc/`, can open a session and then hang on
the first graph.

### Running

```bash
export LD_LIBRARY_PATH=$PWD/pkg/lib
./pkg/bin/vla-server --bind 'tcp://*:5555' smolvla-libero.gguf
```

`vla-server` served SmolVLA to `eval/client/vla_cpp_client.py`.

### Fidelity

`vla_predict_check` at the Hexagon defaults, against the same binary with
`VLA_DEVICE=cpu --weight-dtype f32`:

| Model | max\|Δ\| |
|---|--:|
| SmolVLA | 7.2e-4 |
| π0.5 | 3.6e-4 |
| Evo-1 | 4.1e-4 |
| GR00T N1.5 | 1.7e-3 |
| GR00T N1.6 | 1.3e-3 |
| GR00T N1.7 | 5.4e-3 |

GR00T N1.7 is above the 2.9e-3 bar. The other archs have not been run on the
board yet.

## IQ-10, imagined

Nothing about a second-generation part is knowable from this repository, so what
Expand Down
126 changes: 126 additions & 0 deletions scripts/build_linux_hexagon.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# Copyright 2026 VinRobotics
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Cross-build vla.cpp with the Hexagon NPU for a Qualcomm Linux board such as
# the Dragonwing IQ-10, in Qualcomm's toolchain container. The Hexagon SDK has
# no arm64 Linux build, so the HTP skels cannot be built on the board.
#
# scripts/build_linux_hexagon.sh # with vla-server
# scripts/build_linux_hexagon.sh --no-server # without it
# scripts/build_linux_hexagon.sh -- -DVLA_SPM=ON # extra CMake arguments
#
# Needs Docker. The package lands in <build-dir>/pkg (bin/, lib/); copy it to
# the board and install the HTP skel as docs/backend/hexagon.md describes.
#
# With the server, every run installs Debian's arm64 protobuf and ZeroMQ dev
# packages in the container (multiarch); --no-server skips that.
#
# Environment:
# HTP_SKEL_POST command run as "$HTP_SKEL_POST <in> <out>" on each
# libggml-htp-v*.so, for boards that load only processed
# skels. Unset, the skels are packaged as built.
# TOOLCHAIN_IMAGE toolchain image (default below).

set -euo pipefail

IMAGE="${TOOLCHAIN_IMAGE:-ghcr.io/snapdragon-toolchain/arm64-linux:v0.7}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="build-lnx-htp"
SERVER=1
RECONFIGURE=0
EXTRA=()

usage() { sed -n '/^# Cross-build/,/^# *TOOLCHAIN_IMAGE/p' "$0" | sed 's/^# \{0,1\}//'; }

while (($#)); do
case "$1" in
--build-dir) BUILD_DIR="${2:?--build-dir needs a value}"; shift 2 ;;
--no-server) SERVER=0; shift ;;
--reconfigure) RECONFIGURE=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; EXTRA=("$@"); break ;;
*) echo "unknown option: $1 (see --help)" >&2; exit 2 ;;
esac
done

case "$BUILD_DIR" in
/*|*..*) echo "--build-dir must be a path inside the repository" >&2; exit 2 ;;
esac
command -v docker >/dev/null || { echo "docker not found" >&2; exit 1; }
docker info >/dev/null 2>&1 || { echo "cannot reach the Docker daemon (is this user in the docker group?)" >&2; exit 1; }
if [[ -n "${HTP_SKEL_POST:-}" ]] && ! command -v "${HTP_SKEL_POST%% *}" >/dev/null; then
echo "HTP_SKEL_POST is not executable: ${HTP_SKEL_POST}" >&2; exit 1
fi

B="$REPO/$BUILD_DIR"
PKG="$B/pkg"
mkdir -p "$B"
if ((RECONFIGURE)); then rm -f "$B/CMakeCache.txt"; fi

CMAKE_ARGS=(
-S . -B "$BUILD_DIR" -G Ninja -DCMAKE_BUILD_TYPE=Release
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=arm64
-DCMAKE_LIBRARY_ARCHITECTURE=aarch64-linux-gnu
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
-DCMAKE_C_COMPILER_TARGET=aarch64-linux-gnu -DCMAKE_CXX_COMPILER_TARGET=aarch64-linux-gnu
"-DCMAKE_C_FLAGS=-march=armv8.2a+fp16+dotprod"
"-DCMAKE_CXX_FLAGS=-march=armv8.2a+fp16+dotprod"
-DCMAKE_PREFIX_PATH=/usr/aarch64-linux-gnu -DPREBUILT_LIB_DIR=linux_aarch64
-DGGML_HEXAGON=ON -DGGML_OPENMP=OFF -DLLAMA_OPENSSL=OFF
-DVLA_SPM=OFF -DVLA_BUILD_TESTS=ON -DVLA_BUILD_SERVER="$( ((SERVER)) && echo ON || echo OFF )"
"${EXTRA[@]}"
)

# The container runs as root only to install packages; the build runs as the
# calling user so the build tree stays theirs.
docker run --rm --platform linux/amd64 -v "$REPO":/src -w /src \
-e SERVER="$SERVER" -e BUILD_DIR="$BUILD_DIR" -e UID_GID="$(id -u):$(id -g)" "$IMAGE" bash -c '
set -euo pipefail
if [[ $SERVER == 1 ]]; then
dpkg --add-architecture arm64
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
pkg-config protobuf-compiler libprotobuf-dev:arm64 libzmq3-dev:arm64 cppzmq-dev:arm64 >/dev/null
fi
exec setpriv --reuid="${UID_GID%:*}" --regid="${UID_GID#*:}" --clear-groups \
env HOME=/tmp PKG_CONFIG_LIBDIR=/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig \
bash -c "set -e
cmake -DHEXAGON_SDK_ROOT=\$HEXAGON_SDK_ROOT -DHEXAGON_TOOLS_ROOT=\$HEXAGON_TOOLS_ROOT \"\$@\"
cmake --build \"\$BUILD_DIR\" -j\$(nproc)
cmake --build \"\$BUILD_DIR\" -j\$(nproc) --target htp-v73 htp-v75 htp-v79 htp-v81" _ "$@"
' _ "${CMAKE_ARGS[@]}"

rm -rf "$PKG"
mkdir -p "$PKG/bin" "$PKG/lib"
BINS=(vla-cli vla-bench tests/vla_predict_check)
if ((SERVER)); then BINS+=(vla-server); fi
for t in "${BINS[@]}"; do
if [[ -f "$B/$t" ]]; then cp "$B/$t" "$PKG/bin/"; fi
done
cp -a "$B"/libvla_core.so* "$B"/bin/lib*.so* "$PKG/lib/"
for v in 73 75 79 81; do
skel="$B/_deps/llama-build/ggml/src/ggml-hexagon/htp-v$v-prefix/src/htp-v$v-build/libggml-htp-v$v.so"
if [[ -n "${HTP_SKEL_POST:-}" ]]; then
$HTP_SKEL_POST "$skel" "$PKG/lib/libggml-htp-v$v.so"
else
cp "$skel" "$PKG/lib/"
fi
done
if [[ -z "${HTP_SKEL_POST:-}" ]]; then
echo "note: HTP_SKEL_POST unset, skels packaged as built; some boards load only processed skels"
fi

echo "package: $PKG"
ls "$PKG/bin"
Loading