Skip to main content

README.md

# CixP1

Elixir NIF bindings for the **CIX P1 (Arm-China "Zhouyi") NPU** on the Orange Pi
6 / 6 Plus, via the CIX **NOE** runtime (`libnoe`).

It is a thin, low-level wrapper: load a pre-compiled model graph, feed raw input
tensors, run inference on the NPU, read raw output tensors. Model
pre/post-processing (image resize, detection decoding, …) stays in your code. An
optional [`Nx`](https://hex.pm/packages/nx) layer converts tensor binaries
to/from `Nx.Tensor`.

## Requirements

Runs only on a Nerves image built from
[`nerves_system_orangepi6`](../nerves_system_orangepi6) **v0.2.0 or later**, which
provides:

- the `aipu` kernel driver (`/dev/aipu`, world-accessible via udev),
- the NOE/AIPU userspace runtime (`libnoe`, `libaipudrv`) on the rootfs **and in
  the staging sysroot** (so this NIF cross-links at firmware build time),
- `LD_LIBRARY_PATH=/usr/share/cix/lib` exported to the BEAM.

Models must be compiled to the NOE `.cix` graph format — either with the **NOE
Compiler** (NOE SDK / AI ModelHub Development Guide) or downloaded pre-built from
the **CIX AI Model Hub**. The Radxa Orion O6 shares this silicon, so the
[Zhouyi NPU tutorial](https://zhouyi-npu-tutorial.readthedocs.io) applies for
model compilation.

## Installation

Add it to a Nerves firmware project that targets `nerves_system_orangepi6`:

```elixir
def deps do
  [
    {:cix_p1_tpu, "~> 0.1"},
    # optional, for the CixP1.Nx helpers:
    {:nx, "~> 0.7"}
  ]
end
```

Tagged releases publish a **precompiled** aarch64 NIF (built with the matched
Nerves toolchain) to GitHub Releases + Hex via `cc_precompiler`, so consumers
don't rebuild the native code — `mix deps.get` downloads it and verifies it
against `checksum.exs`. If no precompiled artifact matches, the NIF builds from
source during `mix firmware` against the system's staging sysroot. On a plain
host (no NOE SDK) the native build is skipped so pure-Elixir compilation and
`mix test` still work.

## Releases

Cut a release by tagging `vX.Y.Z` (matching the `mix.exs` version); the
`Release Precompiled NIF` workflow cross-compiles the NIF with the Nerves
aarch64 toolchain against the system's `libnoe`/`libaipudrv`, uploads the tarball
+ `checksum.exs` to the GitHub release, and publishes to Hex (needs the
`HEX_API_KEY` secret).

## Usage

### One-shot inference

```elixir
{:ok, ctx}   = CixP1.Context.new()
{:ok, graph} = CixP1.Graph.load(ctx, "/data/models/mobilenet.cix")

# inputs is a list of raw binaries, one per input tensor, in order
{:ok, [logits]} = CixP1.run(graph, [image_binary], timeout_ms: 5_000)
```

### Step-by-step (reuse a job for repeated inference)

```elixir
{:ok, ctx}   = CixP1.Context.new()
{:ok, graph} = CixP1.Graph.load(ctx, "/data/models/model.cix")
{:ok, job}   = CixP1.Job.create(graph)

:ok = CixP1.Job.load_input(job, 0, input_binary)
:ok = CixP1.Job.infer(job, 5_000)
{:ok, output_binary} = CixP1.Job.get_output(job, 0)
```

### Inspecting tensors

```elixir
{:ok, 1} = CixP1.Graph.input_count(graph)
{:ok, in_desc}  = CixP1.Graph.input_descriptor(graph, 0)
{:ok, out_desc} = CixP1.Graph.output_descriptor(graph, 0)
# => %{id: _, size: bytes, scale: _, zero_point: _, data_type: :u8 | :f32 | ...}
```

Note: NOE descriptors expose the element **type** and flat **byte size**, not the
logical shape. Track the shape from your model definition (or `noe_get_tensor_shape`
on the `.cix` file offline) and pass it to `CixP1.Nx.to_nx/3`.

### Nx interop (optional)

```elixir
{:ok, [out]} = CixP1.run(graph, [input])
{:ok, desc}  = CixP1.Graph.output_descriptor(graph, 0)

probs =
  out
  |> CixP1.Nx.to_nx(desc, shape: {1, 1000})
  |> CixP1.Nx.dequantize(desc)      # (q - zero_point) * scale; no-op if scale == 0.0
  |> Nx.squeeze()

top1 = probs |> Nx.argmax() |> Nx.to_number()
```

## Architecture

| Layer | Role |
|-------|------|
| `CixP1.Context` | NOE UMD context (opens `/dev/aipu`) |
| `CixP1.Graph`   | a loaded `.cix` graph + tensor descriptors |
| `CixP1.Job`     | one inference invocation (load inputs → infer → read outputs) |
| `CixP1` | `run/2` one-shot convenience |
| `CixP1.Nx` | optional `Nx.Tensor` ↔ binary conversion + dequantization |
| `CixP1.Nif` | raw NIF over `libnoe` (`c_src/cix_p1_nif.c`) — internal |

Resources are reference-counted by the BEAM and form a keep-alive chain
(Job → Graph → Context), so `noe_clean_job` / `noe_unload_graph` /
`noe_deinit_context` run in the correct order regardless of GC timing. Inference
and buffer operations run on dirty schedulers so they never block the BEAM's
normal schedulers.

## Development

```
mix deps.get
mix compile   # native build skipped off-target
mix test      # host: runs pure-Elixir tests; NIF tests are tagged :hardware
```

On-device, run the hardware-tagged tests with `mix test --include hardware`.

### Full off-target compile+link (CI)

`make crossbuild` cross-compiles the NIF for aarch64 and links it against the
**real** `libnoe`/`libaipudrv`, proving the native code fully compiles and every
symbol resolves — not just a syntax check. It clones `nerves_system_orangepi6`
(LFS blobs) into `.nerves-system` for the libraries:

```
make crossbuild                                   # clones the system for blobs
make crossbuild NERVES_SYSTEM_DIR=/path/to/system # reuse an existing checkout
```

Inside the devenv shell the aarch64 toolchain (`CROSS_CC`) is already on PATH,
and `devenv test` runs `mix test` + `make crossbuild`. The resulting `.so` is a
compile/link artifact (built with the nixpkgs cross-glibc) — the **deployable**
binary comes from the Nerves firmware build (see `example/`).

## License

Apache-2.0. The NOE/AIPU runtime libraries and headers are proprietary CIX/Arm
components shipped by the Nerves system, not by this package.