Packages

QNN (Qualcomm AI Engine Direct) NIF bindings for the Hexagon NPU (QCS6490/HTP)

Current section

Files

Jump to
hexagon_tpu c_src hexagon_tpu_nif.cpp
Raw

c_src/hexagon_tpu_nif.cpp

// NIF bindings for the QNN (Qualcomm AI Engine Direct) runtime.
//
// Resource hierarchy: Graph -> Context -> Runtime, chained with
// enif_keep_resource so destructors run leaf-first. Explicit close/1 exists
// for Runtime/Context because freeing HTP handles can take tens of
// milliseconds and GC destructors run on normal schedulers.
#include <erl_nif.h>
#include <atomic>
#include <cstdio>
#include <ctime>
#include <vector>
#include "nif_utils.hpp"
#include "qnn_api.hpp"
#include "qnn_error.hpp"
#include "qnn_meta.hpp"
using erlang::nif::error;
using erlang::nif::make_binary;
using erlang::nif::ok;
// All QNN calls take `lock` for reading; runtime/context close takes it for
// writing. This makes close-during-execute wait instead of freeing handles
// out from under an in-flight graphExecute on another dirty thread. The
// `closed` flags are only trusted under the lock.
struct RuntimeRes {
QnnApi api;
ErlNifRWLock *lock = nullptr;
// QNN requires contexts freed before the backend; runtime_close refuses
// while this is nonzero.
std::atomic<int64_t> live_contexts{0};
bool closed = false;
};
struct ContextRes {
Qnn_ContextHandle_t ctx = nullptr;
NifRes<RuntimeRes> *rt = nullptr;
std::vector<GraphMeta> graphs;
bool closed = false;
};
struct GraphRes {
Qnn_GraphHandle_t graph = nullptr;
NifRes<ContextRes> *ctx = nullptr;
const GraphMeta *meta = nullptr;
ErlNifMutex *mtx = nullptr;
};
template <>
ErlNifResourceType *NifRes<RuntimeRes>::type = nullptr;
template <>
ErlNifResourceType *NifRes<ContextRes>::type = nullptr;
template <>
ErlNifResourceType *NifRes<GraphRes>::type = nullptr;
// ---------------------------------------------------------------------------
// Native stats: leak guards (alive counts must return to baseline after GC)
// and QNN execution accounting. Exposed via the stats/0 NIF.
struct NativeStats {
std::atomic<int64_t> runtimes_alive{0};
std::atomic<int64_t> contexts_alive{0};
std::atomic<int64_t> graphs_alive{0};
std::atomic<uint64_t> runtimes_created{0};
std::atomic<uint64_t> contexts_created{0};
std::atomic<uint64_t> graphs_created{0};
std::atomic<uint64_t> executes{0};
std::atomic<uint64_t> execute_errors{0};
std::atomic<uint64_t> execute_ns_total{0};
std::atomic<uint64_t> execute_ns_last{0};
};
static NativeStats g_stats;
// Atoms used on per-tensor / per-execute paths, cached at on_load.
static struct {
ERL_NIF_TERM none, scale_offset, axis_scale_offset;
ERL_NIF_TERM name, id, qnn_data_type, dims, byte_size, quant;
ERL_NIF_TERM inputs, outputs;
} ATOMS;
static uint64_t monotonic_ns() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<uint64_t>(ts.tv_sec) * 1000000000ull + ts.tv_nsec;
}
// ---------------------------------------------------------------------------
// Internal helpers
static void runtime_do_close(RuntimeRes *rt) {
if (rt && !rt->closed) {
qnn_api_close(rt->api);
rt->closed = true;
}
}
static void context_do_close(ContextRes *ctx) {
if (ctx && !ctx->closed) {
RuntimeRes *rt = ctx->rt ? ctx->rt->val : nullptr;
if (ctx->ctx && rt && !rt->closed && rt->api.fn.contextFree) {
rt->api.fn.contextFree(ctx->ctx, nullptr);
}
if (rt) rt->live_contexts--;
ctx->ctx = nullptr;
ctx->closed = true;
}
}
static ERL_NIF_TERM quant_term(ErlNifEnv *env, const TensorMeta &t) {
switch (t.quant) {
case TensorMeta::Quant::PerTensor:
return enif_make_tuple3(env,
ATOMS.scale_offset,
enif_make_double(env, t.scale_offset.scale),
enif_make_int(env, t.scale_offset.offset));
case TensorMeta::Quant::PerAxis: {
std::vector<ERL_NIF_TERM> pairs;
pairs.reserve(t.axis_scale_offsets.size());
for (const auto &so : t.axis_scale_offsets) {
pairs.push_back(enif_make_tuple2(env,
enif_make_double(env, so.scale),
enif_make_int(env, so.offset)));
}
return enif_make_tuple3(env,
ATOMS.axis_scale_offset,
enif_make_int(env, t.axis),
enif_make_list_from_array(env, pairs.data(), pairs.size()));
}
default:
return ATOMS.none;
}
}
static ERL_NIF_TERM tensor_meta_term(ErlNifEnv *env, const TensorMeta &t) {
std::vector<ERL_NIF_TERM> dims;
dims.reserve(t.dims.size());
for (uint32_t d : t.dims) dims.push_back(enif_make_uint(env, d));
ERL_NIF_TERM map = enif_make_new_map(env);
enif_make_map_put(env, map, ATOMS.name, make_binary(env, t.name), &map);
enif_make_map_put(env, map, ATOMS.id, enif_make_uint(env, t.id), &map);
enif_make_map_put(env, map, ATOMS.qnn_data_type, enif_make_uint(env, t.data_type), &map);
enif_make_map_put(env, map, ATOMS.dims,
enif_make_list_from_array(env, dims.data(), dims.size()), &map);
enif_make_map_put(env, map, ATOMS.byte_size, enif_make_uint64(env, t.byte_size), &map);
enif_make_map_put(env, map, ATOMS.quant, quant_term(env, t), &map);
return map;
}
// Normalize error strings from qnn_api/qnn_meta into terms:
// "tag" -> {:error, :tag}; "tag: detail" -> {:error, {:tag, "detail"}}.
static ERL_NIF_TERM tagged_error(ErlNifEnv *env, const std::string &msg) {
auto pos = msg.find(": ");
if (pos == std::string::npos) {
return error(env, enif_make_atom(env, msg.c_str()));
}
return error(env, enif_make_tuple2(env, enif_make_atom(env, msg.substr(0, pos).c_str()),
make_binary(env, msg.substr(pos + 2))));
}
// Load a whole file into memory. Context binaries can be hundreds of MB;
// this runs on a dirty scheduler.
static bool read_file(const std::string &path, std::vector<uint8_t> &out) {
FILE *f = std::fopen(path.c_str(), "rb");
if (!f) return false;
if (std::fseek(f, 0, SEEK_END) != 0) {
std::fclose(f);
return false;
}
long size = std::ftell(f);
if (size <= 0 || std::fseek(f, 0, SEEK_SET) != 0) {
std::fclose(f);
return false;
}
out.resize(static_cast<size_t>(size));
size_t read = std::fread(out.data(), 1, out.size(), f);
std::fclose(f);
return read == out.size();
}
// ---------------------------------------------------------------------------
// Runtime
static ERL_NIF_TERM runtime_create(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
std::string backend_lib, system_lib;
int log_level = 0;
if (!erlang::nif::get_binary_string(env, argv[0], backend_lib) ||
!erlang::nif::get_binary_string(env, argv[1], system_lib) ||
!enif_get_int(env, argv[2], &log_level)) {
return enif_make_badarg(env);
}
auto *res = NifRes<RuntimeRes>::allocate(env);
if (!res) return error(env, "alloc_failed");
res->val = new RuntimeRes();
// Counters move with res->val: the dtor decrements alive whenever val is
// set, so failed creates must have incremented it too.
g_stats.runtimes_alive++;
g_stats.runtimes_created++;
res->val->lock = enif_rwlock_create(const_cast<char *>("hexagon_tpu_runtime"));
if (!res->val->lock) {
enif_release_resource(res);
return error(env, "alloc_failed");
}
std::string err = qnn_api_open(res->val->api, backend_lib, system_lib, log_level);
if (!err.empty()) {
ERL_NIF_TERM reason = tagged_error(env, err);
enif_release_resource(res);
return reason;
}
ERL_NIF_TERM term = enif_make_resource(env, res);
enif_release_resource(res);
return ok(env, term);
}
static ERL_NIF_TERM runtime_info(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
auto *res = NifRes<RuntimeRes>::get(env, argv[0]);
if (!res || !res->val) return enif_make_badarg(env);
ERL_NIF_TERM map = enif_make_new_map(env);
enif_make_map_put(env, map, enif_make_atom(env, "backend_lib"),
make_binary(env, res->val->api.backend_lib_name), &map);
enif_make_map_put(env, map, enif_make_atom(env, "build_id"),
make_binary(env, res->val->api.build_id), &map);
enif_make_map_put(env, map, enif_make_atom(env, "has_device"),
enif_make_atom(env, res->val->api.device ? "true" : "false"), &map);
enif_make_map_put(env, map, enif_make_atom(env, "closed"),
enif_make_atom(env, res->val->closed ? "true" : "false"), &map);
return map;
}
static ERL_NIF_TERM runtime_close(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
auto *res = NifRes<RuntimeRes>::get(env, argv[0]);
if (!res || !res->val) return enif_make_badarg(env);
enif_rwlock_rwlock(res->val->lock);
if (res->val->live_contexts.load() > 0) {
enif_rwlock_rwunlock(res->val->lock);
return error(env, "contexts_alive");
}
runtime_do_close(res->val);
enif_rwlock_rwunlock(res->val->lock);
return ok(env);
}
// ---------------------------------------------------------------------------
// Context
static ERL_NIF_TERM context_create(ErlNifEnv *env,
NifRes<RuntimeRes> *rt_res,
const void *buf,
uint64_t size) {
RuntimeRes *rt = rt_res->val;
auto *res = NifRes<ContextRes>::allocate(env);
if (!res) return error(env, "alloc_failed");
res->val = new ContextRes();
g_stats.contexts_alive++;
g_stats.contexts_created++;
enif_rwlock_rlock(rt->lock);
if (rt->closed) {
enif_rwlock_runlock(rt->lock);
enif_release_resource(res);
return error(env, "runtime_closed");
}
if (!rt->api.fn.contextCreateFromBinary) {
enif_rwlock_runlock(rt->lock);
enif_release_resource(res);
return error(env, "missing_fn");
}
std::string err = qnn_parse_binary_info(rt->api, buf, size, res->val->graphs);
if (!err.empty()) {
enif_rwlock_runlock(rt->lock);
ERL_NIF_TERM reason = tagged_error(env, err);
enif_release_resource(res);
return reason;
}
Qnn_ErrorHandle_t qerr = rt->api.fn.contextCreateFromBinary(
rt->api.backend, rt->api.device, nullptr, buf, size, &res->val->ctx, nullptr);
if (qerr != QNN_SUCCESS) {
// Error term must be built under the lock: errorGetMessage calls
// into the backend library, which close/dlclose could unmap.
ERL_NIF_TERM reason = qnn_error_term(env, rt->api, qerr);
enif_rwlock_runlock(rt->lock);
enif_release_resource(res);
return reason;
}
rt->live_contexts++;
enif_rwlock_runlock(rt->lock);
res->val->rt = rt_res;
enif_keep_resource(rt_res);
ERL_NIF_TERM term = enif_make_resource(env, res);
enif_release_resource(res);
return ok(env, term);
}
static ERL_NIF_TERM context_create_from_file(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
(void)argc;
auto *rt_res = NifRes<RuntimeRes>::get(env, argv[0]);
std::string path;
if (!rt_res || !rt_res->val || !erlang::nif::get_binary_string(env, argv[1], path)) {
return enif_make_badarg(env);
}
std::vector<uint8_t> buf;
if (!read_file(path, buf)) {
return error(env, "file_read_failed");
}
return context_create(env, rt_res, buf.data(), buf.size());
}
static ERL_NIF_TERM context_create_from_binary(ErlNifEnv *env, int argc,
const ERL_NIF_TERM argv[]) {
(void)argc;
auto *rt_res = NifRes<RuntimeRes>::get(env, argv[0]);
ErlNifBinary bin;
if (!rt_res || !rt_res->val || !enif_inspect_binary(env, argv[1], &bin)) {
return enif_make_badarg(env);
}
return context_create(env, rt_res, bin.data, bin.size);
}
static ERL_NIF_TERM context_graphs_info(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
auto *res = NifRes<ContextRes>::get(env, argv[0]);
if (!res || !res->val) return enif_make_badarg(env);
std::vector<ERL_NIF_TERM> graphs;
graphs.reserve(res->val->graphs.size());
for (const auto &g : res->val->graphs) {
std::vector<ERL_NIF_TERM> ins, outs;
ins.reserve(g.inputs.size());
outs.reserve(g.outputs.size());
for (const auto &t : g.inputs) ins.push_back(tensor_meta_term(env, t));
for (const auto &t : g.outputs) outs.push_back(tensor_meta_term(env, t));
ERL_NIF_TERM map = enif_make_new_map(env);
enif_make_map_put(env, map, ATOMS.name, make_binary(env, g.name), &map);
enif_make_map_put(env, map, ATOMS.inputs,
enif_make_list_from_array(env, ins.data(), ins.size()), &map);
enif_make_map_put(env, map, ATOMS.outputs,
enif_make_list_from_array(env, outs.data(), outs.size()), &map);
graphs.push_back(map);
}
return enif_make_list_from_array(env, graphs.data(), graphs.size());
}
static ERL_NIF_TERM context_close(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
auto *res = NifRes<ContextRes>::get(env, argv[0]);
if (!res || !res->val) return enif_make_badarg(env);
RuntimeRes *rt = res->val->rt ? res->val->rt->val : nullptr;
if (rt) enif_rwlock_rwlock(rt->lock);
context_do_close(res->val);
if (rt) enif_rwlock_rwunlock(rt->lock);
return ok(env);
}
// ---------------------------------------------------------------------------
// Graph
static ERL_NIF_TERM graph_retrieve(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
auto *ctx_res = NifRes<ContextRes>::get(env, argv[0]);
std::string name;
if (!ctx_res || !ctx_res->val || !erlang::nif::get_binary_string(env, argv[1], name)) {
return enif_make_badarg(env);
}
ContextRes *ctx = ctx_res->val;
RuntimeRes *rt = ctx->rt->val;
const GraphMeta *meta = nullptr;
for (const auto &g : ctx->graphs) {
if (g.name == name) {
meta = &g;
break;
}
}
if (!meta) return error(env, "graph_not_found");
// byte_size == 0 means a dtype we can't size (4-bit packed, string, ...).
// Executing would hand QNN zero-length buffers to write into.
for (const auto &tensors : {&meta->inputs, &meta->outputs}) {
for (const auto &t : *tensors) {
if (t.byte_size == 0) {
return error(env, enif_make_tuple2(env,
enif_make_atom(env, "unsupported_tensor_type"),
make_binary(env, t.name)));
}
}
}
enif_rwlock_rlock(rt->lock);
if (ctx->closed || rt->closed || !rt->api.fn.graphRetrieve) {
bool ctx_closed = ctx->closed;
bool rt_closed = rt->closed;
enif_rwlock_runlock(rt->lock);
if (ctx_closed) return error(env, "context_closed");
if (rt_closed) return error(env, "runtime_closed");
return error(env, "missing_fn");
}
Qnn_GraphHandle_t handle = nullptr;
Qnn_ErrorHandle_t qerr = rt->api.fn.graphRetrieve(ctx->ctx, name.c_str(), &handle);
if (qerr != QNN_SUCCESS) {
ERL_NIF_TERM reason = qnn_error_term(env, rt->api, qerr);
enif_rwlock_runlock(rt->lock);
return reason;
}
enif_rwlock_runlock(rt->lock);
auto *res = NifRes<GraphRes>::allocate(env);
if (!res) return error(env, "alloc_failed");
res->val = new GraphRes();
g_stats.graphs_alive++;
g_stats.graphs_created++;
res->val->graph = handle;
res->val->meta = meta;
res->val->mtx = enif_mutex_create(const_cast<char *>("hexagon_tpu_graph"));
if (!res->val->mtx) {
enif_release_resource(res);
return error(env, "alloc_failed");
}
res->val->ctx = ctx_res;
enif_keep_resource(ctx_res);
ERL_NIF_TERM term = enif_make_resource(env, res);
enif_release_resource(res);
return ok(env, term);
}
// Build a v1 execute tensor descriptor from cached metadata. `dims` must
// outlive the QNN call (it points into the context's GraphMeta).
static Qnn_Tensor_t make_exec_tensor(const TensorMeta &meta, bool is_input, void *data,
uint32_t data_size) {
Qnn_Tensor_t t = QNN_TENSOR_INIT;
t.version = QNN_TENSOR_VERSION_1;
t.v1.id = meta.id;
t.v1.name = meta.name.c_str();
t.v1.type = is_input ? QNN_TENSOR_TYPE_APP_WRITE : QNN_TENSOR_TYPE_APP_READ;
t.v1.dataFormat = QNN_TENSOR_DATA_FORMAT_DENSE;
t.v1.dataType = meta.data_type;
if (meta.quant == TensorMeta::Quant::PerTensor) {
t.v1.quantizeParams.encodingDefinition = QNN_DEFINITION_DEFINED;
t.v1.quantizeParams.quantizationEncoding = QNN_QUANTIZATION_ENCODING_SCALE_OFFSET;
t.v1.quantizeParams.scaleOffsetEncoding.scale = meta.scale_offset.scale;
t.v1.quantizeParams.scaleOffsetEncoding.offset = meta.scale_offset.offset;
} else if (meta.quant == TensorMeta::Quant::PerAxis) {
t.v1.quantizeParams.encodingDefinition = QNN_DEFINITION_DEFINED;
t.v1.quantizeParams.quantizationEncoding = QNN_QUANTIZATION_ENCODING_AXIS_SCALE_OFFSET;
t.v1.quantizeParams.axisScaleOffsetEncoding.axis = meta.axis;
t.v1.quantizeParams.axisScaleOffsetEncoding.numScaleOffsets =
static_cast<uint32_t>(meta.axis_scale_offsets.size());
t.v1.quantizeParams.axisScaleOffsetEncoding.scaleOffset =
const_cast<Qnn_ScaleOffset_t *>(meta.axis_scale_offsets.data());
}
t.v1.rank = static_cast<uint32_t>(meta.dims.size());
t.v1.dimensions = const_cast<uint32_t *>(meta.dims.data());
t.v1.memType = QNN_TENSORMEMTYPE_RAW;
t.v1.clientBuf.data = data;
t.v1.clientBuf.dataSize = data_size;
return t;
}
static ERL_NIF_TERM graph_execute(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
(void)argc;
auto *graph_res = NifRes<GraphRes>::get(env, argv[0]);
if (!graph_res || !graph_res->val) return enif_make_badarg(env);
GraphRes *g = graph_res->val;
ContextRes *ctx = g->ctx->val;
RuntimeRes *rt = ctx->rt->val;
const auto &in_meta = g->meta->inputs;
const auto &out_meta = g->meta->outputs;
// Inputs: zero-copy views into the caller's binaries. The terms stay
// live for the duration of this (synchronous, dirty) call.
unsigned list_len = 0;
if (!enif_get_list_length(env, argv[1], &list_len) || list_len != in_meta.size()) {
return error(env, "input_count_mismatch");
}
std::vector<Qnn_Tensor_t> inputs;
inputs.reserve(in_meta.size());
ERL_NIF_TERM list = argv[1], head, tail;
for (size_t i = 0; i < in_meta.size(); i++) {
if (!enif_get_list_cell(env, list, &head, &tail)) {
return enif_make_badarg(env);
}
list = tail;
ErlNifBinary bin;
if (!enif_inspect_binary(env, head, &bin)) {
return enif_make_badarg(env);
}
if (in_meta[i].byte_size != 0 && bin.size != in_meta[i].byte_size) {
return error(env,
enif_make_tuple3(env, enif_make_atom(env, "input_size_mismatch"),
enif_make_uint64(env, i),
enif_make_uint64(env, in_meta[i].byte_size)));
}
inputs.push_back(make_exec_tensor(in_meta[i], true, bin.data,
static_cast<uint32_t>(bin.size)));
}
// Outputs: owned binaries handed to QNN to fill, then transferred to
// the returned terms without copying.
std::vector<ErlNifBinary> out_bins(out_meta.size());
std::vector<Qnn_Tensor_t> outputs;
outputs.reserve(out_meta.size());
for (size_t i = 0; i < out_meta.size(); i++) {
if (!enif_alloc_binary(out_meta[i].byte_size, &out_bins[i])) {
for (size_t j = 0; j < i; j++) enif_release_binary(&out_bins[j]);
return error(env, "alloc_failed");
}
outputs.push_back(make_exec_tensor(out_meta[i], false, out_bins[i].data,
static_cast<uint32_t>(out_bins[i].size)));
}
enif_mutex_lock(g->mtx);
enif_rwlock_rlock(rt->lock);
if (ctx->closed || rt->closed || !rt->api.fn.graphExecute) {
bool ctx_closed = ctx->closed;
bool rt_closed = rt->closed;
enif_rwlock_runlock(rt->lock);
enif_mutex_unlock(g->mtx);
for (auto &b : out_bins) enif_release_binary(&b);
if (ctx_closed) return error(env, "context_closed");
if (rt_closed) return error(env, "runtime_closed");
return error(env, "missing_fn");
}
uint64_t t0 = monotonic_ns();
Qnn_ErrorHandle_t qerr = rt->api.fn.graphExecute(
g->graph, inputs.data(), static_cast<uint32_t>(inputs.size()), outputs.data(),
static_cast<uint32_t>(outputs.size()), nullptr, nullptr);
uint64_t elapsed = monotonic_ns() - t0;
// Error term is built under the lock: errorGetMessage calls into the
// backend library, which close/dlclose could unmap.
ERL_NIF_TERM error_reason = 0;
if (qerr != QNN_SUCCESS) {
error_reason = qnn_error_term(env, rt->api, qerr);
}
enif_rwlock_runlock(rt->lock);
enif_mutex_unlock(g->mtx);
g_stats.executes++;
g_stats.execute_ns_total += elapsed;
g_stats.execute_ns_last = elapsed;
if (qerr != QNN_SUCCESS) {
g_stats.execute_errors++;
for (auto &b : out_bins) enif_release_binary(&b);
return error_reason;
}
std::vector<ERL_NIF_TERM> out_terms;
out_terms.reserve(out_bins.size());
for (auto &b : out_bins) {
out_terms.push_back(enif_make_binary(env, &b));
}
return ok(env, enif_make_list_from_array(env, out_terms.data(), out_terms.size()));
}
// ---------------------------------------------------------------------------
// Resource destructors and registration
// Destructors run when the refcount hits zero, so no other thread can hold
// the runtime lock here — locking is still taken for the release/acquire
// pairing with earlier lock holders.
static void runtime_dtor(ErlNifEnv *, void *obj) {
auto *res = static_cast<NifRes<RuntimeRes> *>(obj);
if (res->val) {
if (res->val->lock) {
enif_rwlock_rwlock(res->val->lock);
runtime_do_close(res->val);
enif_rwlock_rwunlock(res->val->lock);
enif_rwlock_destroy(res->val->lock);
} else {
runtime_do_close(res->val);
}
delete res->val;
res->val = nullptr;
g_stats.runtimes_alive--;
}
}
static void context_dtor(ErlNifEnv *, void *obj) {
auto *res = static_cast<NifRes<ContextRes> *>(obj);
if (res->val) {
RuntimeRes *rt = res->val->rt ? res->val->rt->val : nullptr;
if (rt) enif_rwlock_rwlock(rt->lock);
context_do_close(res->val);
if (rt) enif_rwlock_rwunlock(rt->lock);
if (res->val->rt) enif_release_resource(res->val->rt);
delete res->val;
res->val = nullptr;
g_stats.contexts_alive--;
}
}
static void graph_dtor(ErlNifEnv *, void *obj) {
auto *res = static_cast<NifRes<GraphRes> *>(obj);
if (res->val) {
// Graph handles are owned by the context; nothing to free in QNN.
if (res->val->mtx) enif_mutex_destroy(res->val->mtx);
if (res->val->ctx) enif_release_resource(res->val->ctx);
delete res->val;
res->val = nullptr;
g_stats.graphs_alive--;
}
}
// stats/0 — native resource + execution accounting (leak guard).
static ERL_NIF_TERM stats(ErlNifEnv *env, int, const ERL_NIF_TERM[]) {
ERL_NIF_TERM map = enif_make_new_map(env);
auto put_i64 = [&](const char *k, int64_t v) {
enif_make_map_put(env, map, enif_make_atom(env, k), enif_make_int64(env, v), &map);
};
auto put_u64 = [&](const char *k, uint64_t v) {
enif_make_map_put(env, map, enif_make_atom(env, k), enif_make_uint64(env, v), &map);
};
put_i64("runtimes_alive", g_stats.runtimes_alive.load());
put_i64("contexts_alive", g_stats.contexts_alive.load());
put_i64("graphs_alive", g_stats.graphs_alive.load());
put_u64("runtimes_created", g_stats.runtimes_created.load());
put_u64("contexts_created", g_stats.contexts_created.load());
put_u64("graphs_created", g_stats.graphs_created.load());
put_u64("executes", g_stats.executes.load());
put_u64("execute_errors", g_stats.execute_errors.load());
put_u64("execute_ns_total", g_stats.execute_ns_total.load());
put_u64("execute_ns_last", g_stats.execute_ns_last.load());
return map;
}
static int on_load(ErlNifEnv *env, void **, ERL_NIF_TERM) {
ATOMS.none = enif_make_atom(env, "none");
ATOMS.scale_offset = enif_make_atom(env, "scale_offset");
ATOMS.axis_scale_offset = enif_make_atom(env, "axis_scale_offset");
ATOMS.name = enif_make_atom(env, "name");
ATOMS.id = enif_make_atom(env, "id");
ATOMS.qnn_data_type = enif_make_atom(env, "qnn_data_type");
ATOMS.dims = enif_make_atom(env, "dims");
ATOMS.byte_size = enif_make_atom(env, "byte_size");
ATOMS.quant = enif_make_atom(env, "quant");
ATOMS.inputs = enif_make_atom(env, "inputs");
ATOMS.outputs = enif_make_atom(env, "outputs");
NifRes<RuntimeRes>::type =
enif_open_resource_type(env, nullptr, "hexagon_tpu_runtime", runtime_dtor,
(ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER),
nullptr);
NifRes<ContextRes>::type =
enif_open_resource_type(env, nullptr, "hexagon_tpu_context", context_dtor,
(ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER),
nullptr);
NifRes<GraphRes>::type =
enif_open_resource_type(env, nullptr, "hexagon_tpu_graph", graph_dtor,
(ErlNifResourceFlags)(ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER),
nullptr);
if (!NifRes<RuntimeRes>::type || !NifRes<ContextRes>::type || !NifRes<GraphRes>::type) {
return -1;
}
return 0;
}
static int on_upgrade(ErlNifEnv *env, void **priv, void **, ERL_NIF_TERM info) {
return on_load(env, priv, info);
}
static ErlNifFunc nif_functions[] = {
{"runtime_create", 3, runtime_create, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"runtime_info", 1, runtime_info, 0},
{"runtime_close", 1, runtime_close, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"context_create_from_file", 2, context_create_from_file, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"context_create_from_binary", 2, context_create_from_binary, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"context_graphs_info", 1, context_graphs_info, 0},
{"context_close", 1, context_close, ERL_NIF_DIRTY_JOB_IO_BOUND},
{"graph_retrieve", 2, graph_retrieve, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"graph_execute", 2, graph_execute, ERL_NIF_DIRTY_JOB_CPU_BOUND},
{"stats", 0, stats, 0},
};
ERL_NIF_INIT(Elixir.HexagonTpu.Nif, nif_functions, on_load, nullptr, on_upgrade, nullptr)