taconite-adaface
An embeddable Rust library, with no crates.io dependencies, that runs
AdaFace IR face embedders (IR-18, IR-101) on the AIE NPU. It replays a bundle — compiled conv kernels, packed
weights, and the network's steps in order, in the format every IRON bundle
shares (read with the std-only
taconite-bundle) — exported
ahead of time by IRON's
iron/applications/adaface_ir18/export_ir18.py /
iron/applications/adaface_ir101/export_ir101.py (iron acts purely as an
AOT compiler). IRON's run_ir18 / run_ir101 binaries are thin CLI
wrappers over this crate.
Kernels run through taconite, over
either of its two NPU paths:
- XRT (the default
xrtfeature): builds against XRT's headers and library. - Direct (
directfeature): straight to theamdxdnakernel driver's ioctls on/dev/accel/accel0. It needs no XRT to build or run, and it gives the same embeddings as XRT at the same speed. It takes precedence when both features are on.
= { = "0.1", = false, = ["direct"] }
taconite_adaface::BACKEND names the path a build uses.
Prebuilt NPU2 bundles are on Hugging Face:
brishen/iron-adaface-ir18-npu2
and
brishen/iron-adaface-ir101-npu2.
Using it from another Rust app
[]
= "0.1"
use IrEmbedder;
Everything returns Result<_, Error> — the library never prints, panics on
bad bundles, or exits the host process.
Performance model
IrEmbedder::load reads every weight into RAM and checks every step against
them (a malformed bundle fails here, naming the manifest line). The first
embed() creates one hardware context per distinct conv kernel (16 for
IR-18/IR-101 — exactly the NPU2's concurrent-context cap); the session keeps
them all resident, so every
later forward reloads nothing (measured on NPU2, either path: IR-18
~0.2 s, IR-101 ~0.5 s warm). This is the whole point of the runtime — so:
- Construct one
IrEmbedderper process and reuse it. A second simultaneous session fights over the 16-context cap and forces reloads. IrEmbedderisSendbut notSync: move it into a worker thread, or share it behind aMutex. Forwards are serialized either way — the NPU path is one hardware queue.
TACONITE_TIMING=1 prints load/run timing and cache hits/evictions when
the embedder drops; TACONITE_CTX_CACHE overrides the resident-context cap
(default 16).
Build requirements
-
Direct (
--no-default-features --features direct): nothing beyond Rust. No XRT, no C++ step, no rpath. -
XRT (the default): XRT headers/libs at
$XRT_ROOT(default/opt/xilinx/xrt) andg++at build time —taconite'sbuild.rscompiles its C++ shim and linkslibxrt_coreutil. Both dependencies (taconite,taconite-bundle) are std-only, and nothing needs build dependencies. -
With XRT, the link flags propagate to your binary automatically. The rpath does not (cargo drops
rustc-link-argacross crates): either run with XRT'ssetup.shsourced (LD_LIBRARY_PATH), or emit the rpaths in your ownbuild.rs—$ORIGIN/libfor the portable layout below, plus the installed-XRT libdir this crate publishes via itslinks = "taconite_adaface"key (this is what therun_ir*binaries do):// build.rs of your app -
At run time: the NPU device (
/dev/accel/accel0, amdxdna driver) and the bundle directory. The bundle is plain data —manifest.txt,kernels/(~10 MB of xclbins/insts) andtensors.bin(the weights and reference tensors; 47 MB in all for IR-18, 126 MB for IR-101); ship it next to your binary and pass its path toIrEmbedder::load.
Portable deployment (no XRT install on the target)
The simplest route is the direct feature: the binary links only glibc
and libgcc_s, so it and the bundle are the whole deployment. For an XRT
build, read on.
Static linking of XRT is not possible with stock XRT: the NPU backend
(libxrt_driver_xdna.so, from the amd/xdna-driver repo) is shared-only and
is discovered at run time — XRT locates its root via dladdr() on
libxrt_coreutil.so (on Linux XILINX_XRT is ignored), then scans that
same lib/ directory for libxrt_driver_*.so.2 and dlopens them. But
that exact mechanism makes a fully self-contained app directory work with
no install, no environment variables:
myapp/
run_ir101 (rpath $ORIGIN/lib)
lib/libxrt_coreutil.so.2
lib/libxrt_driver_xdna.so.2
lib/libxrt_core.so.2 (dependency of the plugin)
bundle/...
package_portable.sh <binary> <outdir> [bundle_dir] (shipped in this
crate's source) assembles it from an installed XRT ($XRT_ROOT, default /opt/xilinx/xrt).
The lib/ subdirectory is load-bearing — XRT strips the lib component
from coreutil's location to find its root, so the .so files must not sit
beside the binary.
Verified on NPU2: the packaged directory runs IR-101 inference in a stock
ubuntu:24.04 container (no /opt/xilinx, no XRT packages, no
LD_LIBRARY_PATH) with identical results. What the target still needs, and
what can never be linked in: the amdxdna kernel driver + NPU firmware,
access to /dev/accel/accel0, a sufficient memlock limit, and distro
basics (glibc, libstdc++, libuuid).
API summary
| item | purpose |
|---|---|
IrEmbedder::load(dir) |
read and check the bundle, open the NPU device |
embed(&mut self, chw: &[f32]) |
embed a caller image (pads channels, rounds to bf16 round-to-nearest-even — bit-identical to torch) |
embed_recorded(&mut self) |
replay the bundle's recorded input (self-check / benchmark) |
input_dims() / embed_dim() / model() / num_conv_dispatches() |
bundle properties |
cosine(a, b) |
embedding similarity |
cli(net_name) |
the whole run_ir* CLI, built on the API above |
BACKEND |
the NPU path this build uses: "xrt" or "direct" |
Error |
Io / Plan / Xrt (an NPU error on either path) / Input |
Correctness
The exporter records the steps by running the Python NPU implementation, so
the steps and Python share numerics; ref.npu_embedding (recorder embedding)
and ref.cpu_embedding (fp32 CPU reference) ride along in the bundle, and the
CLI checks both every run. ref.input_chw (the raw f32 input) additionally routes
through the public embed() API — its pad + round + tile path reproduces
the recorded replay bit-for-bit (verified cosine 1.00000 on NPU2 for both
IR-18 and IR-101; cosine vs the fp32 reference is 0.9993 / 0.9974).
IRON's iron/applications/adaface_ir18/README.md describes how the network
maps onto NPU kernels and why the glue/head run on the CPU.