Skip to main content

README.md

# hexagon_tpu

Elixir bindings for the Qualcomm Hexagon NPU (HTP) via **QNN**
(Qualcomm AI Engine Direct), targeting the Radxa Dragon Q6A (QCS6490,
HTP v68) running [Nerves](https://nerves-project.org/) with
[`nerves_system_dragon_q6a`](https://github.com/monoflow-ayvu/nerves_system_dragon_q6a).

Nx tensors in, Nx tensors out; quantization handled automatically from the
model's own metadata.

```elixir
{:ok, model} = HexagonTpu.load_model("/data/mobilenet_v2.bin")
{:ok, [scores]} = HexagonTpu.predict(model, [image_tensor])   # f32 in, f32 out
```

## How it works

- Models are **pre-compiled QNN context binaries** (`.bin`) produced on an
  x86-64 host with the QAIRT SDK tools (see [docs/models.md](docs/models.md)).
  On device we only deserialize (`QnnContext_createFromBinary`) and execute —
  no on-device graph prepare, no `libQnnHtpPrepare.so`.
- The NIF dlopen's `libQnnHtp.so`/`libQnnSystem.so` from the device rootfs
  (shipped by the system's `qairt-runtime` Buildroot package). Nothing
  proprietary is linked or bundled here; building only needs the SDK
  **headers** (`QNN_SDK_ROOT`).
- Tensor I/O metadata (names, shapes, dtypes, quant params) is introspected
  from the context binary via QnnSystem at load time.
- Layered API: `HexagonTpu.Runtime` (backend/device) → `HexagonTpu.Context`
  (loaded binary) → `HexagonTpu.Graph` (execute). Resources are freed by GC
  in dependency order; `close/1` exists for deterministic teardown.

## Requirements

On device (provided by `nerves_system_dragon_q6a` with
`BR2_PACKAGE_QAIRT_RUNTIME=y`):

- fastrpc kernel driver + CDSP firmware + `cdsprpcd`
- `/usr/lib/libQnnHtp.so`, `libQnnHtpV68Stub.so`, `libQnnSystem.so`
- `/usr/lib/dsp/libQnnHtpV68Skel.so` (+ `DSP_LIBRARY_PATH=/usr/lib/dsp`)

Build host: `QNN_SDK_ROOT` pointing at an extracted QAIRT SDK
(2.42.0.251225). The provided `devenv.nix` fetches it. Consumers of the
published Hex package get a precompiled NIF and don't need the SDK.

## Usage on host (development)

The QNN CPU backend (`libQnnCpu.so`, x86-64) allows exercising the runtime
lifecycle and context introspection without hardware:

```elixir
{:ok, rt} = HexagonTpu.Runtime.create(
  lib_path: Path.join(System.get_env("QNN_HOST_LIB_DIR"), "libQnnCpu.so"),
  system_lib_path: Path.join(System.get_env("QNN_HOST_LIB_DIR"), "libQnnSystem.so")
)
```

Run tests: `mix test` (pure), `mix test --include qnn_host` (needs devenv).

## Quantization

QNN stores affine quant params as `float = (q + offset) * scale`;
`HexagonTpu.TensorInfo` normalizes to the conventional
`zero_point = -offset`. `Graph.execute/3` quantizes f32 inputs and
dequantizes outputs automatically (`quantize: :none` / `dequantize: :none`
opt out for zero-overhead pre-quantized pipelines).

## Observability & guards

- `HexagonTpu.Stats.native/0` — NIF counters: live runtimes/contexts/graphs
  (leak guard: must return to baseline after GC), execute counts/errors, and
  pure QNN `graphExecute` time (`execute_ns_total`/`execute_ns_last`).
- `HexagonTpu.Stats.dirty_utilization/1` — dirty CPU / dirty IO scheduler
  busy share via microstate accounting (inference runs on dirty schedulers).
- `HexagonTpu.Stats.memory/0` — BEAM binary/total memory (output tensors are
  refc binaries).
- Telemetry: `[:hexagon_tpu, :execute, :stop]` per inference with
  `%{duration: native_time}` and `%{graph:, status:}` metadata.
- `{HexagonTpu.Monitor, interval_ms: 30_000}` in your supervision tree emits
  `[:hexagon_tpu, :stats]` periodically and logs warnings on sustained
  resource growth (leak suspicion) or dirty-scheduler saturation.

Leak-guard tests (`test/stats_test.exs`) assert alive counts return to
baseline across create/close and GC-only cycles.

## Releases

Pushing a `v*` tag runs `.github/workflows/release-precompiled.yml`: builds
the `dragon_q6a` NIF with the Nerves toolchain, uploads the precompiled
tarball + `checksum.exs` to the GitHub release, and publishes to Hex
(requires the `HEX_API_KEY` secret).

## License

Apache-2.0. The QAIRT SDK and its runtime libraries are Qualcomm
proprietary — this repository neither vendors nor redistributes them.